操作系统基础 | 3 系统编程概述
系统调用(System Calls)
- 系统调用是进程进入内核、请求内核代表其执行某些操作的受控入口。内核通过系统调用
API 向程序提供各种服务,如创建新进程、执行 I/O、创建管道等。(可参考
syscalls(2)
手册页查看 Linux 系统调用列表。) - 系统调用的几个基本特点:
- 系统调用会将处理器状态从用户态切换到内核态,使 CPU 能访问受保护的内核内存。
- 系统调用集合是固定的,每个系统调用有唯一编号(程序通常通过名称而非编号调用)。
- 每个系统调用可以有参数,用于在用户空间和内核空间之间传递信息。
系统调用的执行流程(以 x86-32 为例)
- 应用程序通过 C 库中的封装函数(wrapper function)发起系统调用。
- 封装函数将参数从栈传递到特定寄存器,以便内核处理。
- 封装函数将系统调用编号写入特定寄存器(如
%eax
)。 - 封装函数执行 trap 指令(如
int 0x80
),使处理器从用户态切换到内核态,执行内核 trap 向量表中对应位置的代码。新架构用sysenter
指令,速度更快。 - 内核的
system_call()
例程被调用,主要步骤:- 保存寄存器到内核栈
- 检查系统调用编号有效性
- 根据编号查找并调用对应的系统调用服务例程(如
sys_execve()
),并检查参数有效性,执行所需操作(如 I/O、内存操作等),返回结果状态 - 恢复寄存器,并将返回值放到栈上
- 返回到封装函数,同时切换回用户态
- 如果系统调用返回值表示错误,封装函数会设置全局变量
errno
,并返回 -1 表示失败;成功时返回非负值。
- Linux 系统调用服务例程约定:返回非负值表示成功,负值(为 errno
常量的相反数)表示错误。C 库封装函数会将负值转为正数赋给
errno
,并返回 -1。 - 这种约定假设系统调用不会在成功时返回负值,但极少数例外(如
fcntl()
的F_GETOWN
操作)。 - 例如,
execve()
系统调用在sys_call_table
的第 11 项,指向sys_execve()
服务例程。 - 系统调用的实现虽然对程序员透明,但实际上涉及许多底层操作,因此系统调用有一定的性能开销。例如,
getppid()
1,000 万次调用约需 2.2 秒,而等价的 C 函数只需 0.11 秒。 - 在本书中,“调用系统调用 xyz()”通常指调用对应的 C 库封装函数。
- 可用
strace
命令跟踪程序的系统调用,便于调试和分析。
库函数(Library Functions)
- 库函数是标准 C 库(如 glibc)中包含的大量函数之一。它们的功能非常多样,比如打开文件、时间格式转换、字符串比较等。
- 许多库函数并不涉及系统调用(如字符串处理函数),而有些库函数则是对系统调用的封装。例如,
fopen()
库函数内部会调用open()
系统调用来打开文件。 - 库函数通常比底层系统调用更易用。例如,
printf()
提供了格式化输出和缓冲功能,而write()
只负责输出字节块。malloc()
和free()
也比底层的brk()
系统调用更方便。
标准 C 库与 GNU C 库(glibc)
- 不同 UNIX 实现有不同的标准 C 库实现。Linux 上最常用的是 GNU C 库(glibc)。
查看系统上的 glibc 版本
可以直接运行 glibc 的共享库文件来查看版本信息,例如:
输出内容会包含 glibc 的版本号等信息。1
$ /lib/libc.so.6
在某些发行版中,glibc 可能不在
/lib/libc.so.6
,可以用ldd
命令查看某个程序依赖的 glibc 路径:1
2$ ldd myprog | grep libc
libc.so.6 => /lib/tls/libc.so.6 (0x4004b000)程序可以通过两种方式获取 glibc 版本:
- 编译时检测常量:glibc 2.0 及以后定义了
__GLIBC__
和__GLIBC_MINOR__
两个常量,可用于#ifdef
判断。 - 运行时调用函数:可用
gnu_get_libc_version()
获取运行时 glibc 版本号。该函数返回如 "2.12" 的版本号字符串。1
2
const char *gnu_get_libc_version(void);
- 编译时检测常量:glibc 2.0 及以后定义了
还可以用
confstr()
函数获取_CS_GNU_LIBC_VERSION
配置变量,返回如 "glibc 2.12" 的字符串。
处理系统调用和库函数的错误
- 几乎所有系统调用和库函数都会返回一个状态值,指示调用是否成功。必须始终检查这个返回值,如果失败,应采取适当措施(至少要输出错误信息)。
- 虽然省略这些检查看似省事,但实际上会导致难以排查的 bug,浪费大量调试时间。
系统调用错误处理
- 每个系统调用的手册页会说明其返回值,通常返回 –1 表示出错。例如:
1
2
3
4
5
6
7fd = open(pathname, flags, mode);
if (fd == -1) {
/* 错误处理代码 */
}
if (close(fd) == -1) {
/* 错误处理代码 */
} - 系统调用失败时,会将全局变量
errno
设为正值,表示具体错误类型。需要包含<errno.h>
头文件。 errno
的符号常量都以E
开头,手册页的 ERRORS 部分会列出可能的 errno 值。- 示例:
1
2
3
4
5
6
7
8cnt = read(fd, buf, numbytes);
if (cnt == -1) {
if (errno == EINTR)
fprintf(stderr, "read was interrupted by a signal\n");
else {
/* 其他错误 */
}
} - 成功的系统调用不会将
errno
设为 0,因此不能用errno == 0
判断是否成功。应先检查返回值,再看 errno。 - 少数系统调用(如
getpriority()
)在成功时也可能返回 –1。此时应在调用前将errno
设为 0,调用后判断:如果返回 –1 且 errno 不为 0,则为错误。
错误信息输出
- 常用
perror()
和strerror()
输出错误信息。perror()
:输出自定义信息和 errno 对应的错误描述。用法示例:1
2
void perror(const char *msg);1
2
3
4
5fd = open(pathname, flags, mode);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}strerror()
:返回 errno 对应的错误字符串。注意返回的字符串可能被后续调用覆盖。1
2
char *strerror(int errnum);
- 如果错误号未知,
strerror()
返回 "Unknown error nnn" 或 NULL。 - 这两个函数支持本地化,错误信息会用本地语言显示。
库函数错误处理
- 不同库函数返回不同类型和数值表示失败(需查阅手册页)。
- 常见几类:
- 与系统调用一致:返回 –1,errno 指示错误(如
remove()
)。 - 返回其他错误值:如
fopen()
出错返回 NULL,errno 反映具体错误。 - 不使用 errno:某些库函数不用 errno,具体错误判断方式见手册页。此时不应用 errno、perror() 或 strerror()。
- 与系统调用一致:返回 –1,errno 指示错误(如