在现代操作系统中,内核提供了用户进程与内核进行交互的一组接口,这些接口在应用程序和内核之间扮演了使者的角色。这些接口保证了系统的稳定可靠,避免应用程序肆意妄行。
一、与内核通信
系统调用在用户空间进程和硬件设备之间添加了一个中间层。有三个作用:
- 第一,它为用户空间提供了一种硬件的抽象接口。举例来说,当需要读写文件的时候,可以不关心磁盘介质和类型,甚至不需要关心文件所在的文件系统。
- 第二,系统调用保证了系统的稳定和安全。可以避免用户程序不正确的使用硬件设备,窃取其他进程的资源等危害系统的行为。
- 第三,每个进程都运行在虚拟系统中,如果应用程序可以随意访问硬件而内核又对此一无所知,几乎就没法实现多任务和和虚拟内存。
在 Linux 中,系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。
二、API
应用编程接口(API,Application Programming Interface)定义了一组应用程序使用的编程接口,它可以使用系统调用实现,也可以不使用:
从程序员的角度来看,系统调用无关紧要,他们只需要和 API 打交道就行了。相反,内核只跟系统调用打交道。
三、系统调用
每个系统调用被赋予了一个系统调用号,通过这个独一无二的号就可以关联系统调用。
getpid() 返回的是 tgid(线程组 ID),对于普通的进程来说,TGID 和 PID 相等,对于线程来说,同一线程组内的所有线程及其 TGID 都相等。
Linux 系统调用实现十分简洁。
应用程序通过软中断来通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序在内核空间执行系统调用。软中断是通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。
在 x86 上,系统调用号是通过 eax 寄存器传递给内核的(系统调用的返回值也是通过 eax 寄存器传递给用户空间的)。除了系统调用号以外,还需要传入一些其他的参数给内核,同样也是通过寄存器来传递的,ebx、ecx、edx、esi、edi 按顺序存放前五个参数(当大于 6 个的时候就用一个单独的寄存器存放参数在内存中的地址)。
在接收一个用户空间的指针之前,内核必须保证:
- 指针指向的区域属于用户空间。进程不能哄骗内核去读内核空间的数据。
- 指针指向的内存区域在进程的地址空间里。进程绝不能哄骗内核去读其他进程的数据。
- 用户必须有被指针访问的内容的相应访问权限。进程绝不能绕过内存访问限制。
内核提供了两个方法来完成必须的检查和内核空间和用户空间之间的数据拷贝:
- 为了向用户空间写入数据,内核提供了 copy_to_user(),它需要三个参数。第一个参数为进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个参数为需要拷贝的数据长度(字节数)。
- 为了从用户空间读数据,内核提供了 copy_from_user(),所需的参数和 copy_to_user() 类似。
如果执行失败,这两个函数返回的都是没能完成拷贝的数据字节数。成功则返回 0。这两个函数都可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时进程会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。
用户可以使用 capable() 函数来检查是否有权能对指定的资源进行操作,返回非 0 值就有权进行操作,反之则无权。
四、系统调用上下文
内核在执行系统调用的时候处于进程上下文,current 指针指向当前任务,即引发系统调用的那个进程。
在进程上下文中,内核可以休眠(比如在系统调用阻塞或显式调用 schedule() 的时候)并且可以被抢占。中断处理程序不可休眠。在进程上下文中可以被抢占。因为新的进程可以使用相同的系统调用,所以必须保证该系统调用是可重入的。
当系统调用返回的时候,控制权仍在 system_call() 中,它最终会负责切换到用户空间,并让用户进程继续执行下去。
ABI 为应用程序二进制接口。