文章目录
- 进程创建
- fork()
- 进程退出
- 进程退出场景
- 进程退出方法
- 退出码
- exit、_exit
- 进程等待
- 进程等待的方法
- wait
- waitpid
- 阻塞和非阻塞
- 进程替换
- 替换的原理
- 替换所用到的函数
- execl
- execlp
- execle
- 简易的shell
进程创建
fork()
fork函数在之前的文章中也已经提到过了。其主要作用是从已存在的进程中创建一个新的进程,也就是新建的进程为子进程,原进程为父进程
当一个进程调用fork函数后,内核会做几件事:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝给子进程
- 添加子进程到系统进程列表中
- fork返回后,开始调度器调度
下面来看看进程创建的一段简单代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t id = fork();
if(id == 0){
printf("I am child process, pid = %d, ppid = %d\n", getpid(), getppid());
}
printf("I am parent process, pid = %d, ppid = %d\n", getpid(), getppid());
return 0;
}
当其返回值为0时,说明创建出了子进程。
进程退出
进程退出场景
进程退出总共会有三种情况,也就是我们平常写代码执行的时候也是会遇到这三种情况:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码遇到异常终止执行
进程退出方法
对于进程退出而言,可以有两种方法退出。一种就是正常的程序运行完毕终止执行,另一种就是程序遇到异常信号终止运行。
那么现在有一个问题,我们平常写代码的时候为什么总是会带上一个 return 0 呢?这里就涉及到一个知识点—退出码
退出码
其实return 0这个0并没有什么特殊的意思,返回的是0就代表着程序执行正常退出,非0就是程序有错误,每一个非0的退出码都代表着不同的错误信息。可以通过程序看看
#include<stdio.h>
#include<string.h>
int main(){
for(int i = 0; i < 20; i++)
printf("%d: %s\n", i, strerror(i));
return 0;
}
exit、_exit
那么除了return可以返回退出码退出程序外,exit和**_exit**也是可以的。不过这两者之间还是会有所区别的。
- exit是库函数,_exit是系统调用
- exit会刷新缓冲区,_exit不会
进程等待
进程等待是非常重要的。之前在进程状态里面谈到了一种状态—僵尸状态。这种状态是非常危险的,会造成内存泄漏。并且一旦进程变成了僵尸状态,那么即使使用kill -9都无法将其杀死。
所以父进程想要获取子进程的任务完成的程度如何,就必须通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
进程等待有两种方法:1、阻塞等待;2、非阻塞等待。可以使用两个函数去实现:wait和waitpid
wait
wait等待成功会返回被等待的进程的pid,失败则返回-1,下面来一段代码感受一下
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(){
pid_t id = fork();
if(id == 0){
int cnt = 5;
while(cnt--){
printf("I am child process, pid = %d\n", getpid());
sleep(1);
}
exit(1);
}
pid_t ret = wait(NULL);
printf("%d\n", ret);
return 0;
}
可以看到进程最后打印出的是子进程的pid,说明等待成功了。
waitpid
waitpid 相对于 wait 来说能够获取的信息就更多了,可以获取子进程的退出码和子进程返回的状态。
如果子进程是正常终止,那么返回的状态为0,如果收到了异常信号终止则非0
但是这里还要注意的是,waitpid 返回的子进程的数据是有自己的存储方式的。例如 waitpid 返回了一个变量 status 那么这个变量的**高八位为退出状态,低八位为终止信号。
如果进程是被信号所杀,则退出状态就没有用到,终止信号根据实际。如果进程正常终止,则退出状态根据实际,终止信号为0.
所以当waitpid 返回了一个值,我们想要获取终止信号就得用这个值 & 0x7f;获取退出状态就得用这个变量 向右移动8位再 & 0xff
来一段代码感受一下
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t id = fork();
assert(id != -1);
if(id == 0){
int cnt = 5;
while(cnt){
printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(0);
}
int status = 0;
while(1){
pid_t ret = waitpid(id, &status, 0);
if(ret == 0){
//子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
//waitpid调用成功 && 子进程没有退出
printf("wait done, but child is running....., parent running other things\n");
}
else if(ret > 0){
//waitpid调用成功 && 子进程已退出
printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
break;
}
else{
//waitpid调用失败
printf("waitpid call failed\n");
break;
}
sleep(1);
}
return 0;
}
下面再来看看当子进程收到异常信号时退出 waitpid 返回的结果。现在假设一个野指针的情况
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t id = fork();
assert(id != -1);
if(id == 0){
int cnt = 5;
while(cnt){
printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
int* p;
*p = 100;
exit(0);
}
int status = 0;
while(1){
pid_t ret = waitpid(id, &status, 0);
if(ret == 0){
//子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
//waitpid调用成功 && 子进程没有退出
printf("wait done, but child is running....., parent running other things\n");
}
else if(ret > 0){
//waitpid调用成功 && 子进程已退出
printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
break;
}
else{
//waitpid调用失败
printf("waitpid call failed\n");
break;
}
sleep(1);
}
return 0;
}
可以看到,此时waitpid接收到了异常信号退出并返回异常信号的值,可以通过kill -9查看对应的异常信息
阻塞和非阻塞
上面提到了等待可以分为阻塞等待和非阻塞等待,那么这两种有什么区别呢。
通俗点理解
阻塞等待就是父进程在等待子进程退出时并不会再去做其他的事情
非阻塞等待则是父进程在等待时如果他检测到子进程还没有退出,那它就退出检测去做自己的事情。做完自己的事情后又会检测,直至检测到子进程退出
在 waitpid 中可以通过传入 WNOHANG 表示非阻塞等待,传入0则表示阻塞等待。具体来看一段代码感受一下
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
#define N 10
typedef void (*func_t)();//函数指针
func_t handlerTask[N];
void task1(){
printf("task1\n");
}
void task2(){
printf("task2\n");
}
void task3(){
printf("task3\n");
}
void loadTask(){
memset(handlerTask, 0, sizeof(handlerTask));
handlerTask[0] = task1;
handlerTask[1] = task2;
handlerTask[2] = task3;
}
int main(){
pid_t id = fork();
assert(id != -1);
if(id == 0){
int cnt = 5;
while(cnt){
printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(0);
}
loadTask();
int status = 0;
while(1){
pid_t ret = waitpid(id, &status, WNOHANG);//WNOHANG: 非阻塞-> 子进程没有退出,父进程检测时立即退出
if(ret == 0){
//子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
//waitpid调用成功 && 子进程没有退出
printf("wait done, but child is running....., parent running other things\n");
for(int i = 0; handlerTask[i] != NULL; i++)
handlerTask[i]();
}
else if(ret > 0){
//waitpid调用成功 && 子进程已退出
printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
break;
}
else{
//waitpid调用失败
printf("waitpid call failed\n");
break;
}
sleep(1);
}
return 0;
}
可以看到父进程在等待的同时还会去执行指定的任务。等父进程检测到子进程还没有退出时,它就会退出等待去做他的事情。
进程替换
替换的原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
简单理解:替换只是把代码和数据换掉而已,并没有换进程去执行。我们可以通过替换去用A程序执行B程序。
替换所用到的函数
一般来说 实现替换有六种函数选择
#include <unistd.h>`
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[]);
这六个函数各有不同的参数,因此实现的方法不同,但是都是为了替换。
execl
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(){
printf("process running ...\n");
//.c -> exe -> load -> process -> 运行 -> 执行所写代码
printf("process is running ....\n");
//load -> exe
//第一个参数是告诉系统要执行谁,第二个是要怎么执行
execl("/usr/bin/ls", "ls", NULL);// all exec* end of NULL
printf("process running done...\n");
return 0;
}
execl需要传入替换程序的地址。
替换完成后,当我们执行程序就会有替换程序的效果了。要注意:当execl调用成功后,之后的语句就不再执行了。还可以在execl里面传入选项,已完成更全面的功能实现
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);// all exec* end of NULL
execlp
execlp相较于execl是不需要传入地址的,它会自动在环境变量里找。只需要传入需要替换的程序即可
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(){
printf("process running ...\n");
//.c -> exe -> load -> process -> 运行 -> 执行所写代码
printf("process is running ....\n");
//load -> exe
execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);
printf("process running done...\n");
return 0;
}
execle
exec* 的函数不仅可以替换系统中的程序,也可以替换我们自己的程序。现在我写一个程序,程序的主要功能是打印环境变量。然后我再用另一个程序替换
mybin.c
#include<stdio.h>
#include<stdlib.h>
int main(){
printf("这是另一个C程序\n");
printf("PATH: %s\n", getenv("PATH"));
printf("PWD: %s\n", getenv("PWD"));
printf("MYENV: %s\n", getenv("MYENV"));
return 0;
}
myexec.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(){
printf("process running ...\n");
pid_t id = fork();
assert(id != -1);
if(id == 0){
sleep(1);
char* const envp_[] = {(char*)"MYENV=1234565", NULL};
execle("./mybin", "mybin", NULL, envp_);
exit(1);
}
return 0;
}
如果我们自定义了环境变量,那么系统本身的环境变量就不会点出来了。
剩下的几个函数都是一样的道理,这里就不说了,可以自行查文档实现
简易的shell
前面讲完了进程的控制,包括:创建、终止、等待、替换。那么结合这些知识我们就可以自己去写一个简单的shell外壳了。
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(wait)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#include<assert.h>
#define NUM 1024
#define OPT_NUM 100
char lineCommand[NUM];
char *myargv[OPT_NUM];//指针数组
int lastCode = 0;
int lastSig = 0;
int main(){
while(1){
//输出提示符
printf("用户名@主机名 当前路径# ");
fflush(stdout);
//获取输入,输入结束要有'\n'
char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
assert(s != NULL);
(void)s;
//清除最后一个'\n'
lineCommand[strlen(lineCommand) - 1] = 0;
//字符串切割
myargv[0] = strtok(lineCommand, " ");
int i = 1;
//将颜色选项放入ls命令中
if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
myargv[i++] = (char*)"--color=auto";
//没有子串的话,strtok返回NULL
while(myargv[i++] = strtok(NULL, " "))
;
//cd命令不会创建子进程,就让shell自己执行对应命令,执行系统接口
if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){
if(myargv[1] != NULL)
chdir(myargv[1]);
continue;
}
//执行命令
pid_t id = fork();
assert(id != -1);
if(id == 0){
execvp(myargv[0], myargv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
assert(ret > 0);
(void) ret;
lastCode = ((status>>8) & 0xFF);
lastSig = (status & 0x7F);
}
}
运行起来之后,虽然还有很多bug,还是能够执行一些简单的指令的。