系统调用,库函数,exec系列函数,shell实现
一. 系统调用和库函数的区别
| 对比维度 | 系统调用 | 库函数 |
|---|---|---|
| 实现位置 | 内核空间 | 用户空间(如 glibc) |
| 运行权限 | 需要内核态权限 | 用户态即可运行 |
| 调用方式 | 通过软中断(int 0x80)或syscall指令 | 普通函数调用(call指令) |
| 上下文切换 | 有(用户态 → 内核态 → 用户态) | 无 |
| 执行效率 | 较慢(切换开销) | 较快 |
| 可移植性 | 依赖操作系统内核 | 跨平台(遵循语言标准) |
| 典型例子 | read(),write(),open(),fork() | printf(),fread(),strlen(),sin() |
| 是否能访问硬件 | 能(通过驱动程序) | 不能直接访问,必须通过系统调用 |
| 是否缓存 | 无缓存(直接操作内核缓冲区) | 可能有(如fread有用户态缓冲区) |
1.1核心概念
1. 系统调用号
每个系统调用都有一个唯一的编号(如
#define __NR_open 5)。用户程序不直接写函数名,而是通过寄存器(如
eax)传递这个编号给内核。
2. 用户空间 vs 内核空间
用户空间:应用程序运行的地方,受限制(不能直接访问硬件、内核数据)。
内核空间:操作系统核心运行的地方,拥有最高权限。
系统调用是从用户空间进入内核空间的唯一合法入口。
3. 系统调用表
内核维护一个函数指针数组(系统调用表)。
表的下标 = 系统调用号。
例如:
表[1] →
sys_exit()表[2] →
sys_fork()表[3] →
sys_read()表[4] →
sys_write()表[5] →
sys_open()
1.2执行流程(以open()为例)
这张图片讲的是系统调用的底层实现机制,特别是如何通过系统调用号和寄存器在用户空间和内核空间之间传递信息。
1.步骤详解
| 步骤 | 位置 | 动作 |
|---|---|---|
| 1 | 用户空间 | 应用程序调用open()库函数 |
| 2 | 用户空间 | 库函数将系统调用号(5)存入eax寄存器 |
| 3 | 用户空间 | 触发软中断(int 0x80或syscall指令) |
| 4 | 内核空间 | CPU 切换到内核态,根据eax中的编号查找系统调用表 |
| 5 | 内核空间 | 执行对应的内核函数sys_open() |
| 6 | 内核空间 | 返回结果(如文件描述符)给用户空间 |
2.关键寄存器的作用
| 寄存器 | 作用 |
|---|---|
eax | 存放系统调用号(如 open = 5) |
ebx, ecx, edx等 | 存放系统调用的参数(如 open 的文件路径、标志位) |
| 返回值 | 也通过eax返回(如文件描述符或错误码) |
1.3完整示例(逻辑流程)
text
应用程序: int fd = open("test.txt", O_RDONLY); ↓ libc 库函数: 将 __NR_open (5) 放入 eax 将参数放入 ebx, ecx 等 执行 syscall 指令 ↓ 内核: 根据 eax=5 查系统调用表 → sys_open() 执行 sys_open() 内核函数 ↓ 返回: eax = 文件描述符(如 3)或 -1(错误) ↓ 应用程序得到 fd = 3总结:如何切换?(用户态 → 内核态)
切换机制
用户程序无法直接进入内核态,必须通过专门的指令或中断触发 CPU 特权级切换。
两种主要方式
| 方式 | 说明 | 平台 |
|---|---|---|
软中断int 0x80 | 触发 0x80 号中断,CPU 查中断向量表进入内核 | 老式 x86(32位) |
syscall指令 | 专门用于系统调用的快速指令,效率更高 | x86_64(现代系统) |
详细切换步骤(以printf("hello")→write()为例)
text
用户程序: printf("hello"); ↓ // printf 内部处理格式化字符串 // 最终调用write 系统调用↓ // 库函数将系统调用号(write = 4)放入 eax 寄存器 // 将参数(fd, buf, count)放入 ebx, ecx, edx ↓ //执行软中断或 syscall 指令 int 0x80 // 或 syscall↓ 【CPU 硬件自动完成】 1. 保存当前寄存器状态(包括用户栈、指令指针) 2.从用户态切换到内核态(特权级从 3 切换到 0)3. 跳转到内核中预设的入口点(如 system_call) ↓ 内核空间: 根据 eax 中的系统调用号(4)查系统调用表 执行 sys_write() 内核函数 ↓ 将结果放入 eax(返回值) 执行 iret 或 sysret 指令返回用户态 ↓ 【CPU 恢复用户态】 恢复寄存器 返回用户程序继续执行 ↓ 用户程序: printf 返回输出字符数关键点总结
| 要素 | 说明 |
|---|---|
| 触发方式 | int 0x80(老)或syscall(新) |
| 寄存器传递 | eax= 系统调用号,ebx/ecx/edx= 参数 |
| 返回值 | 也通过eax返回 |
| 切换代价 | ~几十到几百个 CPU 周期(包括保存/恢复寄存器、TLB 可能失效) |
| 为什么不能直接调用 | 内核代码在独立地址空间,用户程序无法跳 |
二、exec系列函数
1.概念
exec系列函数的作用
不创建新进程,而是用新程序替换当前进程的代码段、数据段、堆栈。
替换成功后,原进程的后续代码不再执行(除非替换失败)。
常与
fork()配合使用:fork()创建子进程,子进程调用exec()执行新程序。新程序运行在原进程的 PID下(
getpid()返回值相同)
2.代码示例
3.6个exec函数的命名规律与区别
不必全记住,可以查询帮助手册 man exec
| 函数名 | 路径参数 | 参数传递方式 | 是否搜索PATH | 环境变量 |
|---|---|---|---|---|
execl | 完整路径 | 列表(可变参数) | ❌ | 继承 |
execlp | 文件名 | 列表(可变参数) | ✅ | 继承 |
execle | 完整路径 | 列表(可变参数) | ❌ | 自定义(通过参数传递) |
execv | 完整路径 | 数组(char *argv[]) | ❌ | 继承 |
execvp | 文件名 | 数组(char *argv[]) | ✅ | 继承 |
execve | 完整路径 | 数组(char *argv[]) | ❌ | 自定义(通过参数传递) |
命名规律
l:参数以列表(list)形式传递(可变参数,最后一个必须是(char*)0结尾)。v:参数以向量(vector/array)形式传递(字符指针数组)。p:搜索 PATH环境变量,只需传文件名(如"ps"而非/usr/bin/ps)。e:可以自定义环境变量(最后一个参数传envp[])。
4.参数格式说明
列表形式(l系列)
c
execl("/usr/bin/ps", "ps", "-f", (char*)0); // ↑路径 ↑第0个参数 ↑第1个参数 ↑结束标记每个参数单独写出,最后一个必须是
(char*)0或NULL。第一个参数通常是程序名(可随意,但惯例是文件名)。
向量/数组形式(v系列)
c
char* myargv[] = {"ps", "-f", (char*)0}; execv("/usr/bin/ps", myargv); // ↑ 数组名(首地址)先构造一个
char*数组,最后一项为NULL。传给
exec函数时,传数组名即可。
5.为什么需要(char*)0或NULL?
告诉被调用的新程序:参数列表到此结束。
如果不加,函数无法知道参数有多少个,会一直往后读内存,导致错误。
三、如何实现一个迷你版的Shell(命令解释器)
1.定义
命令解释器是用户与操作系统内核之间的接口程序,它接收用户输入的命令,解析并转换为内核能理解的系统调用,最终让内核执行相应操作。命令解释器是这类程序的统称,bash是其中最流行的一种实现。
Shell 的本质 =
fork()+exec()+wait()
核心流程
text
用户输入命令(如 "ls -l") ↓ Shell 解析命令 ↓ fork() 创建子进程 ↓ ┌─────────────────┐ │ 子进程 │ │ exec() 替换为 ls │ → 执行 ls -l → 输出结果 → 退出 └─────────────────┘ ↓ 父进程(Shell)wait() 等待子进程结束 ↓ 回到循环,打印提示符,等待下一条命令
为什么是 fork + exec?
| 步骤 | 作用 | 为什么必须这样 |
|---|---|---|
fork() | 复制出一个子进程 | 不能直接用exec()替换 Shell 自身,否则 Shell 就没了 |
exec() | 将子进程替换为目标程序 | 让子进程去执行用户命令 |
wait() | 父进程等待子进程结束 | 避免僵尸进程,且 Shell 需要等待命令执行完再继续 |
2.代码
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<string.h> int main() { while(1) { printf("\033[1;32mxatu@localhost\033[0m:\033[1;34m~$\033[0m"); fflush(stdout); // 强制刷新输出缓冲区 char buff[128] = {0}; fgets(buff, 128, stdin); // "ls\n" buff[strlen(buff) - 1] = '\0'; // "ls" if(buff[0] == '\0') { continue; } if(strcmp(buff, "exit") == 0) { break; } if(strcmp(buff, "clear") == 0) { printf("\033[2J\033[H"); // \033[2J → 清屏 // \033[H → 将光标移动到左上角 continue; } pid_t pid = fork(); if(pid == 0) { execlp(buff, buff, NULL); printf("exec err\n"); exit(0); } wait(NULL); } return 0; }