再谈进程-控制
进程控制
文章目录
- 进程控制
- 1. 进程创建
- 1.1 重识 fork 函数
- 1.1.1 fork 回顾
- 1.1.2 写时拷贝(Copy On Write)
- 1.2.3 fork常规用法
- 1.2.4 fork 失败的原因
- 2. 进程终止
- 2.1 进程退出场景
- 2.2 进程退出方法
- 2.2.1 退出码
- 2.3.1 exit VS _exit
- 3. 进程等待
- 3.1 基本概念
- 3.2 进程等待的方法
- 3.2.1 wait
- 3.2.2 waitpid
- 3.2.3 获取子进程 status
- 3.2.4 阻塞等待 VS 非阻塞等待
- 4. 进程程序替换
- 4.1 基本概念
- 4.2 替换原理
- 4.3替换函数
- 5. 微型Shell
- 5.1 目标
- 5.2 实现原理
- 5.3 实现源码
1. 进程创建
1.1 重识 fork 函数
1.1.1 fork 回顾
在进程概念中已经提到过 fork
函数
#include <unistd.h>
pid_t fork(void);
// 子进程返回 0,父进程返回子进程 pid,出错返回 -1
调用 fork
时,当控制转移到内核中的 fork
代码部分后,内核:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表中
fork
返回,调度器开始调度
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("Before fork: pid is: %d\n", getpid());pid_t pid = fork();if (pid == -1){perror("Fork error\n");}printf("After fork: pid is: %d\n", getpid());sleep(1);return 0;
}
运行结果
$ ./fork
Before fork: pid is: 1549627
After fork: pid is: 1549627
After fork: pid is: 1549628
可以看到,输出一行 After fork
和两行 After fork
,但是进程 1549628
并没有打印 After fork
原因如下👇
fork
之前父进程独立执行,fork
之后,父子进程分别执行,属于两个执行流,执行的先后顺序完全由调度器决定
1.1.2 写时拷贝(Copy On Write)
写时拷贝,其只有在真正需要修改内存内容时,才执行实际的物理内存复制操作。 在此之前,多个进程可以安全地共享同一份物理内存页的只读副本。通常,父子进程代码共享,父子不再写入时,数据也是共享的,当任意一方尝试写入的时候,便以写时拷贝的方式各自得到一份副本
写时拷贝 fork()
的工作原理
-
共享初始状态:
- 当父进程调用
fork()
时,操作系统不会立即复制父进程的物理内存页 - 相反,它只为子进程创建一个新的进程描述符(PCB) 和一个新的页表
- 子进程的页表被初始化为指向父进程相同的物理内存页
- 此时,父子进程共享所有物理内存页(包括代码、数据、堆、栈)。它们在用户空间看到的是完全相同的地址空间内容
fork()
立即返回给父子进程(父进程得到子进程PID
,子进程得到0
)。这个初始创建过程非常快,因为它只涉及创建元数据(页表项等),不涉及大量内存复制
- 当父进程调用
-
标记为只读(关键步骤):
- 为了让
COW
机制生效,操作系统会将所有被父子进程共享的物理内存页在双方的页表项中都标记为只读 - 这意味着父子进程目前都只能读取这些共享页,不能写入
- 为了让
-
触发写时拷贝:
- 当父进程或子进程中的任何一个首次尝试写入某个共享的内存页时,
CPU
会检测到这是一个对“只读”页的写操作 - 这会触发一个缺页异常
- 操作系统捕获这个异常
- 当父进程或子进程中的任何一个首次尝试写入某个共享的内存页时,
-
执行实际拷贝:
- 操作系统检查引发异常的地址对应的物理页
- 它发现这个页是共享的(标记为
COW
) - 操作系统:
- 分配一个新的、空闲的物理内存页
- 复制原来共享物理页的内容到这个新分配的页
- 更新尝试写入的那个进程(父或子)的页表项:使其指向新复制的物理页,并将页表项标记为可读写
- 保留另一个进程的页表项:仍然指向原来的物理页,并保持(或恢复)其可读写权限(如果该页之前未被另一个进程写入触发过
COW
)。如果另一个进程之后尝试写入这个页,它也会触发自己的COW
操作
- 操作系统在完成复制和页表更新后,返回到引发异常的指令处重新执行
- 这次,写操作就可以成功执行在新复制的、私有的物理页上了
-
后续写入:
- 对于已经被处理过
COW
的页(即某个进程已经拥有其私有副本),该进程后续的写入操作会直接作用于它的私有副本,不会再触发异常 - 另一个进程如果还未写入过该页,它访问的仍然是原来的共享页(或该页后来被它自己触发了
COW
后得到的私有副本)
- 对于已经被处理过
1.2.3 fork常规用法
fork()
的核心价值在于创建隔离的执行环境。其主要应用场景围绕进程创建、程序加载 (fork+exec
)、并发处理、守护进程实现、进程监控以及利用进程间通信实现协作
- 父进程希望复制自己,使父子进程同时执行不同的代码段
- 进程需要执行不同的程序
1.2.4 fork 失败的原因
fork()
系统调用失败(返回 -1
)通常是由于操作系统无法为新进程分配必要的资源。以下是最常见的原因及其触发场景:
-
进程数达到系统或用户限制 (最常见原因之一):
- 系统级限制: 操作系统内核有一个全局变量 (
max_processes
或类似) 限制整个系统能同时存在的最大进程数。当所有进程槽(PCB
表项)都被占用时,fork()
会失败 - 用户级限制 (
RLIMIT_NPROC
): 系统管理员或init
系统(如systemd
)可以为每个用户(或登录会话)设置允许创建的最大进程数限制。通过ulimit -u
命令可以查看或设置当前 Shell 会话的软限制。当用户或其子进程创建的总进程数(包括线程,因为Linux
上线程也是轻量级进程LWP
)达到这个上限时,fork()
会失败 - 触发场景:
- 编写了创建大量子进程且没有正确等待回收的代码(产生了“进程泄漏”或僵尸进程堆积)
- 恶意或错误的程序(如
fork
炸弹:(){ :|:& };:
) - 系统负载极高,大量用户或进程在运行
- 容器(如
Docker
)环境中被配置了严格的进程数限制 (pids.max
cgroup
参数)
- 系统级限制: 操作系统内核有一个全局变量 (
-
内存不足:
- 物理内存 + Swap 耗尽:
fork()
需要为新进程创建内核数据结构(如task_struct
, 页表)。更重要的是,即使采用写时拷贝 (COW
),也需要为新进程的页表分配内存(虚拟地址空间结构),并预留一部分物理内存来应对潜在的COW
操作。如果系统物理内存和交换空间 (Swap
) 都严重不足,无法满足这些需求,fork()
会失败 - 虚拟地址空间耗尽 (
RLIMIT_AS
): 每个进程都有一定的虚拟地址空间大小限制(在32
位系统上通常是3GB
用户空间,64
位系统则非常大)。虽然fork()
本身不立即分配大量物理内存,但它会为子进程复制父进程的虚拟地址空间布局。如果父进程的虚拟地址空间已经非常庞大且碎片化,或者用户进程的虚拟地址空间限制 (RLIMIT_AS
) 设置过低,导致内核无法为子进程分配足够的虚拟内存区域描述符 (vm_area_struct
) 或建立完整的页表映射,fork()
也可能失败。这在32
位系统处理超大进程时更常见
- 物理内存 + Swap 耗尽:
-
内核资源耗尽:
- PID 耗尽: 系统用于标识进程的
PID
号是有限的(由/proc/sys/kernel/pid_max
定义)。虽然这个上限通常很大(默认 32768),但在极端情况下(如短时间创建销毁大量进程),PID
号池可能被暂时耗尽,导致fork()
无法分配新的PID
- 其他内核结构: 创建进程还需要分配其他内核数据结构,如文件描述符表项(虽然文件描述符本身是继承的,但内核需要管理)、信号处理结构、各种内核锁和队列的资源等。如果这些底层内核资源池耗尽,
fork()
也会失败。这种情况相对少见,通常发生在内核存在资源泄漏或系统配置极不合理时
- PID 耗尽: 系统用于标识进程的
注意 ⚠ 检查 fork()
的返回值
2. 进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码
2.1 进程退出场景
- 代码运行完毕,结果正确: 通过退出状态码
0
表示 - 代码运行完毕,结果不正确: 通过非零的退出状态码 (通常 1-255) 表示具体错误类型
- 代码异常终止: 进程因接收到信号而被迫终止(如
SIGINT
(ctrl+c
),SIGSEGV
,SIGFPE
,SIGABRT
,SIGKILL
等)
2.2 进程退出方法
正常终止可以通过 echo $?
查看进程退出码, echo $?
在 shell
中存储的是上一个执行完毕的(子)进程的退出状态码
- 从
main
返回 - 调用
exit
- 调用
_exit
异常退出
ctrl + c
:信号终止
2.2.1 退出码
退出码也叫退出状态,在 Linux
中,退出码(Exit Status 或 Exit Code) 是进程终止时返回给其父进程(通常是 Shell
)的一个 0 到 255 之间的整数值,用于指示进程的执行结果。在命令结束以后,我们知道命令是成功完成的还是以错误结束。其基本思想是:0
通常表示成功,非 0
值表示各种类型的错误(具体含义由程序定义)
退出码 | 含义 | 常见场景/说明 |
---|---|---|
0 | 成功 (Success ) | 命令或程序按预期成功完成 |
1 | 一般性错误 (General Error/Catch-all for errors ) | 程序内部未指定具体错误类型的失败(例如,命令语法正确但操作失败) |
2 | Shell 内置命令或脚本的误用 (Misuse of shell builtins ) | 命令参数错误、非法选项(如 bash -c 'echo $@' 不带参数) |
126 | 命令不可执行 (Command invoked cannot execute ) | 文件权限不足(非可执行文件)、或尝试执行一个目录 |
127 | 命令未找到 (Command not found ) | PATH 中不存在该命令,或输入了错误的命令名 |
128 | 无效退出码参数 (Invalid argument to exit ) | 程序试图使用 exit 传递一个非整数参数(如 exit 3.14 ) |
128 + N | 进程因信号 N 终止 (Fatal error signal "N" ) | 最重要的异常退出码! 表示进程被信号 N 强制终止 |
130 | (128 + 2 ) 进程被 SIGINT 中断 | 用户按下了 Ctrl+C 终止前台进程 |
137 | (128 + 9 ) 进程被 SIGKILL 强制杀死 | 使用 kill -9 或系统 OOM Killer 终止进程 |
139 | (128 + 11) 进程因 SIGSEGV 崩溃 | 段错误(Segmentation Fault ),通常是非法内存访问(如空指针解引用、数组越界) |
143 | (128 + 15 ) 进程被 SIGTERM 终止 | 默认的 kill 命令发送的信号,请求进程正常退出 |
255 | 退出状态超出范围 (Exit status out of range ) | 程序返回了大于 255 的退出码(会被 Shell 截断为 255 ) |
2.3.1 exit VS _exit
exit
#include <stdlib.h>
[[noreturn]] void exit(int status);
_exit
#include <unistd.h>
[[noreturn]] void _exit(int status);
// 属于 POSIX 系统调用#include <stdlib.h>
[[noreturn]] void _Exit(int status);
// 是 C99 标准引入的函数(与 _exit 行为一致)// 注:[[noreturn]] 表示向编译器声明该函数 永远不会返回到调用点
在 Linux
环境下,exit()
和 _exit()
函数都用于终止进程,但它们在资源清理和缓冲区处理上有重要区别:
_exit()
系统调用(粗暴终止)- 定义:
#include <unistd.h>``void _exit(int status);
- 行为:
- 直接终止进程:立即结束调用进程
- 不清理缓冲区:不刷新标准
I
/O
缓冲区(如printf
的缓冲区) - 关闭文件描述符:关闭进程打开的所有文件描述符
- 通知父进程:向父进程发送
SIGCHLD
信号,并通过wait()
传递退出状态status
- 使用场景:
- 子进程退出时避免干扰父进程的
I
/O
缓冲区(尤其在fork()
后) - 需要立即终止进程且不进行任何清理时
- 子进程退出时避免干扰父进程的
- 定义:
示例:
#include <unistd.h>
#include <stdio.h>int main() {printf("Hello"); // 无换行符,数据在缓冲区_exit(0); // 直接退出,"Hello" 不会被输出!
}
执行结果
$ ./_exit
$ echo $?
0
exit()
库函数(安全终止)- 定义:
#include <stdlib.h>``void exit(int status);
- 行为:
- 调用退出处理函数:按注册的逆序执行
atexit()
或on_exit()
注册的函数 - 刷新所有
I
/O
缓冲区:清空stdio
缓冲区(如输出printf
的内容) - 关闭所有
I
/O
流:关闭通过fopen()
打开的文件流 - 删除临时文件:删除
tmpfile()
创建的临时文件 - 最终调用
_exit()
:执行系统调用_exit(status)
终止进程
- 调用退出处理函数:按注册的逆序执行
- 使用场景:
- 正常退出程序时(确保资源正确释放)
- 需要执行自定义清理逻辑(通过
atexit()
注册)
- 定义:
示例:
#include <stdlib.h>
#include <stdio.h>void cleanup() {printf("Cleanup done\n");
}int main() {atexit(cleanup); // 注册清理函数printf("Hello");exit(0); // 输出 "Hello" 和 "Cleanup done"
}
执行结果
$ ./exit
HelloCleanup done
特性 | _exit() | exit() |
---|---|---|
所属库 | 系统调用(Unix API ) | C 标准库函数 |
刷新 I /O 缓冲区 | ❌ 不刷新 | ✅ 刷新 |
执行退出处理函数 | ❌ 不执行 atexit() | ✅ 执行 |
关闭文件描述符 | ✅ 关闭 | ✅ 关闭(间接通过 _exit ) |
典型使用场景 | 子进程退出、紧急终止 | 正常程序退出 |
return退出是更常见的退出进程方法,执行return n等同于执行 exit(n),调用 main函数时会将main的返回值当作exit的参数
3. 进程等待
3.1 基本概念
进程等待是指一个正在运行的进程,由于某些原因(主要是需要外部资源或事件),暂时无法继续执行,主动或被动地让出 CPU
,并进入一种非运行状态(通常是阻塞状态),直到它所等待的条件得到满足为止
- 在进程概念中讲到,子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而导致内存泄漏
- 如果进程变成僵尸状态,哪怕
kill -9
也无能为力(wait
解决) - 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 进程等待的方法
3.2.1 wait
#include <sys/wait.h>pid_t wait(int *_Nullable wstatus);
功能- 等待任意一个子进程结束(无法指定具体子进程,按结束顺序处理)
参数
- 输出型参数,获取子进程退出状态,不关心可以设置为
NULL
返回值 - 等待成功返回等待进程的
pid
,失败返回-1
3.2.2 waitpid
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
功能 等待指定子进程结束,支持非阻塞模式(精确控制等待的子进程)
参数
-
pid
pid = -1
,等待任意一个子进程,等价于wait
pid > 0
,等待其进程与所设值相等的子进程
-
status
输出型参数WIFEXITED(status)
:查看进程是否是正常退出,正常退出为真WEXITSTATUS(status)
:查看进程的退出码
-
options
默认为0
,表示阻塞等待WNOHANG
:若pid
指定的子进程没有结束,则waitpid()
函数返回0,不予等待;若正常结束,则返回该子进程的PID
返回值
-
正常返回的时候返回收集到的子进程的
pid
-
如果设置了
WNOHANG
- 调用中
waitpid
发现没有已退出的子进程可收集,则返回0
- 如果调用出错,则返回
-1
,且errno
被设置为相应的值以示错误所在
- 调用中
-
如果子进程已经退出,调用
wait/waitpid
时,wait/waitpid
会立即返回,并且释放资源,获得子进程的退出信息 -
如果在任意时刻调用
wait/waitpid
,子进程存在且正常运行,则进程可能阻塞 -
如果子进程不存在,则立即出错并且返回
阻塞
非阻塞
3.2.3 获取子进程 status
wait
和waitpid
,都有一个status
,由操作系统填充- 如果传递
NULL
,表示不关心子进程的退出状态信息 status
不能简单的当作整型看待,以更细致的视角—比特看待,类似于位图,如下图(只研究低16
位)
3.2.4 阻塞等待 VS 非阻塞等待
阻塞等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t pid = fork();if (pid < 0){perror("Fork error");return 1;}else if (pid == 0){printf("I'm child process[%d]\n", getpid());printf("Sleeping\n");sleep(3);int *p = NULL;*p = 42; // 制造段错误(SIGSEGV)// exit(128);}int status = 0;int ret = waitpid(pid, &status, 0);if (ret == pid){printf("Wait sucess! Process[%d]\n", ret);// 检测是否正常退出if (WIFEXITED(status)){printf("Exited normally with status: %d\n", WEXITSTATUS(status));}// 检测是否被信号终止else if (WIFSIGNALED(status)){printf("Killed by signall: %d\n", WTERMSIG(status));}}else{perror("Wait error");}return 0;
}
运行结果
$ ./wait
I'm child process[1573142]
Sleeping
Wait sucess! Process[1573142]
Killed by signall: 11
$ echo $?
0 #父进程正常退出
非阻塞等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>typedef void (* task)(int);void PrintTask(int id)
{printf("Task: %d\n",id);sleep(1);
}
void CountTask(int num)
{printf("Count: %d\n",num);sleep(1);
}
void work(task t,int num)
{for(int i=0;i<num;i++){t(i+1);}
}int main()
{pid_t pid = fork();if (pid < 0){perror("Fork error");return 1;}else if (pid == 0){printf("I'm child process[%d]\n", getpid());printf("Sleeping\n");sleep(3);int *p = NULL;*p = 42; // 制造段错误(SIGSEGV)// exit(128);}int status = 0;int ret = 0;while(1){ret = waitpid(pid, &status, WNOHANG);if (ret < 0){perror("Fork error");break;}if(ret == pid){break;}printf("Process is working\n");// 父进程执行自己的代码work(PrintTask,2);work(CountTask,3);}if (ret == pid){printf("Wait sucess! Process[%d]\n", ret);// 检测是否正常退出if (WIFEXITED(status)){printf("Exited normally with status: %d\n", WEXITSTATUS(status));}// 检测是否被信号终止else if (WIFSIGNALED(status)){printf("Killed by signall: %d\n", WTERMSIG(status));}// ……}else{perror("Wait error");}return 0;
}
运行结果
$ ./_wait
Process is working
Task: 1
I'm child process[1575149]
Sleeping
Task: 2
Count: 1
Count: 2
Count: 3
Wait sucess! Process[1575149]
Killed by signall: 11
4. 进程程序替换
4.1 基本概念
进程替换是 Unix/Linux
系统中核心的进程管理技术,指在不创建新进程的情况下,将当前进程的代码和数据完全替换为另一个程序的技术。这是通过 exec
函数族实现的,常与 fork()
结合使用
fork
之后,父子各自执行父进程代码的一部分,但是如果子进程想要执行一个全新的程序呢?🤔
答案是程序替换
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中
4.2 替换原理
用 fork
创建子进程之后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec
函数以执行另一个程序,当进程调用一种 exec
函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动处开始执行。调用 exec
函数并不创建新进程,所以调用 exec
前后该进程的 id
并未改变
- 本质:将当前进程的内存映像(代码、数据、堆栈)替换为新程序的映像
- 进程ID不变:
PID
、PPID
、文件描述符等属性保留 - 执行流重置:从新程序的
main()
函数开始执行 - 永不返回:成功调用后不返回原程序(除非出错)
4.3替换函数
#include <unistd.h>extern char **environ;int execl(const char *pathname, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *pathname, const char *arg, ...);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
关键特性:替换后保留的资源
资源类型 | 是否保留 | 说明 |
---|---|---|
进程 ID (PID ) | ✔️ | 还是同一个进程 |
父进程 ID (PPID ) | ✔️ | 父子关系不变 |
打开的文件描述符 | ✔️ | 除非显式设置 FD_CLOEXEC |
工作目录 | ✔️ | 当前目录不变 |
信号处理设置 | ❌ | 重置为默认处理方式 |
内存锁 (mlock ) | ❌ | 自动释放 |
文件描述符保留示例:
int fd = open("log.txt", O_WRONLY); execl("/bin/ls", "ls", NULL); // ls 的输出会写入 log.txt!
l(list)
v(vector)
p(path)
e(env)
exec 函数族对比(6个变体)
函数 | 参数传递方式 | 环境变量处理 | PATH搜索 | 典型用例 |
---|---|---|---|---|
execl | 参数列表 | 继承当前环境 | ❌ | 固定路径程序 |
execv | 参数数组 | 继承当前环境 | ❌ | 动态构建参数 |
execle | 参数列表 | 自定义环境变量 | ❌ | 精确控制环境 |
execve | 参数数组 | 自定义环境变量 | ❌ | 系统编程首选(唯一系统调用) |
execlp | 参数列表 | 继承当前环境 | ✔️ | 执行 PATH 中的命令 |
execvp | 参数数组 | 继承当前环境 | ✔️ | Shell 命令实现核心 |
使用示例
#include<stdio.h>
#include<unistd.h>int main()
{char *const argv[]={"ls","-l","NULL"};char *const envp[]={"PATH=/bin:/usr/bin","TERM=console","NULL"};execl("/bin/ls","ls","-l",NULL);// 使用环境变量PATH,无需写全路径execlp("ls","ls","-l",NULL);perror("execl error");// 自定义环境变量execle("ls","ls","-l",NULL,envp);perror("execl error");// 使用数组传递参数execv("/bin/ls",argv);perror("execl error");// 使用环境变量PATH,无需写全路径execvp("ls",argv);perror("execl error");// 自定义环境变量 execve("/bin/ls",argv,envp);perror("execl error");return 0;
}
错误处理关键点
- 返回值检查:
if (execvp("unknown_cmd", args) == -1) {perror("execvp failed"); // 打印 "execvp failed: No such file or directory" }
- 错误类型:
ENOENT
:文件不存在EACCES
:权限不足ENOMEM
:内存不足E2BIG
:参数列表过长
进程替换法则:
- 需要保持进程上下文 → 用 exec
- 需要隔离执行环境 → 用 fork-exec
- 需要精确控制环境 → 用 execve
- 执行用户命令 → 用 execvp/execlp
5. 微型Shell
5.1 目标
- 能处理普通命令
- 能处理内建命令
- 将命令同本地变量、环境变量串联起来
- 通过自主实现的
Sehll
理解其运行原理
5.2 实现原理
比如下面的互动
$ ls
client.cpp makefile NamedPipe.hpp server.cpp
$ top
top - 23:35:10 up 34 days, 4:02, 3 users, load average: 1.01, 1.05, 1.01
Tasks: 150 total, 3 running, 147 sleeping, 0 stopped, 0 zombie
%Cpu(s): 19.0 us, 33.3 sy, 0.0 ni, 47.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 1775.4 total, 192.3 free, 1268.1 used, 471.6 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 507.3 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 227896 wyf 20 0 6380 1280 1152 R 100.0 0.1 4w+1d prcess_pool 1 root 20 0 22580 9636 5616 S 0.0 0.5 1:06.51 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.40 kthreadd 3 root 20 0 0 0 0 S 0.0 0.0 0:00.00 pool_workqueue_release 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/R-rcu_g ……
$ pwd
/home/wyf/code/
$
用下图中的时间轴来表示事件的发生次序,其中时间从左向右。shell
由表示为 bash
的方块表示,随着时间从左向右移动。shell
从用户读入字符串 ls
,随机建立新进程,在新进程中运行并等待其结束,随后继续读取新的输入,继续重复上述过程
进而可以将 shell
可以简化为下面的循环过程
- 获取命令行
- 解析命令行
- 建立新的子进程(
fork
) - 替换子进程(
execvp
) - 父进程等待子进程退出
这样就都和前面所介绍的内容串接起来了
5.3 实现源码
#include <stdio.h>
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
#include <sys/stat.h>
#include <fcntl.h>#define CMDLINESIZE 128
#define CMDSIZE 1024
#define ARGVSIZE 512
#define SPACE " "
#define ENVSIZE 1000
#define BUFFERSIZE 1024
#define NO_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3// 命令行参数表
int g_argc = 0;
char *g_argv[ARGVSIZE];// 环境变量表
char *g_env[ENVSIZE];
int g_envs = 0;
extern char **environ;// 重定向
int redir = 0;
std::string filename;// 退出状态
int g_status = 0;// 命令行提示符
char CmdLine[CMDLINESIZE];// 所输入命令行
char Cmd[CMDSIZE];// 更新环境变量所用
char buffer[BUFFERSIZE];
char tmp_buffer[BUFFERSIZE];bool InitEnv()
{g_envs = 0;memset(g_env, 0, sizeof(g_env));for (int i = 0; environ[i]; i++){g_env[i] = (char *)malloc(strlen(environ[i]) + 1);strcpy(g_env[i], environ[i]);g_envs++;}g_env[g_envs] = NULL;// g_env[1]=(char*)"MYTEST=SUCESS!!!";for (int i = 0; g_env[i]; i++){putenv(g_env[i]);}environ = g_env;return true;
}
const char *GetUsr()
{const char *usr = getenv("USER");return usr == NULL ? NULL : usr;
}
const char *GetHostName()
{static char hostname[1024];// 系统调用获取主机名gethostname(hostname, sizeof(hostname));return hostname == NULL ? NULL : hostname;
}
const char *GetPwd()
{// 获取当前路径---使用系统调用if (getcwd(tmp_buffer, sizeof(tmp_buffer))){if (strcmp(tmp_buffer, getenv("PWD")) != 0){snprintf(buffer, sizeof(buffer), "PWD=%s", tmp_buffer);putenv(buffer);}}const char *pwd = getenv("PWD");return pwd == NULL ? NULL : pwd;
}
char *GetHome()
{char *home = getenv("HOME");return home == NULL ? NULL : home;
}
bool MakCmdLine()
{std::string pwd = GetPwd();std::string pwd_last = pwd.substr(pwd.rfind("/") + 1, strlen(pwd.c_str()) - pwd.rfind("/"));snprintf(CmdLine, sizeof(CmdLine), "[%s@%s %s]-_-", GetUsr(), GetHostName(), pwd_last.c_str());return pwd_last.c_str() == NULL ? false : true;
}
bool PrintCommandLine()
{// 打印命令行提示符if (MakCmdLine()){printf("%s", CmdLine);fflush(stdout);return true;}return false;
}
bool GetCmd()
{// 获取输入的命令return fgets(Cmd, sizeof(Cmd), stdin) == NULL ? false : true;
}
void IsSpace(char c[], int *index)
{while (c[*index] != '\0' && isspace(c[*index])){++(*index);}
}// ls -a -l -n > log.txt
bool CheckRedir()
{redir = NO_REDIR;filename.clear();int start = 0;int end = strlen(Cmd) - 1;while (start <= end){if (Cmd[start] == '<'){redir = INPUT_REDIR;Cmd[start] = 0;start++;IsSpace(Cmd, &start);if (Cmd[start]){filename = Cmd + start;filename.erase(filename.find_last_not_of("\n\r\t") + 1);return true;}else{printf("No file name\n");redir = NO_REDIR;return false;}}else if (Cmd[start] == '>'){if (Cmd[start + 1] == '>'){redir = APPEND_REDIR;Cmd[start] = 0;Cmd[start + 1] = 0;start += 2;}else{redir = OUTPUT_REDIR;Cmd[start] = 0;start++;}IsSpace(Cmd, &start);if (Cmd[start]){filename = Cmd + start;filename.erase(filename.find_last_not_of("\n\r\t") + 1);return true;}else{printf("No file name\n");redir = NO_REDIR;return false;}}else{++start;}}return false;
}bool CmdSlice()
{// 处理输入的命令Cmd[strlen(Cmd) - 1] = 0;g_argc = 0;g_argv[g_argc] = strtok(Cmd, SPACE);while (g_argv[g_argc] != NULL){g_argv[++g_argc] = strtok(NULL, SPACE);}g_argv[g_argc] = NULL;return g_argc == 0 ? false : true;
}bool RedirOpen(int &fd)
{if (redir == INPUT_REDIR){fd = open(filename.c_str(), O_RDONLY);if (fd < 0){perror("open failed:");}dup2(fd, 0);return true;}else if (redir == OUTPUT_REDIR){fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 00666);if (fd < 0){perror("open failed:");}dup2(fd, 1);return true;}else if (redir == APPEND_REDIR){fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 00666);if (fd < 0){perror("open failed:");}dup2(fd, 1);return true;}else{return false;}return false;
}
bool RedirOpenAndRcover(int &fd, int &back_up)
{if (redir == INPUT_REDIR){fd = open(filename.c_str(), O_RDONLY);if (fd < 0){perror("open failed:");return false;}back_up = dup(0);dup2(fd, 0);}else if (redir == OUTPUT_REDIR){fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 00666);if (fd < 0){perror("open failed:");return false;}back_up = dup(1);dup2(fd, 1);}else if (redir == APPEND_REDIR){fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 00666);if (fd < 0){perror("open failed:");return false;}back_up = dup(1);dup2(fd, 1);}else{return false;}return true;
}bool Cd()
{if (g_argc == 1){if (chdir(GetHome()) != 0){perror("cd failed:");g_status = 1;return false;}return true;}else if (g_argc == 2){const char *target_path = g_argv[1];if (strcmp(target_path, "~") == 0){target_path = GetHome();}if (chdir(target_path) != 0){perror("cd failed:");g_status = 1;return false;}}else{g_status = 1;fprintf(stderr, "cd too many arguments\n");return false;}if (getcwd(tmp_buffer, sizeof(tmp_buffer))){snprintf(buffer, sizeof(buffer), "PWD=%s", tmp_buffer);putenv(buffer);}return true;
}bool Echo()
{if (g_argc == 1){printf("\n");return true;}if (g_argc == 2){std::string echo = g_argv[1];if (echo[0] == '$'){if (echo[1] == '?'){printf("%d\n", g_status);g_status = 0;return true;}std::string what = echo.substr(1, echo.size() - 1);for (int i = 0; environ[i]; i++){std::string tmp = environ[i];std::string prefix = tmp.substr(0, tmp.find("="));if (strcmp(prefix.c_str(), what.c_str()) == 0){printf("%s\n", getenv(what.c_str()));return true;break;}}g_status = 1;return true;}printf("%s\n", g_argv[1]);return true;}g_status = 1;return false;
}bool ChecKAndExecuteBulitin()
{std::string cmd = g_argv[0];int fd = -1, back_up = -1;bool redir_sucess = RedirOpenAndRcover(fd, back_up);bool is_builtin = false;if (cmd == "cd"){is_builtin = Cd();}else if (cmd == "echo"){is_builtin = Echo();}if (redir_sucess){fflush(stdin);fflush(stdout);close(fd);// if(redir==INPUT_REDIR)//{// dup2(back_up,0);// }// else if(redir==OUTPUT_REDIR || redir== APPEND_REDIR)//{// dup2(back_up,1);// }dup2(back_up, redir == INPUT_REDIR ? 0 : 1);close(back_up);}if (is_builtin)return true;return false;
}bool Execution()
{// 执行命令int id = fork();if (id == 0){int fd = -1;if (RedirOpen(fd))close(fd);execvp(g_argv[0], g_argv);perror("execvpe failed:");g_status = 1;exit(1);}int status = 0;int wait = waitpid(id, &status, 0);if (wait == -1){perror("wait failed:");}if (WIFEXITED(status)){g_status = WEXITSTATUS(status);}if (WIFSIGNALED(status)){g_status = WTERMSIG(status);}return true;
}void test()
{for (int i = 0; g_argv[i]; i++){printf("g_argv[%d]:%s\n", i, g_argv[i]);}printf("g_argc:%d\n", g_argc);
}int main()
{InitEnv();while (true){// 输出命令行提示符PrintCommandLine();// 获取输入命令if (!GetCmd())continue;// 检查重定向if (CheckRedir());// 处理输入命令if (!CmdSlice())continue;// 检查并执行内建命令if (ChecKAndExecuteBulitin())continue;// 执行普通命令if (Execution())continue;}return 0;
}
效果展示
wyf@hcss-ecs-0be3:~/code/myshell$ ./myshell
[wyf@hcss-ecs-0be3 myshell]-_-ls
makefile myshell myshell.cpp myshell.o
[wyf@hcss-ecs-0be3 myshell]-_-ls -lin
total 260
804103 -rw-rw-r-- 1 1000 1000 362 Jul 14 23:21 makefile
807122 -rwxrwxr-x 1 1000 1000 105240 Jul 15 00:04 myshell
804105 -rw-rw-r-- 1 1000 1000 10050 Jul 14 23:32 myshell.cpp
807121 -rw-rw-r-- 1 1000 1000 142024 Jul 15 00:04 myshell.o
[wyf@hcss-ecs-0be3 myshell]-_-top
top - 00:04:24 up 34 days, 4:32, 3 users, load average: 1.00, 1.00, 1.00
Tasks: 150 total, 2 running, 148 sleeping, 0 stopped, 0 zombie
%Cpu(s): 20.0 us, 35.0 sy, 0.0 ni, 45.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 1775.4 total, 163.1 free, 1284.2 used, 484.4 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 491.2 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 227896 wyf 20 0 6380 1280 1152 R 100.0 0.1 4w+1d prcess_pool
1605632 wyf 20 0 1158712 77376 46592 S 9.1 4.3 0:04.80 node 1 root 20 0 22580 9636 5616 S 0.0 0.5 1:06.56 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.40 kthreadd 3 root 20 0 0 0 0 S 0.0 0.0 0:00.00 pool_workqueue_release 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/R-rcu_g ……
[wyf@hcss-ecs-0be3 myshell]-_-pwd
/home/wyf/code/myshell
[wyf@hcss-ecs-0be3 myshell]-_-cd ..
[wyf@hcss-ecs-0be3 code]-_-cd ..
[wyf@hcss-ecs-0be3 wyf]-_-pwd
/home/wyf
[wyf@hcss-ecs-0be3 wyf]-_-echo "Hello"
"Hello"
[wyf@hcss-ecs-0be3 wyf]-_-^C130 wyf@hcss-ecs-0be3:~/code/myshell$