为什么需要受控入口

操作系统既要让程序高效运行,又要保留对硬件资源和受保护数据的控制。通常,应用在受限制的用户态执行,内核在权限更高的内核态处理受控操作。文件访问、创建进程等服务通过系统调用开放,不意味着调用者获得了任意执行特权指令的能力。

权限转换必须经过内核预设的入口,不能由应用自由选择内核中的跳转地址。内核仍须核验参数、地址及访问条件;如果只提供切换模式的机制而不检查输入,接口本身并不能保证安全。因此,系统调用既是服务接口,也是保护边界的一部分。

参考:[1]

从函数外观到内核处理

应用常通过C库包装函数发起请求。包装按平台约定准备系统调用编号与参数,放入指定寄存器或其他约定位置,再执行进入内核的专用指令。硬件与内核保存必要的执行状态,分派请求、完成检查和工作;可返回的调用结束后,结果再交回用户程序。

具体指令、编号和寄存器约定随体系结构及接口而异。Linux手册说明,包装通常还负责把底层错误表示转换成应用使用的返回值和 errno。因而不能把某个平台的寄存器布局,或者某一种错误编码,写成所有操作系统必须遵守的统一规则。

参考:[1] [2]

返回值是协议的一部分

以Linux的 read 为例,调用者提供文件描述符、目标缓冲区及最多读取的字节数。成功时返回实际读到的字节数;少于请求数量不一定是错误,可能是当前可用数据不足,或已接近文件结尾。对一次非零长度的普通文件读取,返回零表示到达文件结尾,而不是“读取失败”。

失败时,应用所见的包装接口返回 −1 并设置 errno,例如无效描述符对应 EBADF,不可访问的缓冲区地址可能导致 EFAULT。非阻塞读取当前不能完成时,可能返回 EAGAIN;信号中断又有自己的规则。正确使用接口须依据该调用的文档区分成功、部分完成与失败,不能只检查是否凑满请求长度。

参考:[3]

库函数不等于系统调用

Linux的库包装名称常与底层系统调用相同,但并非总是如此;包装还可能选择不同内核入口或做额外处理。另一层的标准输入输出库以 FILE 流提供缓冲和格式化等服务,隐藏较低层的I/O细节。一次高层库操作不应被直接当成一次内核进入。

例如,库可以先从已有输入缓冲区取出字符,或者把输出留在缓冲区,等到需要时再传递给底层。了解这层差别,有助于解释为何输出可能尚未立即出现,以及为什么分析程序的I/O行为时,应区分库的流状态、系统调用返回和资源本身。系统调用是请求内核服务的边界,不是所有函数调用的统称。

参考:[2] [4]