1. 进程创建
1.1 fork函数
cs
#include <unistd.h>
pid_t fork(void);
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝到子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
1.2 fork返回值
- 子进程返回0
- 父进程返回的是子进程的pid
- 创建出错返回-1
1.3 写时拷贝
刚创建时,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本:
1.4 fork调失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
2. 进程终止
本质是释放系统资源
2.1 进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.2 进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
- 从main返回
- 调用exit
- 调用_exit

异常退出: ctrl + c,信号终止
2.2.1 退出码
程序返回退出代码0 时表示执行成功没有问题。 !0被视为不成功
2.2.2 _exit函数
cs
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执行$?发现 返回值是255
2.2.3 exit函数
cs
#include <unistd.h>
void exit(int status);
exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:
- 执行用户通过atexit或on_exit定义的清理函数
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit

3. 进程等待
3.1 进程等待必要性
- 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 进程等待的方法
1)wait
cs
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
2)waitpid
cs
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是
否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的
退出码)
options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得子进程退出信息
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
- 如果不存在该子进程,则立即出错返回
3)获取子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
- 如果传递NULL,表示不关心子进程的退出状态信息
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16bit位):

4)阻塞与非阻塞等待
cs
// 阻塞模式(第三个参数=0)
waitpid(-1, &status, 0);
// 非阻塞模式(第三个参数=WNOHANG)
waitpid(-1, &status, WNOHANG);
1. 阻塞模式
- 行为 :调用
waitpid后,父进程会暂停所有操作(挂起),不再执行后续代码,直到「子进程退出」这个事件发生。 - 核心特点:等待期间,调用者(父进程)"啥也干不了",CPU 不会给它分配时间片(节省 CPU 资源)
2. 非阻塞模式(时时询问)
- 行为 :调用
waitpid后,父进程不会暂停 ,而是立即返回结果:- 如果子进程还在运行(事件未发生),返回
0; - 如果子进程已退出(事件发生),返回子进程 PID;
- 失败返回
-1。
- 如果子进程还在运行(事件未发生),返回
- 核心特点:等待期间,调用者(父进程)"不闲着",可以执行其他任务,但需要主动循环查询结果(轮询),可能消耗更多 CPU。
4. 进程程序替换
fork 创建的子进程只是执行父进程代码的 "不同分支",但本质上子进程和父进程跑的是同一个程序文件(a.out)。但实际开发中,子进程往往需要执行 另一个完全不同的程序,所以进程的程序替换来完成这个功能
4.1 替换原理
1. 「替换」的是用户空间,不是进程本身
- 子进程调用 exec 前:和父进程共享同一个程序文件(a.out),用户空间是父进程的副本(代码、数据都一样);
- 子进程调用 exec 后:用户空间的所有内容被新程序(比如
/bin/ls)替换 ------ 原来的 a.out 代码、数据全没了,换成新程序的代码和数据,执行入口变成新程序的启动例程(最终调用新程序的main)。
2. 「不变」的是内核层面的进程属性
调用 exec 不会创建新进程(所以 PID 不变),进程的核心内核数据结构(task_struct)没被重建,只是更新了用户空间的映射。比如:
- PID、PPID(父进程 ID)不变;
- 已经打开的文件描述符(比如标准输入、输出)不变;
- 进程的工作目录、用户 ID、组 ID 不变。

4.2 替换函数
有六种 **exec***开头的函数
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值
- 原来的程序会被新程序完全覆盖了,换成了新程序的代码和数据
命名解释
| 后缀 | 含义 |
|---|---|
l |
list(列表传参):参数以可变参数列表形式传递,必须以 NULL 结尾 |
v |
vector(数组传参):参数以字符串数组形式传递,数组最后一个元素必须是 NULL |
p |
path(路径查找):自动从系统 PATH 环境变量中查找程序,无需写全路径 |
e |
environment(自定义环境):允许传入自定义环境变量数组,覆盖默认环境 |
cs
#include <unistd.h>
//exec*
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
1. execl ------ 列表传参 + 需全路径
cs
int execl(const char *path, const char *arg, ...);
execl("/bin/ls", "ls", "-l", NULL); // 正确:最后一个参数是NULL
// execl("/bin/ls", "-l", NULL); // 错误:第一个参数必须是程序名(ls)
2. execlp ------ 列表传参 + PATH 查找(最常用之一)
cs
int execlp(const char *file, const char *arg, ...);
execlp("ls", "ls", "-l", NULL); // 正确:自动从PATH找ls程序
execlp("pwd", "pwd", NULL); // 正确:执行pwd命令,无额外参数
3. execv ------ 数组传参 + 需全路径
cs
int execv(const char *path, char *const argv[]);
char *argv[] = {"ls", "-l", "-a", NULL}; // 数组结尾必须是NULL
execv("/bin/ls", argv);
4. execvp ------ 数组传参 + PATH 查找(最常用之一)
cs
int execvp(const char *file, char *const argv[]);
char *argv[] = {"ps", "aux", NULL};
execvp("ps", argv); // 自动从PATH找ps程序
5. execle ------ 列表传参 + 自定义环境
cs
int execle(const char *path, const char *arg, ..., char *const envp[]);
char *envp[] = {"MY_ENV=test", NULL};
// 执行 sh -c "echo $MY_ENV":通过Shell解析环境变量
execle("/bin/sh", "sh", "-c", "echo $MY_ENV", NULL, envp);
6. execvpe ------ 数组传参 + PATH 查找 + 自定义环境
cs
int execvpe(const char *file, char *const argv[], char *const envp[]);
char *argv[] = {"sh", "-c", "echo $MY_ENV", NULL};
char *envp[] = {"MY_ENV=test", NULL};
execvpe("sh", argv, envp); // 自动从PATH找sh程序
5. 自主Shell命令行解释器

所以要写⼀个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(wait)
1.从环境变量里获得UESR,HOSTNAME,PWD,模拟shell打印输出行结果

2.
bash
1 #include <iostream>
2 #include <cstdio>
3 #include <cstring>
4 #include <cstdlib>
5
6 #define COMMAND_SIZE 1024
7 #define FORMAT "[%s@%s %s]#"
8
9 const char *GetUserName()
10 {
11 const char *name = getenv("USER");
12 return name == NULL ? "None" : name;
13 }
14
15 const char *GetHostName()
16 {
17 const char *hostname = getenv("HOSTNAME");
18 return hostname == NULL ? "None" : hostname;
19 }
20
21 const char *GetPwd()
22 {
23 const char *pwd = getenv("PWD");
24 return pwd == NULL ? "None" : pwd;
25 }
26
27 void MakeCommandLine(char cmd_prompt[],int size)
28 {
29 //格式化命令行提示符
30 snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
31 }
32
33 void PrintCommandPrompt()
34 {
35 //打印命令行提示符
36 char prompt [COMMAND_SIZE];
37 MakeCommandLine(prompt,sizeof(prompt));
38 printf("%s",prompt);
39 fflush(stdout);//刷新
40 }
41
42 bool GetCommandLine(char *out,int size)
43 {
44
45 char *c = fgets(out,size,stdin);
46 if(c == NULL) return false;
47 //echo ls -a -l\n
48 out[strlen(out)-1]=0;//清理输入最后的回车键
49 if(strlen(out) == 0) return false;//用户什么都不输入情况
50 return true;
51 }
52
53 int main()
54 {
55 while(1)
56 {
57 // 1. 输出命令行提示符
58 PrintCommandPrompt();
59 // 2. 获取用户输入命令
60 char commandline[COMMAND_SIZE];
61 if(! GetCommandLine(commandline,sizeof(commandline)))
62 continue;
63 printf("%s\n",commandline);
64 }
65 return 0;
66 }
3.开始分割输入字符串,创建argv表
strtok 是 C 语言标准库(string.h)中的一个字符串分割函数,用于按照指定的分隔符将一个字符串拆分成多个子串
cpp
#include <string.h>
char *strtok(char *str, const char *delim);
str:首次调用时,传入要分割的原始字符串;后续调用时,传入NULL:表示 "继续处理上一个字符串"delim:一个字符串,包含所有用作分隔符的字符(例如",; "表示逗号、分号、空格都是分隔符)。
chdir
chdir 函数接收一个以空字符('\0')结尾的 C 风格字符串(const char*),用于指定要切换到的目标目录路径
因为先改变路径,再更新环境变量,如果一直用getenv(),则在实现内建命令cd时输出命令行的环境地址不能及时改变,所以需要使用系统调用getcwd()
char *getcwd(char *buf, size_t size);
buf:指向一个字符数组的指针,用于存储获取到的当前工作目录路径。size:指定buf的大小(以字节为单位)。
但此时环境变量中pwd还没有改变,仍需修改:把当前环境变量putenv导给环境变量
增加环境变量表:

