分析linux的系统调用_linux 调用系统调用的方式 - CSDN
  • linux内核中设置了一组用于实现系统功能的子程序,称为系统调用系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。   ...

    转载自:http://blog.csdn.net/sailor_8318/archive/2008/09/10/2906968.aspx



    1       系统调用意义

    linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。

     

    一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作"保护模式")。为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求(或者让应用程序暂时搁置)。实际上提供这组接口主要是为了保证系统稳定可靠,避免应用程序肆意妄行,惹出大麻烦。

     

    系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:

    ²      它为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。

    ²      系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他什么危害系统的事情。

    ²      每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中,系统调用是用户空间访问内核的惟一手段;除异常和中断外,它们是内核惟一的合法入口。

     

    2       API/POSIX/C库的关系

    一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。

     

    在Unix世界中,最流行的应用编程接口是基于POSIX标准的,其目标是提供一套大体上基于Unix的可移植操作系统标准。POSIX是说明API和系统调用之间关系的一个极好例子。在大多数Unix系统上,根据POSIX而定义的API函数和系统调用之间有着直接关系。

     

    Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供如下图所示。C库实现了 Unix系统的主要API,包括标准C库函数和系统调用。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。

     

    从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了。相反,内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用不是内核所关心的。

     

    关于Unix的界面设计有一句通用的格言“提供机制而不是策略”。换句话说,Unix的系统调用抽象出了用于完成某种确定目的的函数。至干这些函数怎么用完全不需要内核去关心。区别对待机制(mechanism)和策略(policy)是Unix设计中的一大亮点。大部分的编程问题都可以被切割成两个部分:“需要提供什么功能”(机制)和“怎样实现这些功能”(策略)。

     

    3       系统调用的实现

    3.1    系统调用处理程序

    您或许疑惑: “当我输入 cat /proc/cpuinfo 时,cpuinfo() 函数是如何被调用的?”内核完成引导后,控制流就从相对直观的“接下来调用哪个函数?”改变为取决于系统调用、异常和中断。

     

    用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。

     

    通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。

     

    新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。

     

    3.2    系统调用号

    在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。进程不会提及系统调用的名称。

     

    系统调用号相当关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。

     

    因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷人内核之前,用户空间就把相应系统调用所对应的号放入eax中了。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。

     

    内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了惟一的系统调用号。sys_call_table是一张由指向实现各种系统调用的内核函数的函数指针组成的表:

    ENTRY(sys_call_table)

    .long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/

    .long SYMBOL_NAME(sys_exit)

    .long SYMBOL_NAME(sys_fork)

    .long SYMBOL_NAME(sys_read)

    .long SYMBOL_NAME(sys_write)

    .long SYMBOL_NAME(sys_open) /* 5 */

    .long SYMBOL_NAME(sys_close)

    .long SYMBOL_NAME(sys_waitpid)

    。。。。。

    .long SYMBOL_NAME(sys_capget)

    .long SYMBOL_NAME(sys_capset)      /* 185 */

    .long SYMBOL_NAME(sys_sigaltstack)

    .long SYMBOL_NAME(sys_sendfile)

    .long SYMBOL_NAME(sys_ni_syscall) /* streams1 */

    .long SYMBOL_NAME(sys_ni_syscall) /* streams2 */

    .long SYMBOL_NAME(sys_vfork)      /* 190 */

     

    system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR syscalls,该函数就返回一ENOSYS。否则,就执行相应的系统调用。

          call *sys_ call-table(,%eax, 4)

    由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置

     

    3.3    参数传递

    除了系统调用号以外,大部分系统调用都还需要一些外部的参数输人。所以,在发生异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上,ebx, ecx, edx, esi和edi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

     

    给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。接下来许多关于系统调用处理程序的描述都是针对x86版本的。但不用担心,所有体系结构的实现都很类似。

     

    3.4    参数验证

    系统调用必须仔细检查它们所有的参数是否合法有效。举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。

     

    最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须被检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据。在接收一个用户空间的指针之前,内核必须保证:

    ²      指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。

    ²      指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。

    ²      如果是读,该内存应被标记为可读。如果是写,该内存应被标记为可写。进程决不能绕过内存访问限制。

     

    内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。注意,内核无论何时都不能轻率地接受来自用户空间的指针!这两个方法中必须有一个被调用。为了向用户空间写入数据,内核提供了copy_to_user(),它需要三个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址。最后一个参数是需要拷贝的数据长度(字节数)。

     

    为了从用户空间读取数据,内核提供了copy_from_ user(),它和copy-to-User()相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

     

    如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,返回0。当出现上述错误时,系统调用返回标准-EFAULT。

     

    注意copy_to_user()和copy_from_user()都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。

     

    3.5    系统调用的返回值

    系统调用(在Linux中常称作syscalls)通常通过函数进行调用。它们通常都需要定义一个或几个参数(输入)而且可能产生一些副作用,例如写某个文件或向给定的指针拷贝数据等等。为防止和正常的返回值混淆,系统调用并不直接返回错误码,而是将错误码放入一个名为errno的全局变量中。通常用一个负的返回值来表明错误。返回一个0值通常表明成功。如果一个系统调用失败,你可以读出errno的值来确定问题所在。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。

     

    errno不同数值所代表的错误消息定义在errno.h中,你也可以通过命令"man 3 errno"来察看它们。需要注意的是,errno的值只在函数发生错误时设置,如果函数不发生错误,errno的值就无定义,并不会被置为0。另外,在处理errno前最好先把它的值存入另一个变量,因为在错误处理过程中,即使像printf()这样的函数出错时也会改变errno的值。

     

    当然,系统调用最终具有一种明确的操作。举例来说,如getpid()系统调用,根据定义它会返回当前进程的PID。内核中它的实现非常简单:

    asmlinkage long sys_ getpid(void)

    {

        return current-> tgid;

    }

     

    上述的系统调用尽管非常简单,但我们还是可以从中发现两个特别之处。首先,注意函数声明中的asmlinkage限定词,这是一个小戏法,用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。其次,注意系统调用get_pid()在内核中被定义成sys_ getpid。这是Linux中所有系统调用都应该遵守的命名规则

     

    4       添加新系统调用

    给Linux添加一个新的系统调用是件相对容易的工作。怎样设计和实现一个系统调用是难题所在,而把它加到内核里却无须太多周折。让我们关注一下实现一个新的Linux系统调用所需的步骤。

     

    实现一个新的系统调用的第一步是决定它的用途。它要做些什么?每个系统调用都应该有一个明确的用途。在Linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择完成不同的工作)。ioctl()就应该被视为一个反例。

     

    新系统调用的参数、返回值和错误码又该是什么呢?系统调用的接口应该力求简洁,参数尽可能少。设计接口的时候要尽量为将来多做考虑。你是不是对函数做了不必要的限制?系统调用设计得越通用越好。不要假设这个系统调用现在怎么用将来也一定就是这么用。系统调用的目的可能不变,但它的用法却可能改变。这个系统调用可移植吗?别对机器的字节长度和字节序做假设。当你写一个系统调用的时候,要时刻注意可移植性和健壮性,不但要考虑当前,还要为将来做打算。

     

    当编写完一个系统调用后,把它注册成一个正式的系统调用是件琐碎的工作:

    在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作。从0开始算起,系统调用在该表中的位置就是它的系统调用号。

    对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中。

    系统调用必须被编译进内核映象(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以。

     

    让我们通过一个虚构的系统调用f00()来仔细观察一下这些步骤。首先,我们要把sys_foo加入到系统调用表中去。对于大多数体系结构来说,该表位干entry.s文件中,形式如下:

    ENTRY(sys_ call_ table)

          ·long sys_ restart_ syscall/*0*/

          .long sys_ exit

          ·long sys_ fork

          ·long sys_ read

          .long sys_write

    我们把新的系统调用加到这个表的末尾:

         .long sys_foo

    虽然没有明确地指定编号,但我们加入的这个系统调用被按照次序分配给了283这个系统调用号。对于每种需要支持的体系结构,我们都必须将自己的系统调用加人到其系统调用表中去。每种体系结构不需要对应相同的系统调用号。

     

    接下来,我们把系统调用号加入到<asm/unistd.h>中,它的格式如下:

    /*本文件包含系统调用号*/

    #define_ NR_ restart_ syscall

    #define NR exit

    #define NR fork

    #define NR read

    #define NR write

    #define NR- mq getsetattr 282

    然后,我们在该列表中加入下面这行:

    #define_ NR_ foo 283

     

    最后,我们来实现f00()系统调用。无论何种配置,该系统调用都必须编译到核心的内核映象中去,所以我们把它放进kernel/sys.c文件中。你也可以将其放到与其功能联系最紧密的代码中去

     

    asmlinkage long sys-foo(void)

    {

    return THREAD SIZE

    )

    就是这样!严格说来,现在就可以在用户空间调用f00()系统调用了。

     

    建立一个新的系统调用非常容易,但却绝不提倡这么做。通常模块可以更好的代替新建一个系统调用。

     

    5       访问系统调用

    5.1    系统调用上下文

    内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。

     

    在进程上下文中,内核可以休眠并且可以被抢占。这两点都很重要。首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。休眠的能力会给内核编程带来极大便利。在进程上下文中能够被抢占,其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重人的。当然,这也是在对称多处理中必须同样关心的问题。

     

    当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去。

     

    5.2    系统调用访问示例

    操作系统使用系统调用表将系统调用编号翻译为特定的系统调用。系统调用表包含有实现每个系统调用的函数的地址。例如,read() 系统调用函数名为 sys_read。read() 系统调用编号是 3,所以 sys_read()位于系统调用表的第四个条目中(因为系统调用起始编号为0)。从地址 sys_call_table + (3 * word_size) 读取数据,得到 sys_read()的地址。

     

    找到正确的系统调用地址后,它将控制权转交给那个系统调用。我们来看定义 sys_read() 的位置,即 fs/read_write.c 文件。这个函数会找到关联到 fd 编号(传递给 read() 函数的)的文件结构体。那个结构体包含指向用来读取特定类型文件数据的函数的指针。进行一些检查后,它调用与文件相关的 read() 函数,来真正从文件中读取数据并返回。与文件相关的函数是在其他地方定义的 —— 比如套接字代码、文件系统代码,或者设备驱动程序代码。这是特定内核子系统最终与内核其他部分协作的一个方面。

     

    读取函数结束后,从 sys_read() 返回,它将控制权切换给 ret_from_sys。它会去检查那些在切换回用户空间之前需要完成的任务。如果没有需要做的事情,那么就恢复用户进程的状态,并将控制权交还给用户程序。

    5.3    从用户空间直接访问系统调用

    通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但如果你仅仅写出系统调用,glibc库恐怕并不提供支持。值得庆幸的是,Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷人指令。这些宏是_syscalln(),其中n的范围从0到6。代表需要传递给系统调用的参数个数,这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器。举个例子,open()系统调用的定义是:

    long open(const char *filename, int flags, int mode)

    而不靠库支持,直接调用此系统调用的宏的形式为:

    #define NR_ open 5

    syscall3(long, open, const char*,filename, int, flags, int, mode)

    这样,应用程序就可以直接使用open()

     

    对于每个宏来说,都有2+ n个参数。第一个参数对应着系统调用的返回值类型。第二个参数是系统调用的名称。再以后是按照系统调用参数的顺序排列的每个参数的类型和名称。_NR_ open在<asm/unistd.h>中定义,是系统调用号。该宏会被扩展成为内嵌汇编的C函数。由汇编语言执行前一节所讨论的步骤,将系统调用号和参数压入寄存器并触发软中断来陷入内核。调用open()系统调用直接把上面的宏放置在应用程序中就可以了。

     

    让我们写一个宏来使用前面编写的foo()系统调用,然后再写出测试代码炫耀一下我们所做的努力。

    #define NR foo 283

    _sysca110(long, foo)

    int main()

    {

    long stack size;

    stack_ size=foo();

    printf("The kernel stack

    size is 81d/n",stack_ size);

    return;

    }

     

    6       参考文档

    linux系统调用实现代码分析

    理解Linux的系统调用

    什么是系统调用

    Linux内核设计与实现

     

    附录:2.4 内核Linux系统调用的列表

     

    以下是Linux系统调用的一个列表,包含了大部分常用系统调用和由系统调用派生出的的函数。其中有一些函数的作用完全相同,只是参数不同。可能很多熟悉C++朋友马上就能联想起函数重载,但是别忘了Linux核心是用C语言写的,所以只能取成不同的函数名。

     

    一、进程控制:

     

     

    fork

    创建一个新进程

    clone

    按指定条件创建子进程

    execve

    运行可执行文件

    exit

    中止进程

    _exit

    立即中止当前进程

    getdtablesize

    进程所能打开的最大文件数

    getpgid

    获取指定进程组标识号

    setpgid

    设置指定进程组标志号

    getpgrp

    获取当前进程组标识号

    setpgrp

    设置当前进程组标志号

    getpid

    获取进程标识号

    getppid

    获取父进程标识号

    getpriority

    获取调度优先级

    setpriority

    设置调度优先级

    modify_ldt

    读写进程的本地描述表

    nanosleep

    使进程睡眠指定的时间

    nice

    改变分时进程的优先级

    pause

    挂起进程,等待信号

    personality

    设置进程运行域

    prctl

    对进程进行特定操作

    ptrace

    进程跟踪

    sched_get_priority_max

    取得静态优先级的上限

    sched_get_priority_min

    取得静态优先级的下限

    sched_getparam

    取得进程的调度参数

    sched_getscheduler

    取得指定进程的调度策略

    sched_rr_get_interval

    取得按RR算法调度的实时进程的时间片长度

    sched_setparam

    设置进程的调度参数

    sched_setscheduler

    设置指定进程的调度策略和参数

    sched_yield

    进程主动让出处理器,并将自己等候调度队列队尾

    vfork

    创建一个子进程,以供执行新程序,常与execve等同时使用

    wait

    等待子进程终止

    wait3

    参见wait

    waitpid

    等待指定子进程终止

    wait4

    参见waitpid

    capget

    获取进程权限

    capset

    设置进程权限

    getsid

    获取会晤标识号

    setsid

    设置会晤标识号

     

     

    二、文件系统控制

    1、文件读写操作

     

    fcntl

    文件控制

    open

    打开文件

    creat

    创建新文件

    close

    关闭文件描述字

    read

    读文件

    write

    写文件

    readv

    从文件读入数据到缓冲数组中

    writev

    将缓冲数组里的数据写入文件

    pread

    对文件随机读

    pwrite

    对文件随机写

    lseek

    移动文件指针

    _llseek

    在64位地址空间里移动文件指针

    dup

    复制已打开的文件描述字

    dup2

    按指定条件复制文件描述字

    flock

    文件加/解锁

    poll

    I/O多路转换

    truncate

    截断文件

    ftruncate

    参见truncate

    umask

    设置文件权限掩码

    fsync

    把文件在内存中的部分写回磁盘

     

     

    2、文件系统操作

     

    access

    确定文件的可存取性

    chdir

    改变当前工作目录

    fchdir

    参见chdir

    chmod

    改变文件方式

    fchmod

    参见chmod

    chown

    改变文件的属主或用户组

    fchown

    参见chown

    lchown

    参见chown

    chroot

    改变根目录

    stat

    取文件状态信息

    lstat

    参见stat

    fstat

    参见stat

    statfs

    取文件系统信息

    fstatfs

    参见statfs

    readdir

    读取目录项

    getdents

    读取目录项

    mkdir

    创建目录

    mknod

    创建索引节点

    rmdir

    删除目录

    rename

    文件改名

    link

    创建链接

    symlink

    创建符号链接

    unlink

    删除链接

    readlink

    读符号链接的值

    mount

    安装文件系统

    umount

    卸下文件系统

    ustat

    取文件系统信息

    utime

    改变文件的访问修改时间

    utimes

    参见utime

    quotactl

    控制磁盘配额

     

     

    三、系统控制

     

    ioctl

    I/O总控制函数

    _sysctl

    读/写系统参数

    acct

    启用或禁止进程记账

    getrlimit

    获取系统资源上限

    setrlimit

    设置系统资源上限

    getrusage

    获取系统资源使用情况

    uselib

    选择要使用的二进制函数库

    ioperm

    设置端口I/O权限

    iopl

    改变进程I/O权限级别

    outb

    低级端口操作

    reboot

    重新启动

    swapon

    打开交换文件和设备

    swapoff

    关闭交换文件和设备

    bdflush

    控制bdflush守护进程

    sysfs

    取核心支持的文件系统类型

    sysinfo

    取得系统信息

    adjtimex

    调整系统时钟

    alarm

    设置进程的闹钟

    getitimer

    获取计时器值

    setitimer

    设置计时器值

    gettimeofday

    取时间和时区

    settimeofday

    设置时间和时区

    stime

    设置系统日期和时间

    time

    取得系统时间

    times

    取进程运行时间

    uname

    获取当前UNIX系统的名称、版本和主机等信息

    vhangup

    挂起当前终端

    nfsservctl

    对NFS守护进程进行控制

    vm86

    进入模拟8086模式

    create_module

    创建可装载的模块项

    delete_module

    删除可装载的模块项

    init_module

    初始化模块

    query_module

    查询模块信息

    *get_kernel_syms

    取得核心符号,已被query_module代替

     

     

     

    四、内存管理

     

    brk

    改变数据段空间的分配

    sbrk

    参见brk

    mlock

    内存页面加锁

    munlock

    内存页面解锁

    mlockall

    调用进程所有内存页面加锁

    munlockall

    调用进程所有内存页面解锁

    mmap

    映射虚拟内存页

    munmap

    去除内存页映射

    mremap

    重新映射虚拟内存地址

    msync

    将映射内存中的数据写回磁盘

    mprotect

    设置内存映像保护

    getpagesize

    获取页面大小

    sync

    将内存缓冲区数据写回硬盘

    cacheflush

    将指定缓冲区中的内容写回磁盘

     

     

    五、网络管理

     

    getdomainname

    取域名

    setdomainname

    设置域名

    gethostid

    获取主机标识号

    sethostid

    设置主机标识号

    gethostname

    获取本主机名称

    sethostname

    设置主机名称

     

     

    六、socket控制

     

    socketcall

    socket系统调用

    socket

    建立socket

    bind

    绑定socket到端口

    connect

    连接远程主机

    accept

    响应socket连接请求

    send

    通过socket发送信息

    sendto

    发送UDP信息

    sendmsg

    参见send

    recv

    通过socket接收信息

    recvfrom

    接收UDP信息

    recvmsg

    参见recv

    listen

    监听socket端口

    select

    对多路同步I/O进行轮询

    shutdown

    关闭socket上的连接

    getsockname

    取得本地socket名字

    getpeername

    获取通信对方的socket名字

    getsockopt

    取端口设置

    setsockopt

    设置端口参数

    sendfile

    在文件或端口间传输数据

    socketpair

    创建一对已联接的无名socket

     

     

     

    七、用户管理

     

    getuid

    获取用户标识号

    setuid

    设置用户标志号

    getgid

    获取组标识号

    setgid

    设置组标志号

    getegid

    获取有效组标识号

    setegid

    设置有效组标识号

    geteuid

    获取有效用户标识号

    seteuid

    设置有效用户标识号

    setregid

    分别设置真实和有效的的组标识号

    setreuid

    分别设置真实和有效的用户标识号

    getresgid

    分别获取真实的,有效的和保存过的组标识号

    setresgid

    分别设置真实的,有效的和保存过的组标识号

    getresuid

    分别获取真实的,有效的和保存过的用户标识号

    setresuid

    分别设置真实的,有效的和保存过的用户标识号

    setfsgid

    设置文件系统检查时使用的组标识号

    setfsuid

    设置文件系统检查时使用的用户标识号

    getgroups

    获取后补组标志清单

    setgroups

    设置后补组标志清单

     

     

    八、进程间通信

     

    ipc

    进程间通信总控制调用

     

     

    1、信号

     

    sigaction

    设置对指定信号的处理方法

    sigprocmask

    根据参数对信号集中的信号执行阻塞/解除阻塞等操作

    sigpending

    为指定的被阻塞信号设置队列

    sigsuspend

    挂起进程等待特定信号

    signal

    参见signal

    kill

    向进程或进程组发信号

    *sigblock

    向被阻塞信号掩码中添加信号,已被sigprocmask代替

    *siggetmask

    取得现有阻塞信号掩码,已被sigprocmask代替

    *sigsetmask

    用给定信号掩码替换现有阻塞信号掩码,已被sigprocmask代替

    *sigmask

    将给定的信号转化为掩码,已被sigprocmask代替

    *sigpause

    作用同sigsuspend,已被sigsuspend代替

    sigvec

    为兼容BSD而设的信号处理函数,作用类似sigaction

    ssetmask

    ANSI C的信号处理函数,作用类似sigaction

     

     

    2、消息

     

    msgctl

    消息控制操作

    msgget

    获取消息队列

    msgsnd

    发消息

    msgrcv

    取消息

     

     

    3、管道

     

    pipe

    创建管道

     

     

    4、信号量

     

    semctl

    信号量控制

    semget

    获取一组信号量

    semop

    信号量操作

     

     

    5、共享内存

     

    shmctl

    控制共享内存

    shmget

    获取共享内存

    shmat

    连接共享内存

    shmdt

    拆卸共享内存

     

     

     

     

    /linux+v2.6.19/arch/arm/kernel/entry-common.S

     114        .align  5

     115ENTRY(vector_swi)

     116        sub     sp, sp, #S_FRAME_SIZE

     117        stmia   sp, {r0 - r12}                  @ Calling r0 - r12

     118        add     r8, sp, #S_PC

     119        stmdb   r8, {sp, lr}^                   @ Calling sp, lr

     120        mrs     r8, spsr                        @ called from non-FIQ mode, so ok.

     121        str     lr, [sp, #S_PC]                 @ Save calling PC

     122        str     r8, [sp, #S_PSR]                @ Save CPSR

     123        str     r0, [sp, #S_OLD_R0]             @ Save OLD_R0

     124        zero_fp

     125

     126        /*

     127         * Get the system call number.

     128         */

     129

     130#if defined(CONFIG_OABI_COMPAT)

     131

     149        /*

     150         * Pure EABI user space always put syscall number into scno (r7).

     151         */

     152  A710( ldr     ip, [lr, #-4]                   @ get SWI instruction   )

     153  A710( and     ip, ip, #0x0f000000             @ check for SWI         )

     154  A710( teq     ip, #0x0f000000                                         )

     155  A710( bne     .Larm710bug                                             )

     156

     157#elif defined(CONFIG_ARM_THUMB)

     158

     159        /* Legacy ABI only, possibly thumb mode. */

     160        tst     r8, #PSR_T_BIT                  @ this is SPSR from save_user_regs

     161        addne   scno, r7, #__NR_SYSCALL_BASE    @ put OS number in

     162        ldreq   scno, [lr, #-4]

     163

     164#else

     165

     166        /* Legacy ABI only. */

     167        ldr     scno, [lr, #-4]                 @ get SWI instruction

     168  A710( and     ip, scno, #0x0f000000           @ check for SWI         )

     169  A710( teq     ip, #0x0f000000                                         )

     170  A710( bne     .Larm710bug                                             )

     171

     172#endif

     173

     179        enable_irq

     180

     181        get_thread_info tsk

     182        adr     tbl, sys_call_table             @ load syscall table pointer

     183        ldr     ip, [tsk, #TI_FLAGS]            @ check for syscall tracing

     184

     185#if defined(CONFIG_OABI_COMPAT)

     186        /*

     187         * If the swi argument is zero, this is an EABI call and we do nothing.

     188         *

     189         * If this is an old ABI call, get the syscall number into scno and

     190         * get the old ABI syscall table address.

     191         */

     192        bics    r10, r10, #0xff000000

     193        eorne   scno, r10, #__NR_OABI_SYSCALL_BASE

     194        ldrne   tbl, =sys_oabi_call_table

     199

     200        stmdb   sp!, {r4, r5}                   @ push fifth and sixth args

     201        tst     ip, #_TIF_SYSCALL_TRACE         @ are we tracing syscalls?

     202        bne     __sys_trace

     203

     204        cmp     scno, #NR_syscalls              @ check upper syscall limit

     205        adr     lr, ret_fast_syscall            @ return address

     206        ldrcc   pc, [tbl, scno, lsl #2]         @ call sys_* routine

     207

     208        add     r1, sp, #S_OFF

     2092:      mov     why, #0                         @ no longer a real syscall

     210        cmp     scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)

     211        eor     r0, scno, #__NR_SYSCALL_BASE    @ put OS number back

     212        bcs     arm_syscall    

     213        b       sys_ni_syscall                  @ not private func

     214

     215        /*

     216         * This is the really slow path.  We're going to be doing

     217         * context switches, and waiting for our parent to respond.

     218         */

     219__sys_trace:

     220        mov     r2, scno

     221        add     r1, sp, #S_OFF

     222        mov     r0, #0                          @ trace entry [IP = 0]

     223        bl      syscall_trace

     224

     225        adr     lr, __sys_trace_return          @ return address

     226        mov     scno, r0                        @ syscall number (possibly new)

     227        add     r1, sp, #S_R0 + S_OFF           @ pointer to regs

     228        cmp     scno, #NR_syscalls              @ check upper syscall limit

     229        ldmccia r1, {r0 - r3}                   @ have to reload r0 - r3

     230        ldrcc   pc, [tbl, scno, lsl #2]         @ call sys_* routine

     231        b       2b

     232

     233__sys_trace_return:

     234        str     r0, [sp, #S_R0 + S_OFF]!        @ save returned r0

     235        mov     r2, scno

     236        mov     r1, sp

     237        mov     r0, #1                          @ trace exit [IP = 1]

     238        bl      syscall_trace

     239        b       ret_slow_syscall

     240

     247        .ltorg

     248

     249/*

     250 * This is the syscall table declaration for native ABI syscalls.

     251 * With EABI a couple syscalls are obsolete and defined as sys_ni_syscall.

     252 */

     253#define ABI(native, compat) native

     254#ifdef CONFIG_AEABI

     255#define OBSOLETE(syscall) sys_ni_syscall

     256#else

     257#define OBSOLETE(syscall) syscall

     258#endif

     259

     260        .type   sys_call_table, #object

     261ENTRY(sys_call_table)

     262#include "calls.S"

     263#undef ABI

     264#undef OBSOLETE

     265

     266/*============================================================================

     267 * Special system call wrappers

     268 */

     269@ r0 = syscall number

     270@ r8 = syscall table

     271                .type   sys_syscall, #function

     272sys_syscall:

     273                bic     scno, r0, #__NR_OABI_SYSCALL_BASE

     274                cmp     scno, #__NR_syscall - __NR_SYSCALL_BASE

     275                cmpne   scno, #NR_syscalls      @ check range

     276                stmloia sp, {r5, r6}            @ shuffle args

     277                movlo   r0, r1

     278                movlo   r1, r2

     279                movlo   r2, r3

     280                movlo   r3, r4

     281                ldrlo   pc, [tbl, scno, lsl #2]

     282                b       sys_ni_syscall

     283

     284sys_fork_wrapper:

     285                add     r0, sp, #S_OFF

     286                b       sys_fork

     287

    。。。。。

     

    /linux+v2.6.19/arch/arm/kernel/entry-armv.S

    1058.LCvswi:

    1059        .word   vector_swi

    1060

    1061        .globl  __stubs_end

    1062__stubs_end:

    1063

    1064        .equ    stubs_offset, __vectors_start + 0x200 - __stubs_start

    1065

    1066        .globl  __vectors_start

    1067__vectors_start:

    1068        swi     SYS_ERROR0

    1069        b       vector_und + stubs_offset

    1070        ldr     pc, .LCvswi + stubs_offset

    1071        b       vector_pabt + stubs_offset

    1072        b       vector_dabt + stubs_offset

    1073        b       vector_addrexcptn + stubs_offset

    1074        b       vector_irq + stubs_offset

    1075        b       vector_fiq + stubs_offset

    1076

    1077        .globl  __vectors_end

    1078__vectors_end:

    展开全文
  • 然后,我们考察了Linux内核如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。最后讨论了如何增加系统调用,并提供了从用户空间...

    本文介绍了系统调用的一些实现细节。首先分析了系统调用的意义,它们与库函数和应用程序接口(API)有怎样的关系。然后,我们考察了Linux内核如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。最后讨论了如何增加系统调用,并提供了从用户空间访问系统调用的简单例子。
    参考 《Linux内核设计与实现》读书笔记(五)- 系统调用

    系统调用概述


    计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。

    一般情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程。在Unix世界,最流行的API是基于POSIX标准的。

    操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。

    中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。

    一般地,系统调用都是通过软件中断实现的,x86系统上的软件中断由int $0x80指令产生,而128号异常处理程序就是系统调用处理程序system_call(),它与硬件体系有关,在entry.S中用汇编写。接下来就来看一下Linux下系统调用具体的实现过程。

    为什么需要系统调用


    linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态,而普通的函数调用由函数库或用户自己提供,运行于用户态

    一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作“保护模式”(详细参见深入理解计算机系统-之-内存寻址(二)–存储保护机制(CPU实模式与保护模式)))。

    为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求(或者让应用程序暂时搁置)。实际上提供这组接口主要是为了保证系统稳定可靠,避免应用程序肆意妄行,惹出大麻烦。

    系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:

    • 它为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。

    • 系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他什么危害系统的事情。

    • 每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中,系统调用是用户空间访问内核的惟一手段;除异常和中断外,它们是内核惟一的合法入口。

    API/POSIX/C库的区别与联系


    一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。

    一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。

    在Unix世界中,最流行的应用编程接口是基于POSIX标准的,其目标是提供一套大体上基于Unix的可移植操作系统标准。POSIX是说明API和系统调用之间关系的一个极好例子。在大多数Unix系统上,根据POSIX而定义的API函数和系统调用之间有着直接关系。

    Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供如下图所示。C库实现了 Unix系统的主要API,包括标准C库函数和系统调用。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。

    从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了。相反,内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用不是内核所关心的。

    关于Unix的界面设计有一句通用的格言“提供机制而不是策略”。换句话说,Unix的系统调用抽象出了用于完成某种确定目的的函数。至干这些函数怎么用完全不需要内核去关心。区别对待机制(mechanism)和策略(policy)是Unix设计中的一大亮点。大部分的编程问题都可以被切割成两个部分:“需要提供什么功能”(机制)和“怎样实现这些功能”(策略)。

    区别


    api是函数的定义,规定了这个函数的功能,跟内核无直接关系。而系统调用是通过中断向内核发请求,实现内核提供的某些服务。

    联系


    一个api可能会需要一个或多个系统调用来完成特定功能。通俗点说就是如果这个api需要跟内核打交道就需要系统调用,否则不需要。
    程序员调用的是API(API函数),然后通过与系统调用共同完成函数的功能。
    因此,API是一个提供给应用程序的接口,一组函数,是与程序员进行直接交互的。
    系统调用则不与程序员进行交互的,它根据API函数,通过一个软中断机制向内核提交请求,以获取内核服务的接口。
    并不是所有的API函数都一一对应一个系统调用,有时,一个API函数会需要几个系统调用来共同完成函数的功能,甚至还有一些API函数不需要调用相应的系统调用(因此它所完成的不是内核提供的服务)

    系统调用的实现原理


    基本机制


    前文已经提到了Linux下的系统调用是通过0x80实现的,但是我们知道操作系统会有多个系统调用(Linux下有319个系统调用),而对于同一个中断号是如何处理多个不同的系统调用的?最简单的方式是对于不同的系统调用采用不同的中断号,但是中断号明显是一种稀缺资源,Linux显然不会这么做;还有一个问题就是系统调用是需要提供参数,并且具有返回值的,这些参数又是怎么传递的?也就是说,对于系统调用我们要搞清楚两点:

    1. 系统调用的函数名称转换。
    2. 系统调用的参数传递。

    首先看第一个问题。实际上,Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。比如fork()的实现:

    用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。

    通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。

    新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。

    从系统分析的角度,linux的系统调用涉及4个方面的问题。

    响应函数sys_xxx


    响应函数名以“sys_”开头,后跟该系统调用的名字。

    例如

    系统调用fork()的响应函数是sys_fork()(见Kernel/fork.c),

    exit()的响应函数是sys_exit()(见kernel/fork.)。

    系统调用表与系统调用号-=>数组与下标


    文件include/asm/unisted.h为每个系统调用规定了唯一的编号。
    系统调用号

    在我们系统中/usr/include/asm/unistd_32.h,可以通过find / -name unistd_32.h -print查找)
    而内核中的头文件路径不同的内核版本以及不同的发行版,文件的存储结构可能有所区别

    linux-3.0
    这里写图片描述

    linux-2.6

    这里写图片描述

    假设用name表示系统调用的名称,那么系统调用号与系统调用响应函数的关系是:以系统调用号_NR_name作为下标,可找出系统调用表sys_call_table(见arch/i386/kernel/entry.S)中对应表项的内容,它正好是该系统调用的响应函数sys_name的入口地址。

    系统调用表sys_call_table记录了各sys_name函数在表中的位置,共190项。有了这张表,就很容易根据特定系统调用

    sys_call_table

    在表中的偏移量,找到对应的系统调用响应函数的入口地址。系统调用表共256项,余下的项是可供用户自己添加的系统调用空间。

    在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。进程不会提及系统调用的名称。

    系统调用号相当关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。

    因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷人内核之前,用户空间就把相应系统调用所对应的号放入eax中了。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。

    内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了惟一的系统调用号。sys_call_table是一张由指向实现各种系统调用的内核函数的函数指针组成的表:
    system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR syscalls,该函数就返回一ENOSYS。否则,就执行相应的系统调用。
    这里写图片描述

          call *sys_ call-table(,%eax, 4)

    由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置

    进程的系统调用命令转换为INT 0x80中断的过程


    宏定义_syscallN()include/asm/unisted.h)用于系统调用的格式转换和参数的传递。N取0~5之间的整数。

    参数个数为N的系统调用由_syscallN()负责格式转换和参数传递。系统调用号放入EAX寄存器,启动INT 0x80后,规定返回值送EAX寄存器。

    系统调用功能模块的初始化


    对系统调用的初始化也就是对INT 0x80的初始化。

    系统启动时,汇编子程序setup_idt(见arch/i386/kernel/head.S)准备了1张256项的idt表,由start_kernel()(见init/main.c),trap_init()(见arch/i386/kernel/traps.c)调用的C语言宏定义set_system_gate(0x80,&system_call)(见include/asm/system.h)设置0x80号软中断的服务程序为 system_call(见arch/i386/kernel/entry.S), system.call就是所有系统调用的总入口。

    内核如何为各种系统调用服务


    当进程需要进行系统调用时,必须以C语言函数的形式写一句系统调用命令。该命令如果已在某个头文件中由相应的_syscallN()展开,则用户程序必须包含该文件。当进程执行到用户程序的系统调用命令时,实际上执行了由宏命令_syscallN()展开的函数。系统调用的参数 由各通用寄存器传递,然后执行INT 0x80,以内核态进入入口地址system_call

    ret_from_sys_call


    ret_from_sys_call入口的汇编程序段在linux进程管理中起到了十分重要的作用。

    所有系统调用结束前以及大部分中断服务返回前,都会跳转至此处入口地址。 该段程序不仅仅为系统调用服务,它还处理中断嵌套、CPU调度、信号等事务。

    内核如何为系统调用的参数传递参数


    参数传递


    除了系统调用号以外,大部分系统调用都还需要一些外部的参数输人。所以,在发生异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上,ebx, ecx, edx, esiedi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

    给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。接下来许多关于系统调用处理程序的描述都是针对x86版本的。但不用担心,所有体系结构的实现都很类似。

    参数验证


    系统调用必须仔细检查它们所有的参数是否合法有效。举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。

    最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须被检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据。在接收一个用户空间的指针之前,内核必须保证:

    • 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。

    • 指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。

    • 如果是读,该内存应被标记为可读。如果是写,该内存应被标记为可写。进程决不能绕过内存访问限制。

    内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。注意,内核无论何时都不能轻率地接受来自用户空间的指针!这两个方法中必须有一个被调用。为了向用户空间写入数据,内核提供了copy_to_user(),它需要三个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址。最后一个参数是需要拷贝的数据长度(字节数)。

    为了从用户空间读取数据,内核提供了copy_from_ user(),它和copy-to-User()相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

    如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,返回0。当出现上述错误时,系统调用返回标准-EFAULT。

    注意copy_to_user()copy_from_user()都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。

    系统调用的返回值


    系统调用(在Linux中常称作syscalls)通常通过函数进行调用。它们通常都需要定义一个或几个参数(输入)而且可能产生一些副作用,例如写某个文件或向给定的指针拷贝数据等等。为防止和正常的返回值混淆,系统调用并不直接返回错误码,而是将错误码放入一个名为errno的全局变量中。通常用一个负的返回值来表明错误。返回一个0值通常表明成功。如果一个系统调用失败,你可以读出errno的值来确定问题所在。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。

    errno不同数值所代表的错误消息定义在errno.h中,你也可以通过命令”man 3 errno”来察看它们。需要注意的是,errno的值只在函数发生错误时设置,如果函数不发生错误,errno的值就无定义,并不会被置为0。另外,在处理errno前最好先把它的值存入另一个变量,因为在错误处理过程中,即使像printf()这样的函数出错时也会改变errno的值。

    当然,系统调用最终具有一种明确的操作。举例来说,如getpid()系统调用,根据定义它会返回当前进程的PID。内核中它的实现非常简单:

    asmlinkage long sys_ getpid(void)
    {
        return current-> tgid;
    }

    上述的系统调用尽管非常简单,但我们还是可以从中发现两个特别之处。首先,注意函数声明中的asmlinkage限定词,这是一个小戏法,用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。其次,注意系统调用get_pid()在内核中被定义成sys_ getpid。这是Linux中所有系统调用都应该遵守的命名规则。

    访问系统调用


    系统调用上下文


    内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。

    在进程上下文中,内核可以休眠并且可以被抢占。这两点都很重要。首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。休眠的能力会给内核编程带来极大便利。在进程上下文中能够被抢占,其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重人的。当然,这也是在对称多处理中必须同样关心的问题。

    当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去。

    系统调用访问示例


    操作系统使用系统调用表将系统调用编号翻译为特定的系统调用。系统调用表包含有实现每个系统调用的函数的地址。例如,read() 系统调用函数名为sys_readread()系统调用编号是 3,所以sys_read() 位于系统调用表的第四个条目中(因为系统调用起始编号为0)。从地址 sys_call_table + (3 * word_size) 读取数据,得到sys_read()的地址。

    找到正确的系统调用地址后,它将控制权转交给那个系统调用。我们来看定义sys_read()的位置,即fs/read_write.c文件。这个函数会找到关联到 fd 编号(传递给 read() 函数的)的文件结构体。那个结构体包含指向用来读取特定类型文件数据的函数的指针。进行一些检查后,它调用与文件相关的 read() 函数,来真正从文件中读取数据并返回。与文件相关的函数是在其他地方定义的 —— 比如套接字代码、文件系统代码,或者设备驱动程序代码。这是特定内核子系统最终与内核其他部分协作的一个方面。

    读取函数结束后,从sys_read()返回,它将控制权切换给 ret_from_sys。它会去检查那些在切换回用户空间之前需要完成的任务。如果没有需要做的事情,那么就恢复用户进程的状态,并将控制权交还给用户程序。

    从用户空间直接访问系统调用


    通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但如果你仅仅写出系统调用,glibc库恐怕并不提供支持。值得庆幸的是,Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷人指令。这些宏是_syscalln(),其中n的范围从0到6。代表需要传递给系统调用的参数个数,这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器。举个例子,open()系统调用的定义是:

    long open(const char *filename, int flags, int mode)

    而不靠库支持,直接调用此系统调用的宏的形式为:

    #define NR_ open 5
    syscall3(long, open, const char*,filename, int, flags, int, mode)

    这样,应用程序就可以直接使用open()
    对于每个宏来说,都有2+ n个参数。
    第一个参数对应着系统调用的返回值类型。
    第二个参数是系统调用的名称。再以后是按照系统调用参数的顺序排列的每个参数的类型和名称。
    _NR_ open<asm/unistd.h>中定义,是系统调用号。该宏会被扩展成为内嵌汇编的C函数。由汇编语言执行前一节所讨论的步骤,将系统调用号和参数压入寄存器并触发软中断来陷入内核。调用open()系统调用直接把上面的宏放置在应用程序中就可以了。

    让我们写一个宏来使用前面编写的foo()系统调用,然后再写出测试代码炫耀一下我们所做的努力。

    #define NR foo 283
    _sysca110(long, foo)
    int main()
    {
    long stack size;
    stack_ size=foo();
    printf("The kernel stack
    size is 81d/n",stack_ size);
    return;
    }

    添加系统调用


    通过修改内核源代码添加系统调用


    linux-2.6.*


    通过以上分析linux系统调用的过程,

    将自己的系统调用加到内核中就是一件容易的事情。下面介绍一个实际的系统调用,

    并把它加到内核中去。要增加的系统调用是:inttestsyscall(),其功能是在控制终端屏幕上显示hello world,

    执行成功后返回0。

    编写int testsyscall()系统调用–响应函数


    编写一个系统调用意味着要给内核增加1个函数,将新函数放入文件kernel/sys.c中。新函数代码如下:

    asmlingkage sys_testsyscall()
    { 
        print("hello world\n");    
        return 0;
     }

    添加系统调用号


    编写了新的系统调用过程后,下一项任务是使内核的其余部分知道这一程序的存在,然后重建包含新的系统调用的内核。为了把新的函数连接到已有的内核中去, 需要编辑2个文件:

    1).inculde/asm/unistd.h在这个文件中加入

    #define_NR_testsyscall 191

    系统调用表中添加对应项


    2).are/i386/kernel/entry.s这个文件用来对指针数组初始化,在这个文件中增加一行:

     .long SYMBOL_NAME(_sys_tsetsycall)

    .rept NR_syscalls-190改为NR_SYSCALLS-191,然后重新编译和运行新内核。

    使用新的系统调用


    在保证的C语言库中没有新的系统调用的程序段,必须自己建立其代码如下

    #inculde
    
    _syscall0(int,testsyscall)
    
    main()
    {
        tsetsyscall();
    }

    在这里使用了_syscall0宏指令,宏指令本身在程序中将扩展成名为syscall()的函数,它在main()函数内部加以调用。

    testsyscall()函数中, 预处理程序产生所有必要的机器指令代码,包括用系统调用参数值加载相应的cpu寄存器, 然后执行int 0x80中断指令。

    linux-3.*


    在linux-3.8.4/kernel/sys.c 文件末尾添加新的系统调用函数如:

    asmlinkage int sys_mycall(int number)
    {    
        printk("这是我添加的第一个系统调用");    
        return number; }

    arch/x86/syscall_32.tbl下找到unused 223号调用然后替换如:

    223 i386 mycall sys_mycall

    如果是64位系统,在arch/x86/syscalls/syscall_64.tbl下找到313号系统调用,然后在其下面加上314号自己的中断如:
    `314 common mycall sys_mycall

    利用内核模块添加系统调用


    模块是内核的一部分,但是并没有被编译到内核里面去。它们被分别编译并连接成一组目标文件, 这些文件能被插入到正在运行的内核,或者从正在运行的内核中移走。内核模块至少必须有2个函数:
    

    init_modulecleanup_module

    第一个函数是在把模块插入内核时调用的;

    第二个函数则在删除该模块时调用。由于内核模块是内核的一部分,所以能访问所有内核资源。根据对linux系统调用机制的分析,

    如果要增加系统调用,可以编写自己的函数来实现,然后在sys_call_table表中增加一项,使该项中的指针指向自己编写的函数,

    就可以实现系统调用。下面用该方法实现在控制终端上打印“hello world” 的系统调用testsyscall()。

    编写系统调用内核模块


    #inculde(linux/kernel.h)
    
    #inculde(linux/module.h)
    
    #inculde(linux/modversions.h)
    
    #inculde(linux/sched.h)
    
     #inculde(asm/uaccess.h)
    
    #define_NR_testsyscall 191
    
    extern viod *sys_call+table[];
    
    asmlinkage int testsyscall()
    
    { 
        printf("hello world\n");
    
        return 0;
    
    }
    
    int init_module()
    
    { 
        sys_call_table[_NR_tsetsyscall]=testsyscall;
        printf("system call testsyscall() loaded success\n");
    
        return 0;
    }
    
    void cleanup_module()
    {
    
    }

    使用新的系统调用

    #define_NR_testsyscall 191
    
    _syscall0(int,testsyscall)
    
    main()
    {
        testsyscall();
    }

    内核Linux系统调用的列表


    以下是Linux系统调用的一个列表,包含了大部分常用系统调用和由系统调用派生出的的函数。

    进程控制


    系统调用 描述
    fork 创建一个新进程
    clone 按指定条件创建子进程
    execve 运行可执行文件
    exit 中止进程
    _exit 立即中止当前进程
    getdtablesize 进程所能打开的最大文件数
    getpgid 获取指定进程组标识号
    setpgid 设置指定进程组标志号
    getpgrp 获取当前进程组标识号
    setpgrp 设置当前进程组标志号
    getpid 获取进程标识号
    getppid 获取父进程标识号
    getpriority 获取调度优先级
    setpriority 设置调度优先级
    modify_ldt 读写进程的本地描述表
    nanosleep 使进程睡眠指定的时间
    nice 改变分时进程的优先级
    pause 挂起进程,等待信号
    personality 设置进程运行域
    prctl 对进程进行特定操作
    ptrace 进程跟踪
    sched_get_priority_max 取得静态优先级的上限
    sched_get_priority_min 取得静态优先级的下限
    sched_getparam 取得进程的调度参数
    sched_getscheduler 取得指定进程的调度策略
    sched_rr_get_interval 取得按RR算法调度的实时进程的时间片长度
    sched_setparam 设置进程的调度参数
    sched_setscheduler 设置指定进程的调度策略和参数
    sched_yield 进程主动让出处理器,并将自己等候调度队列队尾
    vfork 创建一个子进程,以供执行新程序,常与execve等同时使用
    wait 等待子进程终止
    wait3 参见wait
    waitpid 等待指定子进程终止
    wait4 参见waitpid
    capget 获取进程权限
    capset 设置进程权限
    getsid 获取会晤标识号
    setsid 设置会晤标识号

    文件系统控制


    文件读写操作


    系统调用 描述
    fcntl 文件控制
    open 打开文件
    creat 创建新文件
    close 关闭文件描述字
    read 读文件
    write 写文件
    readv 从文件读入数据到缓冲数组中
    writev 将缓冲数组里的数据写入文件
    pread 对文件随机读
    pwrite 对文件随机写
    lseek 移动文件指针
    _llseek 在64位地址空间里移动文件指针
    dup 复制已打开的文件描述字
    dup2 按指定条件复制文件描述字
    flock 文件加/解锁
    poll I/O多路转换
    truncat e 截断文件
    ftruncate 参见truncate
    vumask 设置文件权限掩码
    fsync 把文件在内存中的部分写回磁盘

    文件系统操作


    系统调用 描述
    access 确定文件的可存取性
    chdir 改变当前工作目录
    fchdir 参见chdir
    chmod 改变文件方式
    fchmod 参见chmod
    chown 改变文件的属主或用户组
    fchown 参见chown
    lchown 参见chown
    chroot 改变根目录
    stat 取文件状态信息
    lstat 参见stat
    fstat 参见stat
    statfs 取文件系统信息
    fstatfs 参见statfs
    readdir 读取目录项
    getdents 读取目录项
    mkdir 创建目录
    mknod 创建索引节点
    rmdir 删除目录
    rename 文件改名
    link 创建链接
    symlink 创建符号链接
    unlink 删除链接
    readlink 读符号链接的值
    mount 安装文件系统
    umount 卸下文件系统
    ustat 取文件系统信息
    utime 改变文件的访问修改时间
    utimes 参见utime
    quotactl 控制磁盘配额

    系统控制


    系统调用 描述
    ioctl I/O总控制函数
    _sysctl 读/写系统参数
    acct 启用或禁止进程记账
    getrlimit 获取系统资源上限
    setrlimit 设置系统资源上限
    getrusage 获取系统资源使用情况
    uselib 选择要使用的二进制函数库
    ioperm 设置端口I/O权限
    iopl 改变进程I/O权限级别
    outb 低级端口操作
    reboot 重新启动
    swapon 打开交换文件和设备
    swapoff 关闭交换文件和设备
    bdflush 控制bdflush守护进程
    sysfs 取核心支持的文件系统类型
    sysinfo 取得系统信息
    adjtimex 调整系统时钟
    alarm 设置进程的闹钟
    getitimer 获取计时器值
    setitimer 设置计时器值
    gettimeofday 取时间和时区
    settimeofday 设置时间和时区
    stime 设置系统日期和时间
    time 取得系统时间
    times 取进程运行时间
    uname 获取当前UNIX系统的名称、版本和主机等信息
    vhangup 挂起当前终端
    nfsservctl 对NFS守护进程进行控制
    vm86 进入模拟8086模式
    create_module 创建可装载的模块项
    delete_module 删除可装载的模块项
    init_module 初始化模块
    query_module 查询模块信息
    *get_kernel_syms 取得核心符号,已被query_module代替

    内存管理


    系统调用 描述
    brk 改变数据段空间的分配
    sbrk 参见brk
    mlock 内存页面加锁
    munlock 内存页面解锁
    mlockall 调用进程所有内存页面加锁
    munlockall 调用进程所有内存页面解锁
    mmap 映射虚拟内存页
    munmap 去除内存页映射
    mremap 重新映射虚拟内存地址
    msync 将映射内存中的数据写回磁盘
    mprotect 设置内存映像保护
    getpagesize 获取页面大小
    sync 将内存缓冲区数据写回硬盘
    cacheflush 将指定缓冲区中的内容写回磁盘

    网络管理


    系统调用 描述
    getdomainname 取域名
    setdomainname 设置域名
    gethostid 获取主机标识号
    sethostid 设置主机标识号
    gethostname 获取本主机名称
    sethostname 设置主机名称

    socket控制


    系统调用 描述
    socketcall socket系统调用
    socket 建立socket
    bind 绑定socket到端口
    connect 连接远程主机
    accept 响应socket连接请求
    send 通过socket发送信息
    sendto 发送UDP信息
    sendmsg 参见send
    recv 通过socket接收信息
    recvfrom 接收UDP信息
    recvmsg 参见recv
    listen 监听socket端口
    select 对多路同步I/O进行轮询
    shutdown 关闭socket上的连接
    getsockname 取得本地socket名字
    getpeername 获取通信对方的socket名字
    getsockopt 取端口设置
    setsockopt 设置端口参数
    sendfile 在文件或端口间传输数据
    socketpair 创建一对已联接的无名socket

    用户管理


    系统调用 描述
    getuid 获取用户标识号
    setuid 设置用户标志号
    getgid 获取组标识号
    setgid 设置组标志号
    getegid 获取有效组标识号
    setegid 设置有效组标识号
    geteuid 获取有效用户标识号
    seteuid 设置有效用户标识号
    setregid 分别设置真实和有效的的组标识号
    setreuid 分别设置真实和有效的用户标识号
    getresgid 分别获取真实的,有效的和保存过的组标识号
    setresgid 分别设置真实的,有效的和保存过的组标识号
    getresuid 分别获取真实的,有效的和保存过的用户标识号
    setresuid 分别设置真实的,有效的和保存过的用户标识号
    setfsgid 设置文件系统检查时使用的组标识号
    setfsuid 设置文件系统检查时使用的用户标识号
    getgroups 获取后补组标志清单
    setgroups 设置后补组标志清单

    进程间通信


    系统调用 描述
    ipc 进程间通信总控制调用

    信号


    系统调用 描述
    sigaction 设置对指定信号的处理方法
    sigprocmask 根据参数对信号集中的信号执行阻塞/解除阻塞等操作
    sigpending 为指定的被阻塞信号设置队列
    sigsuspend 挂起进程等待特定信号
    signal 参见signal
    kill 向进程或进程组发信号
    *sigblock 向被阻塞信号掩码中添加信号,已被sigprocmask代替
    *siggetmask 取得现有阻塞信号掩码,已被sigprocmask代替
    *sigsetmask 用给定信号掩码替换现有阻塞信号掩码,已被sigprocmask代替
    *sigmask 将给定的信号转化为掩码,已被sigprocmask代替
    *sigpause 作用同sigsuspend,已被sigsuspend代替
    sigvec 为兼容BSD而设的信号处理函数,作用类似sigaction
    ssetmask ANSI C的信号处理函数,作用类似sigaction

    消息


    系统调用 描述
    msgctl 消息控制操作
    msgget 获取消息队列
    msgsnd 发消息
    msgrcv 取消息

    管道


    系统调用 描述
    pipe 创建管道

    信号量


    系统调用 描述
    semctl 信号量控制
    semget 获取一组信号量
    semop 信号量操作

    共享内存


    系统调用 描述
    shmctl 控制共享内存
    shmget 获取共享内存
    shmat 连接共享内存
    shmdt 拆卸共享内存
    展开全文
  • linux系统调用 系统调用是linux内核为用户态程序提供的主要功能接口。通过系统调用,用户态进程能够临时切换到内核态,使用内核态才能访问的硬件和资源完成特定功能。系统调用由linux内核和内核模块实现,内核在...

    linux系统调用

    系统调用是linux内核为用户态程序提供的主要功能接口。通过系统调用,用户态进程能够临时切换到内核态,使用内核态才能访问的硬件和资源完成特定功能。系统调用由linux内核和内核模块实现,内核在处理系统调用时还会检查系统调用请求和参数是否正确,保证对特权资源和硬件访问的正确性。通过这种方式,linux在提供内核和硬件资源访问接口的同时,保证了内核和硬件资源的使用正确性和安全性。

    本文主要对linux下系统调用的原理和实现进行分析。本文的分析基于x86架构,涉及到的linux内核代码版本为4.17.6。

    用户态调用接口

    用户态进程主要通过如下方式,直接使用系统调用:

    #define _GNU_SOURCE         /* See feature_test_macros(7) */
    #include <unistd.h>
    #include <sys/syscall.h>   /* For SYS_xxx definitions */
    
    int syscall(int number, ...);
    

    syscall接口由glibc提供和实现,第一个参数number表示需要调用的系统调用编号,后续的可变参数根据系统调用类型确定。内核具体支持的系统调用号可在<sys/syscall.h>中查看。函数调用失败会返回-1,具体错误原因保存在errno中,errno的含义可参考<errno.h>

    需要注意的是,这里的返回值和errno是glibc封装提供的,内核的系统调用响应函数本身不提供errno,返回值也不同。

    实现原理

    一次系统调用的完整执行过程如下:

    1. 通过特定指令发出系统调用(int $80、sysenter、syscall)

    2. CPU从用户态切换到内核态,进行一些寄存器和环境设置

    3. 调用system_call内核函数,通过系统调用号获取对应的服务例程

    4. 调用系统调用处理例程

    5. 使用特定指令从系统调用返回用户态(iret、sysexit、sysret)

    系统调用指令

    向内核发起系统调用需要使用特定的指令。在Linux中,传统的方法是使用汇编指令int发起中断,使用0x80(128)号中断使CPU进入内核态,之后调用对应的中断响应函数system_call来执行系统调用例程。

    由于通过中断方式发起系统调用的性能较差,较新的CPU和内核都支持使用sysenter和syscall这两条专用指令来发起系统调用。其中sysenter在32位系统中使用,对应的退出指令为sysexit;syscall在64位系统中使用,对应的退出指令为sysret。

    以sysenter为例,使用该指令时,首先调用__kernel_vsyscall()函数保存用户态堆栈;之后执行sysenter指令切换到内核态;最后开始执行sysenter_entry()函数设置内核态堆栈,并根据系统调用号调用处理例程,之后的逻辑和system_call相似。

    内核实现逻辑

    内核调用系统调用处理例程的核心数据结构是sys_call_table,这个数据结构在<arch/x86/entry/syscall_64.c>中定义如下:

    /* this is a lie, but it does not hurt as sys_ni_syscall just returns -EINVAL */
    extern asmlinkage long sys_ni_syscall(const struct pt_regs *);
    #define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
    #include <asm/syscalls_64.h>
    #undef __SYSCALL_64
    
    #define __SYSCALL_64(nr, sym, qual) [nr] = sym,
    
    asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    	/*
    	 * Smells like a compiler bug -- it doesn't work
    	 * when the & below is removed.
    	 */
    	[0 ... __NR_syscall_max] = &sys_ni_syscall,
    #include <asm/syscalls_64.h>
    };
    

    sys_call_table是一个函数指针数组,其中保存了所有系统调用处理函数的指针。system_call等函数以系统调用号作为下标,从sys_call_table中查找对应的系统调用函数执行。

    sys_call_table的初始化过程中,第一步是将所有指针数组元素赋值为sys_ni_syscall。这是为了避免有部分系统调用号没有被使用,没有定义对应的处理函数。sys_ni_syscall在<kernel/sys_ni.c>中定义,直接返回-ENOSYS,表示系统调用不存在。

    sys_call_table的具体内容在<asm/syscalls_64.h>中提供,内容类似于:__SYSCALL_64(19, sys_readv, sys_readv)。从之前对__SYSCALL_64宏的两处定义可见,syscall_64.c先将__SYSCALL_64宏展开为函数声明extern asmlinkage long sym(const struct pt_regs *),再将其展开为数组元素初始化语句[nr] = sym。

    需要注意的是<asm/syscalls_64.h>和提供系统调用号宏定义的头文件<asm/unistd_64.h>等文件在内核源码树中是不存在的,会在内核编译的预编译阶段自动生成。内核源码中真正定义系统调用号和处理函数的文件,是<arch/x86/entry/syscalls/syscall_64.tbl>,该文件的内容格式如下:

    #
    # 64-bit system call numbers and entry vectors
    #
    # The format is:
    # <number> <abi> <name> <entry point>
    #
    # The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
    #
    # The abi is "common", "64" or "x32" for this file.
    #
    0	common	read			__x64_sys_read
    1	common	write			__x64_sys_write
    2	common	open			__x64_sys_open
    3	common	close			__x64_sys_close
    4	common	stat			__x64_sys_newstat
    5	common	fstat			__x64_sys_newfstat
    6	common	lstat			__x64_sys_newlstat
    7	common	poll			__x64_sys_poll
    8	common	lseek			__x64_sys_lseek
    9	common	mmap			__x64_sys_mmap

    内核预编译系统根据这个文件中提供的系统调用号、系统调用名称和对应的处理函数名称来生成对应的头文件。

    添加新的系统调用

    根据上述分析,如果需要添加一个新的系统调用号和处理函数,需要完成如下修改:

    1. 在syscall_64.tbl中添加新的系统调用号、名称和处理函数名称。例如“666  common  mycall   __x64_sys_mycall”

    2. 提供sys_mycall函数实现。函数应定义为asmlinkage long sys_mycall(...)

    3. 如果sys_mycall函数实现在独立的.c文件中,需要将其加入lib/路径下的makefile中,在obj-y中添加.c文件路径

    之后重新编译内核即可提供自定义的系统调用功能。

    需要注意的是,sys_call_table数据结构在源码中是一个const变量,因此系统调用函数指针初始化完成后是不能修改的。如果需要在运行中动态修改或添加系统调用处理函数(例如通过可加载内核模块来提供处理函数),可以将const限定去掉,然后在运行中切换调用处理函数。

    展开全文
  • 本文以write()来简要分析一下Linux系统调用过程: write系统调用: 函数定义如下: lib/write.c 其中_syscall是一个宏定义, 内容类似如下: include/unistd.h 其实就相当于int write(int fd, const char *buf, off_...

    本文以write()来简要分析一下Linux系统调用过程:

    write系统调用:
    函数定义如下:

    在这里插入图片描述lib/write.c

    其中_syscall是一个宏定义, 内容类似如下:

    在这里插入图片描述
    include/unistd.h

    其实就相当于int write(int fd, const char *buf, off_t count) { … }
    就是定义了write()这个函数。

    可以看到,在执行write函数时,其实就是执行了int 0x80, 使用了0x80中断来实现系统调用,因为write函数调用的时候是在用户态的,而write要做的是把字符串写入到指定文件中,比如写到显存中,写入显存这个操作是需要内核态,也就是0特权级下才能进行的(当然要看显存对应选择子的DPL),也就是说现在在用户态完成不了这个写入显存的操作,怎么办?当然是先切换特权级到内核态,等完成了0特权级完成了操作后再回到用户态即可,int 0x80就完成了用户态向内核态的切换。

    Linux中不止一个write调用,还有open, read…等,那int 0x80如何确定是调用哪一个系统函数呢?
    int 0x80这个中断也有“参数”, 上面"0" (__NR_name) 部分就是int 0x80的参数,0x80中断会根据__NR_name这个下标来调用与write对应的系统调用,为什么说__NR_name是下标呢,既然说它是下标,那么就会有一个数组。是的,Linux内部维护了一个系统调用的数组,如下:

    在这里插入图片描述
    include/linux/sys.h

    这些下标定义如下:

    在这里插入图片描述
    include/unistd.h

    可以看到,里面存放了Linux所支持的所有系统调用(内核态函数的指针),而__NR_name就是每个用户态函数在这个数组对应内核态函数的下标。比如write()对应的内核态函数为sys_write,当调用write时,最终会调用到上面这个sys_call_table中的sys_write函数,下面看下是怎么通过int 0x80和他的参数__NR_name来调用对应的内核态函数的。

    在这里插入图片描述
    kernel/sched.c

    可以看到,在初始化的时候,int 0x80对应的处理函数为system_call, 当调用int 0x80时,会执行system_call这个函数,如下:

    在这里插入图片描述
    kernel/sys_call.s

    看上面99行,就是调用了sys_call_table中相应的项,其中eax中的值就是上面的__NR_name,(数组的下标), 比如write()调用到system_call时,__NR_write = 4, sys_call_table第5荐正好为sys_write, 所以就执行了相应的内核态函数(执行内核态才能执行的操作)。

    总结:

    Linux实现系统调用首先要有一个表来存放所有与用户态函数对应的系统态的函数(sys_call_table), 调用过程:user_func() --> int 0x80 (user_func对应的系统态函数在表中的下标, 记为index), 此时已经进入内核态 --> system_call(index) --> sys_func();


    Linux源码版本: 0.12

    展开全文
  • 不用syscall table的方法来截获系统调用的方法的分析 关注Linuxeden官方微博 前言: 拿到quack给我的这个文章真是很巧,正在整理分析linux系统调用的实现源代码,所以先翻译一下这 篇文章,然后谈一些自己的...
  • 本文介绍了系统调用的一些实现细节...然后,我们考察了Linux内核如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。最后讨论了如...
  • Linux系统调用流程分析 一直很好奇系统调用的详细过程,只简单的了解系统调用是用户与内核交互的借口,看了几天的内容,不知道从何下手开始写,今天略有头绪,做以简单总结。 1, 什么是系统调用。 系统调用:系统...
  • 写这篇文章是看到一个as汇编器里编写的汇编代码,有一个指令int 80h没有搞懂,然后自己查资料发现不少东西,本文旨在浅显的分析linux系统调用,主要是linux0.12内核来说! 目前操作系统内核的结构模式主要分为整体...
  • 1.linux系统调用的基本原理  linux的系统调用形式与POSIX兼容,也是一套C语言函数名的集合。然而,linux系统调用的内部实现方式却与DOC的INT 21H相似,它是经过INT 0X80H软中断进入后,再根据系统调用号分门别类地...
  • Linux系统调用

    2012-04-23 23:52:28
    1. Linux系统调用原理 2. 系统调用的实现 3. Linux系统调用分类及列表 4.系统调用、用户编程接口(API)、系统命令和内核函数的关系 5. Linux系统调用实例 6. Linux自定义系统调用 1.系统调用原理 系统...
  • ← Printf从函数库到OS跟踪流程 printf和标准输出[z]→ Linux系统调用[z] <br />摘要:本期重点和大家讨论系统调用机制。其中涉及到了一些及系统调用的性能、上下文深层问题,同时也穿插着...
  • 系统调用本身是软中断,使用系统调用,内核也陷入内核态,异常处理,找到相应的入口最后就会跳转到sys_mount,跳转到sys_mount之前的这个过程主要是跟系统的异常处理相关,以mips处理器为例,相关代码在arch/mips/...
  • linux的全部系统调用加起来大约只有250个左右。 2.API:  API常以c库(libc)的形式提供,c库提供了绝大部分API,每个系统调用在c库中都有对应的封装函数(通常封装函数与系统调用的名称相同)。系统调用与c库函
  • 一、系统调用初始化void __init trap_init(void) { ...... set_system_gate(SYSCALL_VECTOR,&system_call);//0x80 ...... }  对0x80中断向量,设置了系统调用的总入口system_call。static void __init set_...
  • Linux系统调用劫持:其实就是修改内核符号表,来达到一个劫持的作用。因为系统调用实际上是触发了一个0x80的软中断,然后转到了系统调用处理程序的入口system_call()。system_call()会检查系统调用号来得出到底是...
  • 本次的文章主要是继续讲解系统调用的过程,会更为详细的分析系统调用过程中的细节,包括进程调度的实际,实际服务程序的调用过程,现场的保存和恢复等等,与此同时简述了启动过程的调试以及实验过程。本次的文章中会...
  • 参考文章:http://blog.csdn.net/shaoguangleo/article/details/5822110linux中mmap系统调用原理分析与实现 1、mmap系统调用(功能) void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , ...
  • 主题 Linux 相关学习资料 http://xiaonieblog.com/?post=121 http://hbprotoss.github.io/posts/li-yong-ld_preloadjin-xing-hook.html http://www.catonma
  • 系统调用拦截的实现那么如何去实现系统调用拦截呢,通过学习系统调用的内核实现我们发现其实系统调用的地址是放在sys_call_table中通过系统调用号定位到具体的系统调用地址,然后开始调用,那么通过编写内核模块来...
1 2 3 4 5 ... 20
收藏数 242,653
精华内容 97,061
关键字:

分析linux的系统调用