乐于分享
好东西不私藏

《Linux-0.12 源码篇》- 04 系统调用机制

《Linux-0.12 源码篇》- 04 系统调用机制

字数 5987,阅读大约需 30 分钟

第四章:系统调用机制

系统调用是操作系统提供给用户程序的接口,是用户态和内核态之间的桥梁。用户程序无法直接访问硬件或内核数据,必须通过系统调用请求内核代理完成特权操作。Linux 0.12实现了一套优雅的系统调用机制:用户程序通过int 0x80中断陷入内核,内核根据系统调用号查表执行相应的内核函数,然后将结果返回用户态。这套机制保证了操作系统的安全性,同时也为用户程序提供了强大而统一的接口。

4.1 系统调用的本质

系统调用本质上是一种特殊的软件中断。在x86架构下,Linux使用int 0x80指令作为系统调用的统一入口。用户程序在调用系统函数(如read、write、fork等)时,实际上是执行了一段内嵌汇编代码,这段代码将系统调用号放入eax寄存器,将参数放入ebx、ecx、edx寄存器,然后执行int 0x80指令。

int 0x80指令触发CPU的中断机制,CPU自动完成以下操作:首先查找IDT(中断描述符表)的第0x80项,获取system_call函数的地址和段选择符;然后检查特权级,发现当前在用户态(CPL=3),而system_call在内核态(DPL=0),需要切换特权级;于是CPU自动将用户态的ss和esp压入内核栈,然后将eflags、cs、eip压栈,最后切换到内核态并跳转到system_call执行。

这个过程就像公民(用户程序)向政府(内核)申请办事(系统调用),必须通过指定的窗口(int 0x80),提交申请表(系统调用号和参数),然后等待政府工作人员(内核函数)办理并返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

              用户程序调用 read(fd, buf, count)                                    │                                    ▼                         _syscall3 宏展开                                    │            ┌──────────┼──────────┐            │                      │                      │            ▼                     ▼                     ▼ eax = __NR_read     ebx = fd              ecx = buf      (系统调用号)        (第1个参数)            (第2个参数)                                    │                                    ▼                                edx = count                                (第3个参数)                                    │           └──────────┼──────────┘                                    │                                   ▼                         执行 int 0x80 指令                                    │                                   ▼                      CPU 查找 IDT 第 0x80 项                     (Interrupt Descriptor Table)                                    │                                    ▼                   检测特权级:CPL=3 (用户态)                              DPL=0 (内核态)                                    │                                    ▼                          切换到内核栈                                    │                                    ▼                   压入上下文到内核栈 (从高到低)                                    │              ┌─────────┼─────────┐              │                    │                     │              ▼                   ▼                    ▼             ss                   esp                   eflags         (用户栈段)         (用户栈指针)      (标志寄存器)              ┌─────────┼──────────┐              │                     │                     │              ▼                    ▼                     │             cs                    eip                    │         (用户代码段)           (返回地址)                                                      │                                                       ▼                         跳转到 system_call

4.2 系统调用的完整流程

system_call是所有系统调用的汇编入口函数,定义在kernel/system_call.s中。为了真正看懂这段关键代码,我们有必要先通读它的实现,再结合文字说明理解每一条指令的作用。

system_call汇编代码逐行注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# kernel/system_call.s - Linux 0.12 系统调用入口.globl system_call, sys_call_table, nr_system_calls.align 2system_call:    push %ds                  # 保存原ds段寄存器    push %es                  # 保存原es段寄存器    push %fs                  # 保存原fs段寄存器    pushl %eax                # 保存eax(系统调用号)    pushl %ecx                # 保存ecx(第2个参数)    pushl %edx                # 保存edx(第3个参数)    movl $0x10,%edx           # 0x10 = GDT中内核数据段选择符    mov %dx,%ds               # ds = 内核数据段    mov %dx,%es               # es = 内核数据段    movl $0x17,%edx           # 0x17 = LDT中用户数据段选择符    mov %dx,%fs               # fs = 用户数据段(用于访问用户空间)    cmpl $nr_system_calls,%eax  # eax中是系统调用号    ja bad_sys_call           # 如果eax >= nr_system_calls,跳转到错误处理    call *sys_call_table(,%eax,4)  # 调用sys_call_table[eax]                                  # 也就是对应的sys_xxx内核函数    pushl %eax                # 将返回值压栈,稍后再恢复到eax    movl current,%eax         # eax = current,指向当前进程的task_struct    cmpl $0,state(%eax)       # 检查当前进程state是否为TASK_RUNNING    jne reschedule            # 如果不是就绪态,则需要重新调度    cmpl $0,counter(%eax)     # 检查时间片counter是否为0    je reschedule             # 时间片用完也需要重新调度restore:    movl current,%eax         # 再次获取current指针ret_from_sys_call:    # 处理信号之前,先恢复返回值    popl %eax                 # 恢复sys_xxx的返回值到eax    # 这里(在真正代码中)会检查是否有未处理信号    # 并在需要时调用do_signal(),本章后续详细分析    popl %edx                 # 恢复edx    popl %ecx                 # 恢复ecx    popl %ebx                 # 恢复ebx    pop %fs                   # 恢复fs    pop %es                   # 恢复es    pop %ds                   # 恢复ds    iret                      # 从中断返回,恢复eip、cs、eflags、esp、ss                              # 完成从内核态到用户态的切换reschedule:    call schedule             # 调用调度器选出下一个要运行的进程    jmp restore               # 调度完成后回到restore标签bad_sys_call:    movl $-ENOSYS,%eax        # 系统调用号非法,返回-ENOSYS    jmp ret_from_sys_call     # 走统一的返回路径

说明:上面的代码基于Linux 0.12源码略作整理,省略了部分与调试相关的微小细节,重点突出系统调用路径中的关键步骤。

system_call执行过程文字版

有了完整的汇编代码,我们再回到整体流程来理解system_call在做什么:

  1. 1. 保存现场
    • • 通过 push指令将 ds、es、fs、eax、ecx、edx依次压入内核栈。
    • • 保存它们的原因是:内核代码会自由使用这些寄存器,返回用户态时必须恢复原值。
  2. 2. 切换段寄存器
    • • 将 ds和 es设置为内核数据段选择符 0x10,内核之后通过这两个段寄存器访问自身数据。
    • • 将 fs设置为用户数据段选择符 0x17,约定“凡是通过 fs段访问的地址都是用户空间地址”。
    • • 这种约定让“访问内核数据”和“访问用户数据”在汇编层面清晰分离,避免混淆。
  3. 3. 检查系统调用号合法性
    • • 系统调用号存放在 eax中。
    • • 用 cmpl $nr_system_calls,%eax和 ja bad_sys_call判断是否越界。
    • • 越界则跳转到 bad_sys_call,直接返回 -ENOSYS(功能未实现)。
  4. 4. 通过sys_call_table间接调用内核函数
    • • sys_call_table是一个函数指针数组,下标就是系统调用号。
    • • 指令 call *sys_call_table(,%eax,4)等价于:
      • • 计算地址 sys_call_table + eax*4(每个指针4字节),
      • • 取出该地址处的函数指针并调用。
    • • 例如 eax=3时,调用的是 sys_read()
  5. 5. 检查是否需要调度
    • • 内核函数执行完后返回到system_call,返回值在 eax
    • • 先把 eax压栈保存,然后通过 current指针访问当前进程的 state和 counter字段:
      • • 如果 state!=TASK_RUNNING,说明进程睡眠/停止/退出,不应继续运行,需要调度;
      • • 如果 counter==0,说明时间片用完,也需要调度。
    • • 需要调度时调用 schedule(),选出新的进程,然后再跳回 restore继续后续流程。
  6. 6. 返回路径与ret_from_sys_call
    • • 无论是否调度,最终都会走到 ret_from_sys_call路径:
      • • 先从栈中弹出之前保存的返回值 eax
      • • 真实代码中在此处调用 do_signal(),检查并处理信号;
      • • 再依次恢复 edx、ecx、ebx、fs、es、ds
      • • 最后执行 iret,一次性恢复 eip、cs、eflags、esp、ss,回到用户态。
  7. 7. 错误处理路径bad_sys_call
    • • 如果系统调用号非法,直接将 eax设置为 -ENOSYS,然后复用同一条返回路径:
      • • 即跳转到 ret_from_sys_call,像正常系统调用一样恢复现场并返回用户态。

system_call整体时序图(更新版)

有了上面的代码分析,现在再看系统调用的时序,就更加清晰了:

调度器内核函数system_callCPU用户程序调度器内核函数system_callCPU用户程序int 0x80查IDT 切换特权级 压入ss/esp/eflags/cs/eip跳转到system_call入口push保存ds/es/fs/eax/ecx/edx设置ds=es=0x10, fs=0x17检查eax是否<nr_system_callscall *sys_call_table(,%eax,4)执行具体sys_xxx函数在eax中返回结果将eax压栈保存检查current->state/counter如需要则调用schedule()选出下一个要运行的进程弹出eax为最终返回值(真实实现中在此调用do_signal处理信号)恢复edx/ecx/ebx/fs/es/ds执行iret恢复eip/cs/eflags/esp/ss 返回用户态继续执行

通过这一节,我们从“纯文字描述”升级到了“源码级理解”:不仅知道system_call大致做了什么,更能精确对应到每一条汇编指令,为后续分析参数传递、信号处理、返回值等细节打下坚实基础。

4.3 系统调用宏_syscall0-3

用户程序不会直接编写int 0x80汇编代码,而是通过C库函数(如read、write)来调用。这些库函数的实现使用了系统调用宏_syscall0、_syscall1、_syscall2、_syscall3,定义在include/unistd.h中。这些宏根据参数个数不同而命名:_syscall0用于无参数的系统调用(如getpid),_syscall1用于一个参数(如close),_syscall2用于两个参数(如dup2),_syscall3用于三个参数(如read)。

下面是Linux 0.12中这些宏的真实定义,并加入了详细中文注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

// include/unistd.h - 系统调用封装宏// 无参数系统调用,例如 getpid()#define _syscall0(type,name) \ type name(void) \ { \     long __res; \     __asm__ volatile ( \         "int $0x80" \         : "=a" (__res)            /* 输出: eax -> __res */ \         : "0" (__NR_##name)       /* 输入: __NR_name -> eax (约束"0"复用上面的eax) */ \     ); \     if (__res >= 0)                /* 返回值>=0表示成功 */ \         return (type) __res; \     errno = -__res;                /* 返回值<0,取其相反数作为errno */ \     return -1;                     /* 函数返回-1表示出错 */ \ }// 1个参数系统调用,例如 close(fd)#define _syscall1(type,name,atype,a) \ type name(atype a) \ { \     long __res; \     __asm__ volatile ( \         "int $0x80" \         : "=a" (__res)            /* eax输出到__res */ \         : "0" (__NR_##name),      /* eax中是系统调用号 */ \           "b" ((long)(a))        /* 第1个参数a传入ebx */ \     ); \     if (__res >= 0) \         return (type) __res; \     errno = -__res; \     return -1; \ }// 2个参数系统调用,例如 mkdir(path, mode)#define _syscall2(type,name,atype,a,btype,b) \ type name(atype a, btype b) \ { \     long __res; \     __asm__ volatile ( \         "int $0x80" \         : "=a" (__res) \         : "0" (__NR_##name),      /* eax=系统调用号 */ \           "b" ((long)(a)),        /* ebx=第1个参数 */ \           "c" ((long)(b))         /* ecx=第2个参数 */ \     ); \     if (__res >= 0) \         return (type) __res; \     errno = -__res; \     return -1; \ }// 3个参数系统调用,例如 read(fd, buf, count)#define _syscall3(type,name,atype,a,btype,b,ctype,c) \ type name(atype a, btype b, ctype c) \ { \     long __res; \     __asm__ volatile ( \         "int $0x80" \         : "=a" (__res) \         : "0" (__NR_##name),      /* eax=系统调用号 */ \           "b" ((long)(a)),        /* ebx=第1个参数 */ \           "c" ((long)(b)),        /* ecx=第2个参数 */ \           "d" ((long)(c))         /* edx=第3个参数 */ \     ); \     if (__res >= 0) \         return (type) __res; \     errno = -__res; \     return -1; \ }

有了这些宏,内核或库开发者只需用一行宏定义,就能生成完整的系统调用封装函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// 在某个源文件中使用_syscall3定义read_syscall3(int, read, int, fd, char*, buf, int, count)// 展开后等价于:int read(int fd, char* buf, int count){    long __res;    __asm__ volatile (        "int $0x80"              /* 触发系统调用入口 */        : "=a" (__res)           /* eax输出到__res */        : "0" (__NR_read),       /* eax = __NR_read */          "b" ((long)(fd)),      /* ebx = fd */          "c" ((long)(buf)),     /* ecx = buf */          "d" ((long)(count))    /* edx = count */    );    if (__res >= 0)        return (int) __res;       /* 成功,直接返回结果 */    errno = -__res;               /* 失败,设置errno */    return -1;                    /* 返回-1表示出错 */}

约束字符串解释小结:

  • • “=a” (__res): 表示 __res是一个输出操作数,对应寄存器eax(a),=表示只写。
  • • “0” (_NR##name): 表示这个输入操作数使用与第0号约束(即上面的”=a”)相同的寄存器,因此系统调用号会被放入eax。
  • • “b”、”c”、”d”: 分别指定参数放入ebx、ecx、edx寄存器,对应系统调用约定的前三个参数位置。

这套宏机制非常精巧,它隐藏了系统调用的底层细节(寄存器操作、中断指令等),让程序员可以像调用普通C函数一样调用系统函数。同时,通过内嵌汇编的方式,避免了额外的函数调用开销,提高了效率。这就像银行为客户提供了ATM机:客户只需要按照屏幕提示操作(调用API),无需了解内部的复杂流程(系统调用机制),就能完成取款(系统功能)。

4.4 系统调用表sys_call_table

sys_call_table数组是系统调用机制的核心索引表,定义在include/linux/sys.h中。这个数组中的每个元素是一个函数指针,指向对应系统调用号的内核函数。数组的定义格式是 sys_fn_ptr sys_call_table[] = {sys_setup, sys_exit, sys_fork, sys_read, ...},其中sys_fn_ptr是函数指针类型的别名。

Linux 0.12共有82个系统调用,因此sys_call_table有82个元素。系统调用号从0开始编号:0是sys_setup,1是sys_exit,2是sys_fork,3是sys_read,4是sys_write,等等。每个系统调用都有一个对应的宏定义__NR_xxx,例如__NR_read值为3,__NR_write值为4,这些宏定义在include/unistd.h中。

当system_call执行 call *sys_call_table(,%eax,4)时,如果eax是3(__NR_read),就会调用sys_call_table[3],即sys_read函数。sys_read定义在fs/read_write.c中,它的函数签名是 int sys_read(unsigned int fd, char * buf, int count),参数从栈中获取(system_call已经将ebx、ecx、edx压栈)。

系统调用表的设计体现了查表法的威力:只需要一个简单的数组,就能将系统调用号映射到具体的内核函数,无需复杂的if-else或switch-case分支。这种方式不仅高效,而且易于扩展,添加新系统调用只需要在数组末尾添加一项,并增加nr_system_calls计数。

系统调用号
符号常量
内核函数
功能说明
参数个数
0
__NR_setup
sys_setup
系统初始化
1
1
__NR_exit
sys_exit
进程退出
1
2
__NR_fork
sys_fork
创建子进程
0
3
__NR_read
sys_read
读取文件
3
4
__NR_write
sys_write
写入文件
3
5
__NR_open
sys_open
打开文件
3
6
__NR_close
sys_close
关闭文件
1
7
__NR_waitpid
sys_waitpid
等待子进程
3
10
__NR_unlink
sys_unlink
删除文件
1
11
__NR_execve
sys_execve
执行程序
0(特殊)

4.5 参数传递机制

系统调用的参数传递遵循固定的约定:最多支持3个参数,分别通过ebx、ecx、edx寄存器传递。这个约定是GCC编译器和Linux内核共同遵守的ABI(Application Binary Interface)的一部分。第一个参数在ebx中,第二个在ecx中,第三个在edx中。如果系统调用的参数少于3个,未使用的寄存器内容会被忽略。

system_call在保存现场时,会依次将edx、ecx、ebx压入内核栈。由于栈是向下增长的(从高地址向低地址),压栈顺序是edx、ecx、ebx,因此在栈中的布局从低地址到高地址是ebx、ecx、edx。C语言函数调用时参数是从右往左压栈,因此内核函数声明的参数顺序(从左到右)正好对应栈中参数的顺序(从低地址到高地址)。

例如sys_read的声明是 int sys_read(unsigned int fd, char * buf, int count),调用时fd在ebx中,buf在ecx中,count在edx中。system_call将edx、ecx、ebx依次压栈后,栈中从低地址到高地址依次是fd、buf、count,正好匹配sys_read的参数顺序。这样sys_read函数可以像普通C函数一样访问参数,无需关心参数从哪里来。

对于超过3个参数的系统调用(Linux 0.12中极少),通常采用间接传递的方式:将多个参数打包成一个结构体,然后将结构体的指针作为参数传递。例如sys_socketcall就是这样处理的,它的第二个参数是一个指向参数数组的指针。

4.6 返回值与错误处理

系统调用的返回值通过eax寄存器传递。按照Linux的约定,如果返回值大于等于0,表示调用成功,返回值的具体含义取决于系统调用(例如read返回实际读取的字节数,fork在父进程中返回子进程PID)。如果返回值是负数,表示调用失败,返回值的绝对值是错误码(定义在include/errno.h中)。

内核函数直接返回负的错误码,例如sys_open打开不存在的文件会返回-ENOENT(值为-2)。system_call将这个返回值保存在eax中并传递给用户态。_syscall宏中的代码检测到eax为负数后,将其绝对值赋给全局变量errno,然后让系统调用函数返回-1。

用户程序通过检查函数返回值是否为-1来判断系统调用是否出错,如果出错则查看errno获取具体错误码。例如以下代码片段:调用open打开文件,如果返回值是-1,说明出错,此时errno可能是ENOENT(文件不存在)、EACCES(权限不足)或其他错误码,通过判断errno可以确定错误原因并采取相应的处理措施。

1
2
3
4
5
6
7
8
9
10
11
12

int fd = open("/path/to/file", O_RDONLY);if (fd == -1) {    if (errno == ENOENT) {        printf("文件不存在\n");    } else if (errno == EACCES) {        printf("权限不足\n");    } else {        printf("其他错误:%d\n", errno);    }    return -1;}// 使用fd进行文件操作

这种错误处理机制简洁而统一:所有系统调用都使用相同的返回值约定,用户程序可以用统一的方式处理错误。这就像快递配送,成功时返回包裹(正数或0),失败时返回错误单(-1),错误单上注明原因代码(errno),客户可以根据代码查询失败原因。

4.7 信号处理与系统调用的关系

系统调用返回前会检查并处理信号,这是在ret_from_sys_call段完成的。如果当前进程有待处理的信号(signal & ~blocked不为0),就会调用do_signal()函数。do_signal()会检查每个待处理信号对应的sigaction,如果设置了信号处理函数,就在用户态栈上构造一个调用信号处理函数的栈帧,并修改内核栈中保存的eip,使其指向信号处理函数。

当system_call执行iret返回用户态时,用户程序会先执行信号处理函数,处理完信号后再返回到被中断的位置继续执行。这样就实现了异步信号处理:信号可以在任何时候到达,但只在从内核态返回用户态时才被处理,不会打断内核代码的执行。

对于某些系统调用(如read等待键盘输入),如果在等待过程中收到信号,系统调用会被中断,提前返回错误码-EINTR。用户程序需要检测到这个错误并决定是否重试。这种机制保证了信号的及时响应,避免进程因等待I/O而无法处理紧急信号(如SIGINT终止信号)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

                        系统调用执行完毕                                   │                                   ▼                   返回到 ret_from_sys_call                                   │                                   ▼               ┌─────────────────┐               │   有待处理信号?               │               └────────┬────────┘                                   │               ┌────────┴────────┐               │有                                   │无               ▼                                     ▼        调用 do_signal                   恢复寄存器               │                                      │               ▼                                     │        检查 sigaction                          │        (信号处理函数注册信息)              │               │                                      │               ▼                                     │     在用户栈构造信号处理栈帧             │     (保存返回地址、寄存器等)              │               │                                      │               ▼                                     │    修改保存的 eip 指向信号处理函数    │               │                                      │               └────────┬────────┘                                   │                                   ▼                        执行 iret 返回用户态                                   │                                   ▼               ┌─────────────────┐               │   修改过 eip?                  │               └────────┬────────┘                                   │               ┌────────┴────────┐               │是                                   │否               ▼                                     ▼        先执行信号处理函数            返回原中断位置        (用户定义的信号处理逻辑)       (read 等系统调用返回处)               │               ▼   信号处理完毕后返回原位置   (通过 sigreturn 再次 iret)

4.8 实际示例:一个完整的write系统调用

让我们追踪一个完整的write系统调用过程。用户程序执行 write(1, "Hello", 5),意图向标准输出(文件描述符1)写入字符串”Hello”。这行代码经过编译后,实际上调用的是_syscall3宏生成的write函数。

write函数首先将__NR_write(值为4)放入eax,将1放入ebx,将”Hello”的地址放入ecx,将5放入edx。然后执行int 0x80指令,CPU查IDT表项0x80,切换到内核态,压入用户态的ss、esp、eflags、cs、eip到内核栈,跳转到system_call。

system_call将ds、es、fs、eax、edx、ecx、ebx依次压栈,设置ds=es=0x10(内核数据段),fs=0x17(用户数据段)。检查eax(4)小于nr_system_calls(82),合法。然后执行 call *sys_call_table(,%eax,4),即调用sys_call_table[4],也就是sys_write函数。

write触发系统调用

sys_write定义在fs/read_write.c中,它从栈中取出参数:fd=1,buf=”Hello”的地址,count=5。sys_write首先通过fd=1查找file结构,发现fd 1是标准输出,对应的是终端设备文件。于是调用file->f_op->write(实际是tty_write函数)将数据写入终端。tty_write将字符逐个放入终端输出队列,由终端驱动程序负责实际显示到屏幕上。写入完成后sys_write返回5(成功写入5字节)。

系统调用进入sys_write

返回到system_call,eax=5被压栈保存。检查当前进程状态,进程仍在就绪状态且时间片未用完,无需调度。检查信号,无待处理信号。恢复寄存器,从栈中弹出ebx、ecx、edx、eax(eax=5)、fs、es、ds。执行iret,弹出eip、cs、eflags、esp、ss,返回到用户态write函数的下一条指令。write函数中的内嵌汇编返回,__res=5。检查__res>=0,成功,返回5。用户程序得到返回值5,确认写入成功。

4.9 系统调用的性能考虑

系统调用虽然提供了强大的功能,但也引入了不小的开销。每次系统调用都需要:执行int 0x80指令触发中断,CPU切换特权级和栈,保存和恢复寄存器,查找sys_call_table,调用内核函数,处理信号,再切换回用户态。整个过程涉及大量的上下文切换和内存访问,比普通函数调用慢得多。

因此,高性能程序会尽量减少系统调用的次数。例如,与其多次调用write每次写一个字节,不如一次write写多个字节。标准C库提供了缓冲I/O(如fwrite、fprintf),在用户空间维护缓冲区,积累一定数据后再一次性调用系统调用写入,大大减少了系统调用次数。

在后来的Linux版本中,引入了vsyscall和vDSO机制,将某些简单的系统调用(如gettimeofday、clock_gettime)的代码映射到用户空间,允许用户程序直接调用这些函数而无需陷入内核,进一步提高了性能。这些优化体现了操作系统设计中的权衡:在安全性和性能之间寻找平衡点。

通过本章的学习,我们深入理解了Linux 0.12的系统调用机制:从int 0x80中断到system_call汇编函数,从系统调用表到参数传递,从返回值处理到信号处理,以及完整的调用流程。系统调用是操作系统内核与用户程序交互的唯一正规途径,理解它的实现原理对于深入掌握操作系统至关重要。

4.10 参考资料

本章内容基于以下资料编写:

Intel官方文档

  • • Intel 80386 Programmer’s Reference Manual (1986)
    • • Chapter 9: Exceptions and Interrupts – 详细描述了软件中断int指令的执行过程
    • • Chapter 6: Protection – 介绍了特权级检查和栈切换机制
    • • Chapter 5: Registers – 说明了寄存器在过程调用中的使用约定
    • • 在线阅读:https://css.csail.mit.edu/6.858/2014/readings/i386.pdf

Linux 0.12源代码文件

  • • kernel/sys_call.s – system_call汇编入口函数、ret_from_sys_call
  • • include/unistd.h – _syscall0-3宏定义、系统调用号__NR_xxx
  • • include/linux/sys.h – sys_call_table系统调用表定义
  • • fs/read_write.c – sys_read、sys_write等文件系统相关系统调用
  • • kernel/fork.c – sys_fork系统调用实现
  • • kernel/exit.c – sys_exit系统调用实现
  • • kernel/signal.c – do_signal信号处理函数

“上善若水,水善利万物而不争。” —— 老子