什么是系统调用?
用户程序在运行过程中,需要使用到系统的资源,此时,不能由该进程直接去访问,而是通过内核提供的接口来间接访问,转而由内核代替进程去访问底层资源,这些由内核提供的特殊接口就叫系统调用。
在linux中,系统调用是用户空间访问内核的唯一手段;它们是内核唯一的合法入口。
![](https://i-blog.csdnimg.cn/blog_migrate/158f68b21f5bed85c2184bb52ea6e4ed.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a4392f10cde6d75b7401392607de0c7a.png)
X86架构下系统调用
X86架构
![](https://i-blog.csdnimg.cn/blog_migrate/851a4e792950b1e8104a24ff745e6071.png)
用户调用C库,在C库中通过int 0x80指令触发软中断。
在执行INT指令时,实际完成了以下几条操作(保存进程ss、esp、eflags、cs、eip寄存器):
(1) 由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SS和ESP);
(2) 把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即核心栈)中;
(3) 把EFLAGS,外层CS,EIP推入高优先级堆栈(核心栈)中。
(4) 通过IDT加载CS,EIP(控制转移至中断处理函数)
由通用软中断处理程序处理请求,根据调用号找到实际系统调用处理程序。
参数传递
除了系统调用号以外,大部分系统调用都还需要一些外部的参数输人。所以,在发生异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上,ebx, ecx, edx, esi和edi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。
给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。
参数验证
系统调用必须仔细检查它们所有的参数是否合法有效。举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。
最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须被检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据。在接收一个用户空间的指针之前,内核必须保证:
- 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。
- 指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。
- 如果是读,该内存应被标记为可读。如果是写,该内存应被标记为可写。进程决不能绕过内存访问限制。
内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。注意,内核无论何时都不能轻率地接受来自用户空间的指针!这两个方法中必须有一个被调用。为了向用户空间写入数据,内核提供了copy_to_user(),它需要三个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址。最后一个参数是需要拷贝的数据长度(字节数)。
为了从用户空间读取数据,内核提供了copy_from_ user(),它和copy-to-User()相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。
如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,返回0。当出现上述错误时,系统调用返回标准-EFAULT。
注意copy_to_user()和copy_from_user()都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。