精华内容
下载资源
问答
  • 本文主要分析内存以及I/O相关的系统调用和库函数的实现原理,根据原理给出在使用过程中需要注意的问题优化的侧重点,本文涉及到的系统调用包括readahead,pread/pwrite,read/write,mmap,readv/writev,sendfile...
    本文主要分析内存以及I/O相关的系统调用和库函数的实现原理,根据原理给出在使用过程中需要注意的问题和优化的侧重点,本文涉及到的系统调用包括readahead,pread/pwrite,read/write,mmap,readv/writev,sendfile,fsync/fdatasync/msync,shmget,malloc。 
    

           本文先简单介绍应用程序对内存的使用以及I/O系统对内存的使用的基本原理,这对理解上述系统调用和库函数的实现有很大帮助。

     

    1 内存管理基础

           Linux对物理内存的管理是以页为单位的,通常页大小为4KB,Linux在初始化时为所有物理内存也分配了管理数据结构,管理所有物理页面。

           每一个应用程序有独立的地址空间,当然这个地址是虚拟的,通过应用程序的页表可以把虚拟地址转化为实际的物理地址进行操作,虽然系统可以实现从虚拟地址到物理地址的转换,但并非应用程序的每一块虚拟内存都对应一块物理内存。Linux使用一种按需分配的策略为应用程序分配物理内存,这种按需分配是使用缺页异常实现的。比如一个应用程序动态分配了10MB的内存,这些内存在分配时只是在应用程序的虚拟内存区域管理结构中表示这一区间的地址已经被占用,内核此时并没有为之分配物理内存,而是在应用程序使用(读写)该内存区时,发现该内存地址对应得物理内存并不存在,此时产生缺页异常,促使内核为当前访问的虚拟内存页分配一个物理内存页。

           一个已经分配给应用程序的物理页,在某些情况下也会被系统回收作为其他用途,比如对于上述的动态分配的内存,其内容可能被换到交换分区,系统暂时回收物理页面,当应用程序再次使用这个内存页时,系统再分配物理页面,把该页的内容从交换分区上换回到这个物理页,再重新建立页表映射关系。不同类型的虚拟内存页对应的物理内存的分配回收处理过程是不同的,在分析具体系统调用细节时,我们再做详细说明。

     

    2 文件系统I/O原理

           操作系统I/O部分不仅涉及到对普通块设备的操作,也涉及到对字符设备和网络设备的操作,本文只涉及对普通块设备的描述。

           应用程序对文件的操作基本可以通过两种方式实现:普通的read/write和mmap方式,但这两种方式都并不是应用程序在读写文件内容时直接操作块设备(有一些特殊的例外情况),而是经过了操作系统层的page cache,即,无论应用程序以哪种方式读文件数据时,都是由操作系统把这部分文件数据加载到内核层,应用程序再通过不同的方式操作在内存中的文件数据,写的过程也一样,应用程序实际上只是把数据写到文件在内存中所对应的页上,然后在一定的时机或强行回写到块设备上。

           我们需要对page cache作一些说明,page cache可以理解为对所有文件数据的缓冲,在一般情况下,对文件操作都需要通过page cache这个中间层(特殊情况我们下面会描述),page cache并不单单只是一个数据中转层,在page cache层,内核对数据做了有效的管理,暂时不使用的数据在内存允许的情况下仍然放在page cache中,所有的应用程序共用一个page cache,不同的应用程序对同一块文件数据的访问,或者一个应用程序对一块数据的多次访问都不需要多次访问块设备获得,这样就加快了I/O操作的性能。

           不同的系统调用对文件数据的访问区别在于page cache之上对数据的访问方式的不同,数据在page cache和块设备之间的操作过程是基本类似的。这种区别主要体现在read/write方式和mmap方式。它们各自得细节我们下面会分别描述。

    3 readahead

           在描述了page cache的原理和功能之后,readahead就比较容易理解了,当使用系统调用read读取文件部分数据时,如果数据没有在page cache中,就需要从块设备读取对应数据,对于像磁盘这样的块设备,寻道是最耗时的操作,读一小块数据和读一大块连续数据所花的时间相差不大,但如果这一大块数据分多次读取,就需要多次寻道,这样花费的时间就比较长。

           readahead是基于这样的策略:在需要读取一块数据的时候,如果后继的操作是连续读,可以在多读一些数据到page cache中,这样下次访问的连续数据的时候,这些数据已经在page cache中了,就无需I/O操作,这样会大大提高数据访问的效率。

           Linux的readahead分为自动模式和用户强制模式,自动预读是指在read系统调用的时候,如果需要从块设备传输数据,系统会自动根据当前的状态设置预读的数据的大小,启用预读过程。每次预读的数据的大小是动态调整的,调整地原则是根据预读后的命中情况适当扩大或缩小预读大小。每次预读的默认大小是可以设置的,而且不同的块设备可以有不同的默认预读大小,察看和设置块设备默认预读大小都可以通过blockdev命令。

           这种自动模式的预读机制在每次I/O操作前是都会被启用,所以预读默认大小的设置对性能有一些影响,如果是大量随机读操作,在这种情况下就需要让预读值调小, 但并不是越小越好,一般情况下需要估算一下应用程序平均每次read请求读取的数据量的平均大小,将预读值设成比平均大小稍大一些比较合适;如果是大量顺序读操作,则预读值可以调大一点(对于使用RAID的情况下,预读值的设置还要参考条带大小和条带数)。

           在自动预读模式中需要注意的问题还有,如果文件本身有许多很小的碎片,即使是连续读,而且也设置了较大的预读值,其效率也不会太高,因为如果一次被读取的数据在磁盘中不连续的话,仍然不可避免磁盘寻道,所以预读起的作用就不大了。

           Linux提供一个readahead的系统调用设置对文件进行强制预读,这个操作是把数据从块设备加载到page cache中,可以提高之后对文件数据访问的速度,用户可以根据自己的需要决定是否使用强制预读。

     

    4 read/write

           read/write是读写I/O的基本过程,除了mmap之外,其他I/O读写系统调用的基本原理和调用过程都是和read/write一样的。

           read过程:把需要读取得数据转换成对应的页,对需要读入的每一个页执行如下过程:首先调用page_cache_readahead(如果预读打开),根据当前预读的状态和执行预读策略(预读状态结构根据命中情况和读模式动态调整,预读策略也动态调整),预读过程会进行I/O操作也可能不会,预读过程完毕之后,首先检查page cache中是否已经有所需数据,如果没有,说明预读没有命中,调用handle_ra_miss调整预读策略,进行I/O操作把该页数据读入内存并加入page cache,当该页数据读入page cache之后(或者之前就在page cache中),标记该页mark_page_accessed,然后把该页数据拷贝到应用程序地址空间。

           write过程:和read过程一样,需要把需要写的数据转换成对应页,从应用程序地址空间把数据拷贝到对应页,并标记该页状态为dirty,调用mark_page_accessed如果没有指定为同步写,写操作至此就返回了。如果文件在打开时指定了O_SYNC,系统会把本次写过程所有涉及到的dirty页回写到块设备中,这个过程是阻塞的。关于dirty页的同步在分析fsync/fdatasync/msync时我们再具体说明。

           特殊情况:如果应用程序在打开文件时指定了O_DIRECT,操作系统在读写文件时会完全绕过page cache,读的时候数据直接从块设备传送到应用程序指定的缓存中,写的时候数据也是直接从应用程序指定的缓存中写到块设备中,由于没有经过page cache层,这种方式的写总是同步写。

     

    5 mmap

           mmap的用途很广泛,不仅可以把文件映射到内存地址空间读写文件,也可以用mmap实现共享内存,malloc分配内存是也是用了mmap,本节我们先讨论使用mmap读写文件的实现。

           每个进程对虚拟内存都是通过分区域管理的,在虚拟内存分配时,为不同的用途划分不同的虚拟内存区域,这些虚拟内存区域在分配之初并没有为止分配对应的物理内存,而只是分配和设置了管理结构,当进程使用到某个区域的内存,而其又没有对应的物理内存时,系统产生缺页异常,在缺页异常中,系统根据这块内存对应的虚拟内存管理结构为之分配物理内存,在必要情况下(如mmap)加载数据到这块物理内存,建立虚拟内存到物理内存的对应关系,然后进程可以继续访问刚才的虚拟内存。

           mmap的实现也是基于上述原理,在使用mmap映射某个文件(或者文件的一部分)到进程的地址空间时,并没有加载文件的数据,而只是在进程的虚拟地址空间划分出一块区域,标记这块区域用于映射到文件的数据区域,mmap的操作就完成了。

           当进程试图读或者写文件映射区域时,如果没有对应的物理页面,系统发生缺页异常并进入缺页异常处理程序,缺页异常处理程序根据该区域内存的类型使用不同的策略解决缺页。对于使用mmap映射文件的虚拟内存区域,处理程序首先找到相关的文件的管理数据结构,确定所需页面对应的文件偏移,此时需要从文件中把对应数据加载到page_cache中,与read系统调用流程不同的是,在加载的过程中如果虚拟内存区域管理结构设置了VM_RAND_READ标志,系统只是把所需的页面数据加载,如果设置了VM_SEQ_READ标志,系统会进行和read系统调用相同预读过程,至此应用程序所需的页面已经在page cache中了,系统调整页表把物理页面对应到应用程序的地址空间。mmap对缺页的处理没有读和写的区别,无论是读还是写造成的缺页异常都要执行上述过程。

           虚拟内存区域管理结构的VM_RAND_READ标志和VM_SEQ_READ标志可以使用madvise系统调用调整。

           使用mmap读写文件需要注意的问题:当读写映射的内存区域的物理页面不存在时,发生缺页异常时系统才能进入内核态,如果物理页面存在,应用程序在用户态直接操作内存,不会进入内核态,大家注意到在调用read/write系统调用时,系统对涉及到的页面都调用了mark_page_accessed函数,mark_page_accessed可以标记物理页面的活动状态,活动的页面就不容易被回收,而是用mmap读文件不产生缺页异常时不能进入内核态,就无法标记页面的活动状态,这样页面就容易被系统回收(进入缺页异常处理时也只是对新分配所缺页面调用了mark_page_accessed)。除此之外,在写该内存区域时,如果不进入内核态也无法标记所写的物理页面为dirty(只用把页表项的dirty位置位),这个问题我们会在后面的msync说明中详细描述。

     

    6 pread/pwrite,readv/writev

           这几个系统调用在内核中的实现和read/write区别不大,只是参数不同而已,在read/write中使用的是文件默认偏移,pread/pwrite在参数种指定文件操作的偏移,这样在多线程操作中避免了为读写偏移加锁。readv/writev可以把把文件的内容写到多个位置,也可以从多个位置向文件中写数据,这样就可以避免多次系统调用的开销。

     

    7 sendfile

           sendfile把文件的从某个位置开始的内容送入另一个文件中(可能会是一个套接字),这种操作节省了数据在内存中的拷贝次数,如果使用read/write实现,会增加两次数据拷贝操作。其内核实现方法和read/write也没有太大区别。

     

    8 fsync/fdatasync/msync

           这三个系统调用都涉及把内存中的dirty page同步到的块设备上的文件中去,它们之间有一些区别。

           fsync把文件在page cache中的dirty page写回到磁盘中去,一个文件在page cache中的内容包括文件数据也包括inode数据,当写一个文件时,除了修改文件数据之外,也修改了inode中的数据(比如文件修改时间),所以实际上有这两部分的数据需要同步,fsync把和指定文件相关的这两种dirty page回写到磁盘中。除了使用fsync强行同步文件之外,系统也会定期自动同步,即把dirty page回写到磁盘中。

           Fdatasync只回写文件数据的dirty page到磁盘中,不回写文件inode相关的dirty page。

           msync与fsync有所不同,在使用mmap映射文件到内存地址,向映射地址写入数据时如果没有缺页,就不会进入内核层,也无法设置写入页的状态为dirty,但cpu会自动把页表的dirty位置位,如果不设置页为dirty,其他的同步程序,如fsync以及内核的同步线程都无法同步这部分数据。msync的主要作用就是检查一个内存区域的页表,把dirty位置位的页表项对应的页的状态设置为dirty,如果msync指定了M_SYNC参数,msync还会和fsync一样同步数据,如果指定为M_ASYNC,则用内核同步线程或其他调用同步数据。

           在munmap时,系统会对映射的区域执行类似msync的操作,所以如果不调用msync数据也不一定会丢失(进程在退出时对映射区域也会自动调用munmap),但写大量数据不调用msync会有丢失数据的风险。

     

    9 shmget/shmat

           实际上无论是posix还是system v接口的共享内存,都是使用mmap来实现的,其原理也是一样的。把一个文件(可以是特殊文件或普通文件)映射到不同进程的地址空间,从上面描述的mmap的原理可以得知,一个文件在内核page cache中只有一份,不同的进程操作对同一个文件区域的映射,实际上就实现了对内存的共享。

           基于上面的原理,posix接口的共享内存就很容易理解了,system v接口的共享内存看起来没有那么直观,实际上一样,只不过它使用了特殊文件系统shmfs来做内存映射实现内存共享,shmfs实现了一个特殊的功能,使用普通文件进行文件共享时,当系统需要回收物理页面时,是把dirty页回写到磁盘上,然后回收该页,但如果没有调用msync,系统就无法知道该页是dirty页,在页面回收时就会直接抛弃掉该页的内容(因为它认为磁盘上还有)。这样就导致数据不一致,而shmfs的实现很特殊,它所有的页永远都是脏页,它的回写函数不是把数据回写到普通文件中,而是在交换分区(如果有的话)分配一块空间,把物理页内容写到交换分区上并标记它。shmfs避免了mmap在使用过程中可能出现的风险,而且对用户是透明的,它专为内存共享设计。

           shmget的作用就是想系统申请一定大小的共享内存区域,它只是在操作系统中唯一标示了一块共享内存区,但此时并没有为之分配物理内存,只是分配了管理结构,可以理解为在shmfs中创建了一个文件(如果已经存在,相当于打开了一个文件)。shmat间接使用mmap把shmget打开(或创建)的shmfs文件映射到应用程序的地址空间,其他过程就和mmap普通文件的处理一样了,只不过共享内存通过shmfs巧妙的避开了mmap的缺点。

     

    10 malloc

           malloc只是一个库函数,在不同的平台对malloc有不同的实现,glibc使用的是ptmalloc的实现。malloc是从堆上分配内存,但在内核中并没有堆的概念,堆只是一个应用程序的概念,在进程创建的时候,在进程的虚拟地址空间中划分出一块区域作为堆,这块区域并没有对应的物理内存,使用malloc分配内存实际上只是从这块虚拟内存区域分出更小的区域给应用程序,只有当应用程序访问这个小区域时才会产生缺页中断,从而获得物理内存。而free并不会释放物理内存,而是把在堆上分配的小区域归还给堆,这些操作都是glibc在应用层实现的。

           malloc的使用过程中会使用两个系统调用brk和mmap,brk用于增长(或减小)堆的大小,在进程创建时会指定堆的起始地址(堆空间是向上增长的),而堆的大小为0,当使用malloc分配内存时发现当前堆剩余空间不够时就会调用brk增长堆的大小,实际上brk的作用就是把堆所在的虚拟内存区域的结束地址增长(或减小)到某个位置。当malloc一次分配的空间大于一个阀值大小时(比如128K),malloc不再从堆上分配空间,而是使用mmap重新映射一块虚拟地址区域,在free时调用munmap释放这一区域。这样的策略主要是方便堆管理,避免在一个很大的尺度管理堆,这样做是基于大内存分配并不常使用这个假设。

           可以注意到如果分配的内存过大,在分配和释放时都要通过系统调用,效率会有降低,所以如果应用程序频繁分配大于分配阀值的内存,效率就会很低,这种应用可以通过调整分配阀值使内存分配和释放都在用户态(在堆中)完成。使用mallopt可以调整malloc的参数,M_TRIM_THRESHOLD表示如果堆大小大于该值,就应该在适当的时候收缩堆的大小,M_MMAP_THRESHOLD表示大于此值的内存分配请求要使用 mmap 系统调用。

    展开全文
  • 前面说了glibc以及标准C库函数和系统调用,但总感觉有点隔靴挠痒,下面就来追踪一下系统调用的源码。(linux-3.3内核,glibc-2.17源码) 系统调用以open函数为例 怎么查呢? 用 man 2 open 查看open的用法,看到需要...

    http://blog.chinaunix.net/uid-29401328-id-4866849.html

    前面说了glibc以及标准C库函数和系统调用,但总感觉有点隔靴挠痒,下面就来追踪一下系统调用的源码。(linux-3.3内核,glibc-2.17源码)

    系统调用以open函数为例
    怎么查呢?
    用  man 2 open 查看open的用法,看到需要包含三个头文件sys/types.h、sys/stat.h、fcntl.h,看了一下前两个,不相关,
    好像是fcntl.h,但这个头文件里也没给出open的定义,但这个头文件又包含了一个头文件io/fcntl.h,跟下去,看到168行:

    extern int open (const char *__file, int __oflag, ...) __nonnull ((1));

    引进了open函数,查找它的定义(我用的是source insight,直接按住Ctrl点击函数,就可以跳到该函数定义的地方),出现三个宏两个定义,oldfileops.c和fileops.c参数不匹配,另一个宏不可以,fcntl2.h里的也不像。应该是loadmsgcat.c里的
    # define open(name, flags) open_not_cancel_2 (name, flags)

    此时open_not_cancel_2对应glibc-2.17\sysdeps\unix\sysv\linux\Not-cancel.h 26行
    #define open_not_cancel_2(name, flags) \
       INLINE_SYSCALL (open, 2, (const char *) (name), (flags))

    每个硬件平台的INLINE_SYSCALL封装不一样,对于ARM来说(在glibc-2.17\ports\sysdeps\unix\sysv\linux\arm\Sysdep.h)
    #define INLINE_SYSCALL(name, nr, args...) \
      ({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \
         if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) \
           { \
    __set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \
    _sys_result = (unsigned int) -1; \
           } \
         (int) _sys_result; })

    跟踪上面的宏里INTERNAL_SYSCALL

    #else /* ARM */
    # undef INTERNAL_SYSCALL_RAW
    # define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \
      ({ \
           register int _a1 asm ("r0"), _nr asm ("r7"); \
           LOAD_ARGS_##nr (args) \
           _nr = name; \
           asm volatile ("swi 0x0 @ syscall " #name \
        : "=r" (_a1) \
        : "r" (_nr) ASM_ARGS_##nr \
        : "memory"); \
           _a1; })
    #endif

    看到上面嵌入的汇编指令 swi 异常,它会根据异常向量表实行系统调用,nr代表要实行的系统调用的编号,每一个系统调用对应一个编号。关于 swi 异常及系统调用请参见其他博文,这里只需要知道这条汇编指令能够根据我们传进来的系统调用函数的编号调用内核里定义的系统调用函数,从这里开始,系统调用就进入到内核态了。为什么会进入到内核态,进入到内核态以后又做了什么?
    这就涉及到swi异常处理了,请参见之后的博文。

    展开全文
  • glibc分析一 系统调用

    千次阅读 2018-09-06 21:11:01
    glibc源码分析(一)系统调用 pk2017 程序员 13 人赞了该文章 1.1 什么是glibc glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibcglibc除了封装linux...

    新博客地址(shankusu.me)

    glibc源码分析(一)系统调用

    pk2017

    pk2017

    程序员

    13 人赞了该文章

    1.1 什么是glibc

    glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX 通行的标准,可以想见其内容包罗万象。而就像其他的 UNIX 系统一样,其内含的档案群分散于系统的树状目录结构中,像一个支架一般撑起整个操作系统。在 GNU/Linux 系统中,其C函式库发展史点出了GNU/Linux 演进的几个重要里程碑,用 glibc 作为系统的C函式库,是GNU/Linux演进的一个重要里程碑。

    glibc支持不同的体系结构,不同的体系结构之上又支持不同的操作系统。

    • 支持的体系结构:alpha,arm,i386,ia64,powerpc等
    • 支持的操作系统:bsd,linux等

    本文及以后的一系列文章将对glibc源码进行一系列的分析,这些分析都是基于i386体系结构linux操作系统。glibc版本号为glibc-2.26。

    1.2 什么是系统调用

    1.2.1 概要

    顾名思义,系统调用(system call)是指操作系统提供给程序调用的接口。

    操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有更好的兼容性,为了达到这个目的,内核提供一系列具备预定功能的多内核函数,通过一组称为系统调用(system call)的接口呈现给用户。系统调用把应用程序的请求传给内核,调用相应的的内核函数完成所需的处理,将处理结果返回给应用程序。

    作为开发人员,我们调用系统调用来实现系统功能。

    有过linux下开发经验的人一定对glibc中的open,read,write,close,stat,mkdir等函数有所了解。这些函数其实都是是系统调用,准确的讲是系统调用的封装函数。glibc将诸多系统调用都封装成函数,使我们可以以函数的方式,方便的调用系统调用。本文及后续章节将详细讲解glibc对系统调用封装的过程。

    1.2.2 实例

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>
    #include <stdio.h>
    
    int main(int argc,char **argv)
    {
    
            struct stat buf;
    
            stat("/initrd.img",&buf);
    
            printf("size = %ld\n",buf.st_size);
    
            return 0;
    }
    

    1.3 系统调用的封装

    系统调用的封装按照固定的规则进行。寄存器EAX传递系统调用号。系统调用号用来确定系统调用。寄存器EBX,ECX,EDX,ESI,EDI,EBP依次传递系统调用参数。参数个数决定设置寄存器的个数。int0x80指令切入内核执行系统调用。系统调用执行完成后返回。寄存器EAX保存系统调用的返回值。

    glibc使用了多种不同的方式封装系统调用。但是,万变不离其宗,它们的封装过程一定是按照上面的规则进行的。

    1.4 glibc封装系统调用

    glibc实现了许多系统调用的封装。它们的封装方式大致可以分为两种:一 脚本生成汇编文件,汇编文件中汇编代码封装了系统调用。这种方式,简称脚本封装。二 .c文件中调用嵌入式汇编代码封装系统调用。一般使用.c文件封装系统调用,代码中除了嵌入式汇编封装代码外,还有一些C代码做其他处理。这种方式,简称.c封装。

    1.5 脚本封装

    1.5.1 概要

    glibc中大多数系统调用都是使用脚本封装的方式封装的。

    脚本封装的规则很简单。三种文件生成封装代码。一 make-syscall.sh文件 二 syscall-template.S文件 三 syscalls.list文件。

    make-syscall.sh是shell脚本文件。它读取syscalls.list文件的内容,对文件的每一行进行解析。根据每一行的内容生成一个.S汇编文件,汇编文件封装了一个系统调用。

    syscall-template.S是系统调用封装代码的模板文件。生成的.S汇编文件都调用它。

    syscalls.list是数据文件,它的内容如下:

    # File name Caller  Syscall name    Args    Strong name Weak names
    
    accept      -   accept      Ci:iBN  __libc_accept   accept
    access      -   access      i:si    __access    access
    acct        -   acct        i:S acct
    adjtime     -   adjtime     i:pp    __adjtime   adjtime
    bind        -   bind        i:ipi   __bind      bind
    chdir       -   chdir       i:s __chdir     chdir
    ......
    

    它由许多行组成,每一行可分为6列。File name列指定生成的汇编文件的文件名。Caller指定调用者。Syscall name列指定系统调用的名称,系统调用名称可以转换为系统调用号以标示系统调用。Args列指定系统调用参数类型,个数及返回值类型。Strong name指定系统调用封装函数的函数名。Weak names列指定封装函数的别称,用户可以调用别称来调用封装函数。

    make-syscall.sh分析syscalls.list每一行每一列的内容,生成汇编文件。以分析chdir行为例,生成的汇编文件内容为:

    #define SYSCALL_NAME chdir
    #define SYSCALL_NARGS 1
    #define SYSCALL_SYMBOL __chdir
    #define SYSCALL_CANCELLABLE 0
    #define SYSCALL_NOERRNO 0
    #define SYSCALL_ERRVAL 0
    #include <syscall-template.S>
    weak_alias (__chdir, chdir)
    hidden_weak (chdir)
    

    SYSCALL_NAME宏定义了系统调用的名字。是从Syscall name列获取。

    SYSCALL_NARGS宏定义了系统调用参数的个数。是通过解析Args列获取。

    SYSCALL_SYMBOL宏定义了系统调用的函数名称。是从Strong name列获取。

    SYSCALL_CANCELLABLE宏在生成的所有汇编文件中都定义为0。

    SYSCALL_NOERRNO宏定义为1,则封装代码没有出错返回。用于getpid这些没有出错返回的系统调用。是通过解析Args列设置。

    SYSCALL_ERRVAL宏定义为1,则封装代码直接返回错误号,不是返回-1并将错误号放入errno中。生成的所有.S文件中它都定义为0。

    weak_alias (__chdir, chdir)定义了__chdir函数的别称,我们可以调用chdir来调用__chdir。 chdir从Weak names列获取。

    汇编文件中引用了模板文件syscall-template.S,所有的封装代码都集中在syscall-template.S文件中。

    3种文件,make-syscall.sh文件在sysdeps/unix/make-syscall.sh。syscall-template.S文件在sysdeps/unix/syscall-template.S。syscalls.list文件则有多个,分别在sysdeps/unix/syscalls.list,sysdeps/unix/sysv/linux/syscalls.list,sysdeps/unix/sysv/linux/generic/syscalls.list,sysdeps/unix/sysv/linux/i386/syscalls.list。

    1.5.2 syscall-template.S

    syscall-template.S作为模板文件,包含了所有封装代码。

    #if SYSCALL_CANCELLABLE
    # include <sysdep-cancel.h>
    #else
    # include <sysdep.h>
    #endif
    
    #define syscall_hidden_def(SYMBOL)      hidden_def (SYMBOL)
    
    #define T_PSEUDO(SYMBOL, NAME, N)       PSEUDO (SYMBOL, NAME, N)
    #define T_PSEUDO_NOERRNO(SYMBOL, NAME, N)   PSEUDO_NOERRNO (SYMBOL, NAME, N)
    #define T_PSEUDO_ERRVAL(SYMBOL, NAME, N)    PSEUDO_ERRVAL (SYMBOL, NAME, N)
    #define T_PSEUDO_END(SYMBOL)            PSEUDO_END (SYMBOL)
    #define T_PSEUDO_END_NOERRNO(SYMBOL)        PSEUDO_END_NOERRNO (SYMBOL)
    #define T_PSEUDO_END_ERRVAL(SYMBOL)     PSEUDO_END_ERRVAL (SYMBOL)
    
    #if SYSCALL_NOERRNO
    
    T_PSEUDO_NOERRNO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
        ret_NOERRNO
    T_PSEUDO_END_NOERRNO (SYSCALL_SYMBOL)
    
    #elif SYSCALL_ERRVAL
    
    T_PSEUDO_ERRVAL (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
        ret_ERRVAL
    T_PSEUDO_END_ERRVAL (SYSCALL_SYMBOL)
    
    #else
    
    T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
        ret
    T_PSEUDO_END (SYSCALL_SYMBOL)
    
    #endif
    
    syscall_hidden_def (SYSCALL_SYMBOL)
    

    文件开头引入.h文件。如果SYSCALL_CANCELLABLE宏定义为1,则引入<sysdep-cancel.h>文件,否则引入<sysdep.h>文件。SYSCALL_CANCELLABLE宏在所有生成的汇编文件中都定义为0,所以汇编文件都是引用<sysdep.h>文件。<sysdep.h>文件位于sysdeps/unix/sysv/linux/i386/sysdep.h

    #if SYSCALL_CANCELLABLE
    # include <sysdep-cancel.h>
    #else
    # include <sysdep.h>
    #endif
    

    系统调用的封装代码由3种形式。

    如果系统调用没有错误返回,则执行

    T_PSEUDO_NOERRNO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
        ret_NOERRNO
    T_PSEUDO_END_NOERRNO (SYSCALL_SYMBOL)
    

    如果系统调用有错误返回且直接返回错误,则执行

    T_PSEUDO_ERRVAL (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
        ret_ERRVAL
    T_PSEUDO_END_ERRVAL (SYSCALL_SYMBOL)
    

    如果系统调用有错误返回且返回-1,errno设置错误号,则执行

    T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
        ret
    T_PSEUDO_END (SYSCALL_SYMBOL)
    

    1.5.3 T_PSEUDO_NOERRNO

    在系统调用没有出错返回时,执行

    T_PSEUDO_NOERRNO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
        ret_NOERRNO
    T_PSEUDO_END_NOERRNO (SYSCALL_SYMBOL)
    

    T_PSEUDO_NOERRNO宏引用PSEUDO_NOERRNO宏

    T_PSEUDO_END_NOERRNO宏引用PSEUDO_END_NOERRNO宏

    #define T_PSEUDO_NOERRNO(SYMBOL, NAME, N)   PSEUDO_NOERRNO (SYMBOL, NAME, N)
    #define T_PSEUDO_END_NOERRNO(SYMBOL)        PSEUDO_END_NOERRNO (SYMBOL)
    

    #undef	 PSEUDO_NOERRNO
    #define 	PSEUDO_NOERRNO(name, syscall_name, args)			      \
      .text;								      \
      ENTRY (name)								      \
        DO_CALL (syscall_name, args)
    

    PSEUDO_NOERRNO宏在文件开头声明文件内容为代码段

    .text;
    

    定义了名字为name的函数

    #define 	ENTRY(name)							      \
      .globl C_SYMBOL_NAME(name);						      \
      .type C_SYMBOL_NAME(name),@function;					      \
      .align ALIGNARG(4);							      \
      C_LABEL(name)								      \
      cfi_startproc;							      \
      CALL_MCOUNT
    
    #ifndef C_SYMBOL_NAME
    # define C_SYMBOL_NAME(name) name
    #endif
    
    #define ALIGNARG(log2) 1<<log2     //代码对齐
    
    # define C_LABEL(name)	name##:      //函数名
    
    # define cfi_startproc			.cfi_startproc
    
    #define CALL_MCOUNT		/* Do nothing.  */
    

    执行了系统调用

    #undef	 DO_CALL
    #define DO_CALL(syscall_name, args)			      		      \
        PUSHARGS_##args							      \
        DOARGS_##args							      \
        movl $SYS_ify (syscall_name), %eax;					      \
        ENTER_KERNEL							      \
        POPARGS_##args
    

    DO_CALL宏根据命令行参数个数args的不同执行不同的宏。

    当args为0时:

    #define PUSHARGS_0	/* No arguments to push.  */
    #define	 DOARGS_0	/* No arguments to frob.  */
    #define	 POPARGS_0	/* No arguments to pop.  */
    #define	 _PUSHARGS_0	/* No arguments to push.  */
    #define _DOARGS_0(n)	/* No arguments to frob.  */
    #define	 _POPARGS_0	/* No arguments to pop.  */
    

    程序执行

    movl $SYS_ify (syscall_name), %eax;					      
    ENTER_KERNEL							      
    
    //根据系统调用名,返回系统调用号
    #undef SYS_ify
    #define SYS_ify(syscall_name)	__NR_##syscall_name
    
    //切入内核执行系统调用
    #ifdef I386_USE_SYSENTER
    # ifdef SHARED
    #  define ENTER_KERNEL call *%gs:SYSINFO_OFFSET
    # else
    #  define ENTER_KERNEL call *_dl_sysinfo
    # endif
    #else
    # define ENTER_KERNEL int $0x80
    #endif
    

    当args为1时:

    #define PUSHARGS_1 	movl %ebx, %edx; L(SAVEBX1): PUSHARGS_0
    #define	 DOARGS_1 	_DOARGS_1 (4)
    #define	 POPARGS_1 	POPARGS_0; movl %edx, %ebx; L(RESTBX1):
    #define	 _PUSHARGS_1 	pushl %ebx; cfi_adjust_cfa_offset (4); \
    			cfi_rel_offset (ebx, 0); L(PUSHBX1): _PUSHARGS_0
    #define _DOARGS_1(n)	 movl n(%esp), %ebx; _DOARGS_0(n-4)
    #define	 _POPARGS_1	 _POPARGS_0; popl %ebx; cfi_adjust_cfa_offset (-4); \
    			cfi_restore (ebx); L(POPBX1):
    

    程序执行:

    	movl %ebx, %edx;
    movl 4(%esp), %ebx;
    movl $SYS_ify (syscall_name), %eax;					      
    ENTER_KERNEL
    movl %edx, %ebx;
    

    当args为2时:

    #define PUSHARGS_2 	PUSHARGS_1
    #define	 DOARGS_2 	_DOARGS_2 (8)
    #define	 POPARGS_2	 POPARGS_1
    #define _PUSHARGS_2 	_PUSHARGS_1
    #define 	_DOARGS_2(n) 	movl n(%esp), %ecx; _DOARGS_1 (n-4)
    #define	 _POPARGS_2	 _POPARGS_1
    

    程序执行:

    	movl %ebx, %edx;
    	movl 8(%esp), %ecx;
    movl 4(%esp), %ebx;
    movl $SYS_ify (syscall_name), %eax;					      
    ENTER_KERNEL
    movl %edx, %ebx;
    

    当args为3时:

    #define PUSHARGS_3	 _PUSHARGS_2
    #define DOARGS_3	 _DOARGS_3 (16)
    #define POPARGS_3 	_POPARGS_3
    #define _PUSHARGS_3 	_PUSHARGS_2
    #define _DOARGS_3(n) 	movl n(%esp), %edx; _DOARGS_2 (n-4)
    #define _POPARGS_3	 _POPARGS_2
    

    程序执行

    pushl %ebx;
    movl 16(%esp), %edx;
    movl 12(%esp), %ecx;
    movl 8(%esp), %ebx;
    movl $SYS_ify (syscall_name), %eax;
    ENTER_KERNEL
    popl %ebx
    

    当args参数为4时:

    #define PUSHARGS_4	 _PUSHARGS_4
    #define DOARGS_4	 _DOARGS_4 (24)
    #define POPARGS_4 	_POPARGS_4
    #define _PUSHARGS_4 	pushl %esi; cfi_adjust_cfa_offset (4); \
    			cfi_rel_offset (esi, 0); L(PUSHSI1): _PUSHARGS_3
    #define _DOARGS_4(n) 	movl n(%esp), %esi; _DOARGS_3 (n-4)
    #define _POPARGS_4 	_POPARGS_3; popl %esi; cfi_adjust_cfa_offset (-4); \
    			cfi_restore (esi); L(POPSI1):
    

    程序执行:

    pushl %esi;
    pushl %ebx;
    movl 24(%esp), %esi;
    movl 20(%esp), %edx;
    movl 16(%esp), %ecx;
    movl 12(%esp), %ebx;
    movl $SYS_ify (syscall_name), %eax;
    ENTER_KERNEL
    popl %ebx;
    popl %esi;
    

    当参数为5时:

    #define PUSHARGS_5	 _PUSHARGS_5
    #define DOARGS_5	 _DOARGS_5 (32)
    #define POPARGS_5 	_POPARGS_5
    #define _PUSHARGS_5 	pushl %edi; cfi_adjust_cfa_offset (4); \
    			cfi_rel_offset (edi, 0); L(PUSHDI1): _PUSHARGS_4
    #define _DOARGS_5(n)	 movl n(%esp), %edi; _DOARGS_4 (n-4)
    #define _POPARGS_5	 _POPARGS_4; popl %edi; cfi_adjust_cfa_offset (-4); \
    			cfi_restore (edi); L(POPDI1):
    

    程序执行

    pushl %edi;
    pushl %esi;
    pushl %ebx;
    movl 32(%esp), %edi;
    movl 28(%esp), %esi;
    movl 24(%esp), %edx;
    movl 20(%esp), %ecx;
    movl 16(%esp), %ebx;
    movl $SYS_ify (syscall_name), %eax;
    ENTER_KERNEL
    popl %ebx;
    popl %esi;
    popl %edi;
    

    当参数为6时:

    #define PUSHARGS_6	 _PUSHARGS_6
    #define DOARGS_6	 _DOARGS_6 (40)
    #define POPARGS_6 	_POPARGS_6
    #define _PUSHARGS_6 	pushl %ebp; cfi_adjust_cfa_offset (4); \
    			cfi_rel_offset (ebp, 0); L(PUSHBP1): _PUSHARGS_5
    #define _DOARGS_6(n) 	movl n(%esp), %ebp; _DOARGS_5 (n-4)
    #define _POPARGS_6	 _POPARGS_5; popl %ebp; cfi_adjust_cfa_offset (-4); \
    			cfi_restore (ebp); L(POPBP1):
    

    程序执行

    pushl %ebp; 
    pushl %edi;
    pushl %esi;
    pushl %ebx;
    movl 40(%esp), %ebp;
    movl 36(%esp), %edi;
    movl 32(%esp), %esi;
    movl 28(%esp), %edx;
    movl 24(%esp), %ecx;
    movl 20(%esp), %ebx;
    movl $SYS_ify (syscall_name), %eax;
    ENTER_KERNEL
    popl %ebx;
    popl %esi;
    popl %edi;
    popl %ebp;
    

    DO_CALL宏设置了系统调用参数,系统调用号,切入内核,并将系统调用返回值放入eax寄存器中。

    接着,执行ret指令返回函数。

    ret_NOERRNO
    #define ret_NOERRNO ret
    

    汇编文件结尾

    #undef	PSEUDO_END_NOERRNO
    #define	PSEUDO_END_NOERRNO(name)					      \
      END (name)
    
    //汇编文件结束
    #undef	END
    #define END(name)							      \
      cfi_endproc;								      \
      ASM_SIZE_DIRECTIVE(name)
    
    #define cfi_endproc			 .cfi_endproc
    #define ASM_SIZE_DIRECTIVE(name) .size name,.-name;
    

    到这里,整个封装代码已经全部完成。

    1.5.4 T_PSEUDO_ERRVAL

    #undef	PSEUDO_ERRVAL
    #define	PSEUDO_ERRVAL(name, syscall_name, args) \
      .text;								      \
      ENTRY (name)								      \
        DO_CALL (syscall_name, args);					      \
        negl %eax
    

    T_PSEUDO_ERRVAL宏定义了函数name,函数调用了系统调用syscall_name。执行完DO_CALL 宏后,系统调用执行完毕,系统调用返回值放入eax寄存器中。negl %eax取反eax寄存器的值。此时,eax寄存器中保存着错误号。

    #define ret_ERRVAL ret
    

    函数返回

    #undef	PSEUDO_END_ERRVAL
    #define	PSEUDO_END_ERRVAL(name) \
      END (name)
    

    汇编文件结尾

    1.5.5 T_PSEUDO

    #undef	PSEUDO
    #define	PSEUDO(name, syscall_name, args)				      \
      .text;								      \
      ENTRY (name)								      \
        DO_CALL (syscall_name, args);					      \
        cmpl $-4095, %eax;							      \
        jae SYSCALL_ERROR_LABEL
    

    执行系统调用,如果其返回值大于等于-4095,则跳到SYSCALL_ERROR_LABEL处执行。

    #define SYSCALL_ERROR_LABEL __syscall_error
    

    SYSCALL_ERROR_LABEL指向__syscall_error函数。

    int
    __attribute__ ((__regparm__ (1)))
    __syscall_error (int error)
    {
      __set_errno (-error);
      return -1;
    }
    

    如果小于-4095,则直接返回

     ret
    

    汇编文件结尾

    #undef	PSEUDO_END
    #define	PSEUDO_END(name)						      \
      SYSCALL_ERROR_HANDLER							      \
      END (name)
    
    #define SYSCALL_ERROR_HANDLER	/* Nothing here; code in sysdep.c is used.  */
    

    1.5.6 实例

    chdir函数

    umask函数

    展开全文
  • GLIBC里查找系统调用

    千次阅读 2014-09-03 08:49:43
    有些系统调用GLIBC里是找不到相应的函数的

    有些系统调用在GLIBC里是找不到相应的函数的. 这一类函数会按照 sysdeps/unix/syscall-template.S 里的参数要求在 sysdeps/unix/syscall.S里定义每一个系统调用. 编译的时候,sysdev/unix/make-syscalls.sh 会根据上面的文件生成每个函数的汇编代码到 glibc库里面.

    比如, 如果查看 mmap 的反汇编)(MIPS),你会看到:

    (gdb) disass mmap
    Dump of assembler code for function mmap:
       0x77f3107c <+0>:	lui	gp,0xa
       0x77f31080 <+4>:	addiu	gp,gp,-29292
       0x77f31084 <+8>:	addu	gp,gp,t9
       0x77f31088 <+12>:	li	v0,4090
       0x77f3108c <+16>:	syscall
       0x77f31090 <+20>:	bnez	a3,0x77f31070
       0x77f31094 <+24>:	nop
       0x77f31098 <+28>:	jr	ra
       0x77f3109c <+32>:	nop
       0x77f310a0 <+36>:	nop
    
    查看  mmap的源码:

    (gdb) list mmap
    76	#else
    77	
    78	/* This is a "normal" system call stub: if there is an error,
    79	   it returns -1 and sets errno.  */
    80	
    81	T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
    82		ret
    83	T_PSEUDO_END (SYSCALL_SYMBOL)
    84	
    85	#endif
    ../sysdeps/unix/syscall-template.S:82

    其中, T_PSEUDO定义为:

    # define PSEUDO(name, syscall_name, args)                     \
      .align 2;                                   \
      99: la t9,__syscall_error;                              \
      jr t9;                                      \
      ENTRY (name)                                    \
        .set noreorder;                               \
        .cpload t9;                                   \
        .set reorder;                                 \
        SINGLE_THREAD_P(t0);                              \
        bne zero, t0, L(pseudo_cancel);                       \
        .set noreorder;                               \
        li v0, SYS_ify(syscall_name);                         \
        syscall;                                      \
        .set reorder;                                 \
        bne a3, zero, SYSCALL_ERROR_LABEL;                            \
        ret;                                      \
    

    基本上和前面的反汇编代码是一致的.


    展开全文
  • 其中POSIX接口大部分是对linux系统调用的直接导出,所以完全可以绕开libc库而直接引用系统调用。 具体方法:http://bbs.nankai.edu.cn/cgi-bin/bbs/bbsanc?path=/PersonalCorpus/J/jasonduan/D595ABB25/M.118
  • 为了新增一个 vdso 系统调用,需要同时修改内核和glibc。glib支持多种体系结构操作系统,在gblic中新增一个vdso系统调用,需要在对应体系结构目录下新增一个包含该系统调用的.c文件,比如 ./sysdeps/unix/sysv/...
  • glibc封装了大部分系统API,我们一般都是使用glibc封装的接口进行系统调用,碰到一些没有封装的接口,可以通过这个 函数syscall 进行系统调用。 1 /* Invoke `system call' number SYSNO, passing it the...
  • glibc源码分析之系统调用(一)

    千次阅读 2017-08-10 20:22:41
    在编写本文时,glibc的最新版本是glibc-2.26。本文所有描述都是基于...glibc使用了两种方式来封装系统调用。一种是由脚本生成。一种是.c文件。 使用.c文件封装系统调用,是因为封装过程比较复杂,除了按系统调用的调
  • glibc 中有三种方式封装系统调用,包括汇编封装、通过宏进行封装、定制封装。 对于汇编封装的系统调用,需要在 syscalls.list 文件中进行配置,例如:sysdeps/unix/sysv/linux/x86_64/syscalls.list 文件中配置了...
  • glibc定义的系统调用

    2014-11-12 16:16:53
    最近有人问道,glibc 中对我们常见的那些系统调用的定义在哪里?比如write(2),recv(2)。 这个问题我以前在看glibc的代码时注意到了。我们通常可以直接找到的所谓定义,比如下面这个:  PLAIN TEXT ...
  • 前文详细介绍了glibc封装系统调用的方法,在本文中我将向大家讲解glibc系统调用封装的具体的例子。这些例子都是关于文件系统的。 1.1 文件的创建与删除 linux支持7种文件:普通文件,目录文件,字符设备文件,块...
  • maminjie@fedora /m/h/p/l/glibc (master)> find -name time.c ./sysdeps/unix/sysv/linux/powerpc/time.c ./sysdeps/unix/sysv/linux/time.c ./sysdeps/unix/sysv/linux/x86/time.c ./time/time.c
  • libc库、glibc和系统调用

    千次阅读 2014-02-14 15:35:58
    Linux系统调用这部分经常出现两个词:libc库封装函数,不知道你是否清楚它们的含义? libc 1. libc概念  libc是Stantard C Library的简称,它是符合ANSI C标准的一个标准函数库。libc库提供C语言中所...
  • glibc源码分析(二)系统调用

    千次阅读 2018-09-06 21:11:34
    glibc中许多系统调用是用.c封装的方式封装的。 .c封装都是借助嵌入式汇编,按照系统调用的封装规则进行封装的。 可以查看stat64函数的实现,来探究.c封装。 #undef stat64 int attribute_hidden stat64 (const ...
  • 使用.c文件封装系统调用一般出于以下两个原因:一,系统调用已过时,可以使用新的系统调用替换旧的系统调用系统调用的语义不变。二,封装后的函数改变了系统调用的语义。stat系统调用的封装便是原因一的一个例子。...
  •  本文试图解答系统调用与库函数之间的关系、glibc库函数的实现原理、系统调用的实现原理等知识,本文讲述的CPU架构限定为ARM核心,为什么是ARM呢,原因很简单,X86我不熟。 系统调用概念  从某种程度上来说,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 25,417
精华内容 10,166
关键字:

系统调用和glibc