目录
进程创建
fork函数
写时拷贝
进程终止
进程退出码
exit函数
_exit函数
return,exit _exit之间的区别和联系
进程等待
进程等待的必要性
获取子进程status
进程等待的方法
wait
waipid
多子进程创建理解
非阻塞轮询检测子进程
进程程序替换
替换原理
exec系列替换函数
命名理解
shell(linux指令解释器)简易版
进程创建
fork函数
我喜欢叫分叉函数,用处就是,在当前进程的基础下,创建一个子进程。
返回值
返回值有三种:0 -1 子进程的PID
返回值 == 0:此时说明当前进程是子进程。
返回值 == -1:说明子进程创建失败。
返回值 == 子进程的PID:说明此时是父进程(因为父进程要管理子进程的退出)。
内核中fork函数:
- 分配一块内存创建子进程PCB,将其放入管理队列中。
- 父进程的内容拷贝到子进程。
- 添加子进程到系统进程列表中。
- fork返回,开始执行代码。
我们发现在fork前的代码执行了一次,后面的代码子进程和父进程都执行了一次。
fork函数为什么能有两种返回值?
fork函数在内核中,给子进程分配空间并且管理起来。此时fork函数还没结束的。它还有一个return语句:
此时在返回时二者都要进行返回,所以这个返回值就可以有两个。 然后我们在PCB中是有保存PID和PPID的,此时就可以根据这个确认谁是子进程谁是父进程,此时就能控制返回值。
写时拷贝
顾名思义:只有写的时候才进行拷贝。
我们子进程和父进程是共享一块空间的,为什么不直接给子进程创建新的空间,如果父子进程只有读操作,重新开辟空间copy一份父进程的变量给子进程不是浪费吗?
所以只有子进程和父进程需要更改变量时,才需要把父进程的变量拷贝一个新的变量给到子进程。
什么时候,代码也会被替换?那就是程序替换的范畴了下面会说。
fork通常用于程序替换
要和 exec函数等程序替换函数配合使用。
可以把子进程的执行逻辑直接切换成别的程序。但是父进程依然可以对子进程进行管理(守护进程)。
进程终止
进程退出三种情况:
1.代码运行完毕,结果正确。
2.代码运行完毕,结果不正确。
3.代码异常终止(进程崩溃)。
进程退出码
我们先了解下:main函数到底是什么?
main函数在我们写程序中开来就是一切的起源。但是他是怎么被启动的。
VS2013中main函数被一个叫做__tmainCRTStartup的函数调用。而这个函数又被操作系统通过加载器调用。这就是说main函数也是被系统操作的。
通常我们main函数的return值是0,而这个0,其实就是一个退出码。
0退出码代表执行成功,0以外都代表了执行错误
我们知道C语言中有个函数可以获取错误代码的信息 strerror(数字)。
我们编写一个这样的代码
执行后
我们发现只有0是success,其他都是一些错误表示,所以退出码的选择。
可以用echo $? 查看上一次程序的退出码是什么。
exit函数
exit函数就是用来用来退出程序的。并且有三步:
1.执行用户定义的atexit清理函数。
2.关闭这个进程打开的所有流,所有的缓存数据都被写入。
3.调用_exit函数终止进程。
所有数据都会被写入相对的流重点,才会退出。
_exit函数
_exit函数就是 exit函数的退化版,因为exit函数已经包含了_exit。因为_exit不会做任何处理,直接关闭进程。
这个代码执行结果就是:没有任何输出,我们的printf是先写到对应屏幕的缓冲区,此时_exit在还没写入时就把程序关了,所以就没有任何输出结果。
return,exit _exit之间的区别和联系
return通常都只是返回,但是在main函数中返回即终止主进程,整个代码完结。exit和_exit就是在子函数中强制退出进程用的。
下图是exit和_exit的区别。
进程等待
进程等待的必要性
1.僵尸进程:如果父进程一直不读取子进程退出信息,子进程就变成僵尸进程了,内存泄漏
2.僵尸进程无法被主动删除。
3.对于进程来说,最关心自己的就是父进程,因为父进程需要知道子进程的任务完成情况。
4.父进程通过进程等待的方式,回收子进程资源,获取对应的退出信息。
获取子进程status
这是一个wait和waitpid中的一个参数,这个参数是输入输出型(传入后,在函数中被更改后传出)。如果这个参数给到NULL则表示不关心子进程退出情况。
status是一个整型变量,但是status其实作用就是一个类似位图的运用。
这里只研究低16位。后八位代表终止信号,即退出码。前7位代表的终止信号,即信号码,第八位的core dump标志。
正常来说退出后只有退出状态被设置,剩下的都没设置,而终止后core dump也会被设置。
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
通过位运算就能取出对应的码和信号值
进程等待的方法
wait
函数原型:pid_t wait(int * status);
作用:等待任意子进程
返回值:成功则返回对应的子进程pid,失败返回-1
参数:status,获取状态码。不关心就设置为NULL
waipid
函数原型:pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子进程或任意子进程。
返回值:
1、等待成功返回被等待进程的pid。
2、如果设置了选项WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
参数:
1、pid:待等待子进程的pid,若设置为-1,则等待任意子进程。
2、status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
3、options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid
WNOHANG:设置以后,本来就是子进程不退出,父进程就一直卡着不动,如果设置了子进程没返回,那就先返回0给父进程,此时给父进程在一个循环里持续获取子进程退出信息,再循环体里父进程就可以继续做自己的事情
多子进程创建理解
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t ids[10];for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){//childprintf("child process created successfully...PID:%d\n", getpid());sleep(3);exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标}//fatherids[i] = id;}for (int i = 0; i < 10; i++){int status = 0;pid_t ret = waitpid(ids[i], &status, 0);if (ret >= 0){//wait child successprintf("wiat child success..PID:%d\n", ids[i]);if (WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killedprintf("killed by signal %d\n", status & 0x7F);}}}return 0;
}
这个代码就是用来创建了十个子进程,然后把10个子进程的的pid放到数组里,之后再让父进程for循环等待子进程结束。
可以看到我们上面的进程创建出来好,父进程是一个个等待结束的。
那能不能让父进程在没子进程退出时在执行一套自己的逻辑呢?
非阻塞轮询检测子进程
这里就要用到之前说的WNOHANG:WAIT NO HANG,就是说等待不会被挂起的意思。
此时我们给waitpid函数加上WNOHANG。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0){//childint count = 3;while (count--){printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());sleep(3);}exit(0);}//fatherwhile (1){int status = 0;pid_t ret = waitpid(id, &status, WNOHANG);if (ret > 0){printf("wait child success...\n");printf("exit code:%d\n", WEXITSTATUS(status));break;}else if (ret == 0){printf("father do other things...\n");sleep(1);}else{printf("waitpid error...\n");break;}}return 0;
}
此时发现我们父进程除了等待外,还有其他的判断语句此时就是父进程在等待时能做其他事情的意思
进程程序替换
替换原理
本来子进程和父进程执行的是同一个代码,只是可以用if else 分支,但是怎么让子进程直接去执行另一个程序呢?
如上图,在我们创建好子进程后,将其与磁盘中的程序进行替换。
进程替换时,有没有新的进程创建
都叫进程替换了,那就说明了就是把子进程替换了而已,只是这个新的程序依然可以被父进程管理。pid,进程地址空间,页表都没变,只有代码和数据变了。
那会发生写时拷贝吗?
当然,这里的整个 子进程的代码数据都变了,所以和父进程就发生了写时拷贝,此时就不会影响父进程的数据了
exec系列替换函数
总共有6种exec系列函数
一、
int execl(const char *path, const char *arg, ...);
第一个参数是程序路径,第二个可变参数列表,表示的就是这个程序的选项,怎么执行这个程序。以NULL结尾
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
上述代码就是:ls程序的执行。
二、
int execlp(const char *file, const char *arg, ...);
第一个参数是要执行程序的名字,和环境变量搭配使用。 第二个和上面一样代表怎么执行这个程序
execlp("ls", "ls", "-a", "-i", "-l", NULL);
三、
int execle(const char *path, const char *arg, ..., char *const envp[]);
第一个参数是地址,第二个参数还是老样子,第三个参数是自己想设置的环境变量。
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
如上代码:执行后,在环境变量中就多了一项 MYVAL,此时这个VAL就能被直接使用。在一些程序中,可以配置一些环境变量,便于使用。
四、
int execv(const char *path, char *const argv[]);
第一个参数是要执行代码的地址,第二个就是一个字符指针(字符串)数组,只是把上面的在参数里传递,改编成了传递一个字符串数组。(一样要以NULL结束)
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
五、
int execvp(const char *file, char *const argv[]);
第一个参数是要执行的程序的名字,第二个参数还是执行程序的选项的字符串数组。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
六、
int execve(const char *path, char *const argv[], char *const envp[]);
第一个参数是路径,第二个参数是选项和执行内容,第三个参数是要设置的环境变量。
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);
上述全部函数成功调用,则直接执行新的程序,不再返回。
失败则返回-1.
命名理解
我们发现exec系列的函数,开头都是exec只是后面带的剩余字母不同:
l(list):表示参数采用列表的形式,可变参数列表。
v(vector):采用数组的形式。
p(path):表示自动搜索环境变量PATH,进行相应程序的查找。
e(env):表示可以传入自己设置的环境变量。
只有execve是系统调用,其它5个函数最终都是调用execve。
shell(linux指令解释器)简易版
这里制作的shell采用父子进程制作,即bash(shell)接收命令,然后创建子进程然后把子进程的逻辑替换为对应指令即可
#include <stdio.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 //命令最大长度
#define NUM 32 //命令拆分后的最大个数
int main()
{char cmd[LEN]; //存储命令char* myargv[NUM]; //存储命令拆分后的结果char hostname[32]; //主机名char pwd[128]; //当前目录while (1){//获取命令提示信息struct passwd* pass = getpwuid(getuid());gethostname(hostname, sizeof(hostname)-1);getcwd(pwd, sizeof(pwd)-1);int len = strlen(pwd);char* p = pwd + len - 1;while (*p != '/'){p--;}p++;//打印命令提示信息printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);//读取命令fgets(cmd, LEN, stdin);cmd[strlen(cmd) - 1] = '\0';//拆分命令myargv[0] = strtok(cmd, " ");int i = 1;while (myargv[i] = strtok(NULL, " ")){i++;}pid_t id = fork(); //创建子进程执行命令if (id == 0){//childexecvp(myargv[0], myargv); //child进行程序替换exit(1); //替换失败的退出码设置为1}//shellint status = 0;pid_t ret = waitpid(id, &status, 0); //shell等待child退出if (ret > 0){printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码}}return 0;
}