目录
🍔1.进程创建
在Linux中,第一个进程是由操作系统创建的,其它进程都是由第一个进程通过不断创建子进程分裂出来的。而创建子进程的函数就是fork();
🍉🍉1.1 fork概念
在⼀个进程中,可以⽤fork()创建⼀个⼦进程,当该⼦进程创建时,它从fork()指令的下⼀条开始执行与父进程相同的代码。
🍉🍉1.2 fork用法
1.2.1 fork函数用法演示
#include<stdio.h> #include<unistd.h> int main(){ pid_t p=fork(); if(p>0){ printf("I am parent,ret=%d\n",p); printf("I am parent,pid=%d\n",getpid()); } else if(p==0){ printf("I am child,ret=%d\n",p); printf("I am child,pid=%d\n",getpid()); }else{ printf("创建失败"); } return 0;
分析:fork函数返回值是pid_t类型的,实际上是一个整形。在执行fork后,创建成功会返回两个值,在父进程中返回子进程的进程ID,在子进程中,返回0。创建失败就返回-1.
结论:由于fork函数有两个不同的返回值,所以我们可以用fork函数返回值加条件语句来操作这个函数。
1.2.2 fork函数用途
守护进程:父进程创建出子进程后,让子进程执行相关的业务。父进程负责守护相关的子进程,若子进程崩溃,父进程并不会收到影响,同时父进程重新拉起一个子进程,让子进程继续提供服务。
🍉🍉1.3 fork的特性
fork()函数产⽣了⼀个和当前进程完全⼀样的新进程 ,并和当前进程⼀样从fork()函数⾥返回。
父子进程是独立运行的,相互不干扰。各自有各自的进程虚拟地址空间和页表,数据具有安全性。
父子进程是抢占式执行,谁先谁后本质上是操作系统调度调度决定的。
子程序拷贝了父进程的PCB,上下文信息和程序计数器是和父进程一样的,所以代码开始执行位置也和父进程是一样的。
在刚创建进程时,子进程完全复制父进程信息,甚至公用进程虚拟地址空间。当子进程发生改变时,才以写实拷贝重新拷贝一份,父子进程就有了各自的页表以及进程虚拟地址空间。
🍉🍉1.4fork内部原理
在使用fork函数创建子进程的原理就是子进程直接拷贝父进程的PCB(PCB在进程原理篇)
1.分配新的内存块和内核数据结构(_task_struct)给子进程
2.将父进程内容拷贝给子进程
3.添加子进程到系统进程列表中,添加至pcb的双向链表中。
4.fork返回,操作系统开始调度。
🥩2.进程终止
🍒🍒2.1进程终止概念
进程的生命周期的结束叫做进程终止,进程终止一般分为进程正常终止和进程异常终止。
🍒🍒2.2正常终止
进程正常终止在Linux中可以通过 echo $? 查看进程退出码
正常终止一般有这么几种方法
2.2.1 调用exit函数
exit函数是C语言标准库的函数,底层封装的还是操作系统提供的_exit函数接口
头文件:stdlib.h
函数定义:void exit(int status); status是退出码,调用时传进去
作用:谁调用终止谁
2.2.2 调用_exit函数
exit函数是系统调用函数,是操作系统内核暴露出来的供程序员终止进程的接口。
头文件:<unistd.h>
函数定义:void _exit(int status); status是退出码,调用时传进去
作用:谁调用终止谁
2.2.3 使用return语句终止进程
这里需要注意的是只有在main函数中的return 语句才可以结束进程,子函数中的return语句作用是退出子函数。
2.2.4 代码验证
#include<stdlib.h> #include<unistd.h> int main(){ while(1){ exit(0); //第一种 _exit(0); //第二种 } return 0; //第三种 }
结论:通过设置退出码,我们就可以知道程序是否正常退出。这解释了我们刚学C语言时写return 0。
🍒🍒2.3异常终止
代码异常终止,一般是访问空指针或者内存越界,我相信大家肯定各种花活让它异常终止。
注意:Linux下的ctrl+c也是代码异常终止
代码验证:
#include<stdlib.h> #include<unistd.h> int main(){ int a[10]={0}; int b= a[10000]; return 0; }
结论:此处演示的是越界访问。
🍒🍒 2.4 exit和_exit的区别
exit在底层实现比_exit多了两个步骤,即执行用户自定义清理函数,冲刷缓冲区。
exit函数的底层封装了_exit函数,所以exit = _exit + 清理函数 + 冲刷缓冲区
由于硬件之间读取速度不匹配,即cpu读写速度远大于io操作速度,所以c标准库引入了缓冲区。假如需要完成一次写操作,_exit函数没有缓冲区,一个字节一个字节进行IO操作,非常浪费时间。exit函数引入缓冲区,将一部分内容先写入缓冲区,等等到达一定量的时候再进行一次 IO操作,极大提高了效率。
🍞3.进程等待
🍅🍅3.1概念
我们在写进程方面的代码时,很容易出现僵尸进程(进程原理篇),即子进程先于父进程退出且父进程未回收子进程退出状态信息,僵尸进程使用kill -9指令不能终止,但是进程等待能够解决。
🍅🍅3.2wait函数
3.2.1函数定义
pid_t wait(int* status); 等待任意子进程
作用:等待退出的子进程,回收退出子进程的返回信息。
成功:返回被等待进程的PID
失败:返回-1;
参数:输出型参数,获取子进程退出状态,没这个需求则可以设置成NULL;
这个参数需要程序员自己定义,定义一个整型,将地址传入参数列表,调用wait函数时,回收信息被存入这个int中。
int status: int一共有四个字节,实际上只使用了int的最后两个字节,一个字节存的是退出码,另一个存的是coredump标志位和退出信号。
🥗正常退出情况下,退出码被设置,coredump标志位为0,不会设置退出信号
🌮异常退出时,退出码不会被设置,coredump标志位为1,退出信号被设置。
代码演示
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<wait.h> #include<stdlib.h> int main(){ pid_t pid=fork(); if(pid>0){ int status; printf("I am parent,my pid=%d\n",getpid()); int ret=wait(&status); printf("ret is %d\n",ret); if(ret>0 && ((status & 0x7f)==0 )){ //正常退出,求退出码和coredump printf("退出码是%d\n",(status>>8)&(0xff)); printf("coredump是%d\n",(status>>7)&(1)); } else if(ret>0){ //异常退出,求退出信号和coredump printf("退出信号是%d\n",(status & 0x7f)); printf("coredump是%d\n",(status>>7)&(1)); } } else if(pid==0){ printf("I am child,my pid=%d\n",getpid()); //exit(101); //正常退出 int* a=NULL; //异常退出 *a=20; } else{ return -1; } }
🍚正常退出情况下的status
分析:此处使用exit函数正常退出,设置退出码为101
🍤异常退出情况下的status
分析:次数解引用空指针,程序异常退出,未设置退出码,设置了退出信号11,core dump为0,是因为我们未设置打开coredump文件。
🍅🍅3.3waitpid函数
3.2.1函数定义
pid_t waitpid(pid_t pid,int* status,int options);
作用:等待某一个子进程,回收退出子进程的返回信息。
返回值:
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:pid为等待进程的进程号
status为输出型参数,和wait函数的参数作用一样,都是获取子进程退出状态,没这个需求则可以设置成NULL;
options有个属性为WOHANG,它可以设置为非阻塞调用,即在调用waitpid函数时,若子进程未退出,则主函数继续向下执行。
对比:
waitpid函数比wait函数参数更加丰富,这也就意味着waitpid函数的灵活度更高。waitpid函数的第一个参数pid设置为-1时,他的作用和wait函数作用一样,都是等待任意一个退出的进程。
🍅🍅3.4僵尸进程解决
在父进程中调用wait函数,可以阻塞主进程,等待回收子进程信息,也可以调用waitpid函数回收信息。
#include<stdio.h> #include<unistd.h> #include<wait.h> int main(){ pid_t pid=fork(); if(pid>0){ sleep(20); wait(NULL); while(1){ printf("I am parent,my pid=%d\n",getpid()); } } else if(pid==0){ printf("I am child,my pid=%d\n",getpid()); }else{ return -1; } }
结论:wait函数能够等待子进程,回收子进程消息,避免成为僵尸进程
🍗4.进程程序替换
🍋🍋4.1原理
父进程在创建出子程序后,子进程拷贝出一份父进程的PCB,若想让子进程执行其它的任务,就得替换掉子进程中的数据段和代码段。那么就让子进程调用程序替换的接口,从而让子进程执行不一样的代码。而这个接口,就是exec函数簇。
本质上进程程序替换是更新子进程的堆栈信息,替换掉数据段和代码段。
🍋🍋4.2exec函数簇
execl
int execl(const char* path,const char*arg,...); //可变参数列表
🌯参数:
path:带路径的可执行程序
arg:传递给可执行程序的命令行参数,第一个参数是可执行程序本身,最后一个参数是NULL,中间的参数用 , 隔开
🥩返回值:调用成功直接执行新的代码 不返回,失败则返回-1
excel("/usr/bin/ls","-l",NULL); //栗子
execlp
int execlp(const char* file,const char*arg,...); //可变参数列表
🌯参数:
file:可执行程序,可带路径,也可不带路径(需要在环境变量中)
arg:传递给可执行程序的命令行参数,第一个参数是可执行程序本身,最后一个参数是NULL,中间的参数用 , 隔开
🥩返回值:调用成功直接执行新的代码 不返回,失败则返回-1
excelp("ls","-l",NULL); //栗子
execle
int execle(const char* path,const char*arg,...,char* const envp[ ]); //可变参数列表
🌯参数:
path:带路径的可执行程序
arg:传递给可执行程序的命令行参数,第一个参数是可执行程序本身,最后一个参数是NULL,中间的参数用 , 隔开
envp:程序员传递环境变量给函数
🥩返回值:调用成功直接执行新的代码 不返回,失败则返回-1
excel("/usr/bin/ls","-l",NULL,envp[]); //栗子
execv
int execv(const char* path,const char*argv[ ]); //可变参数列表
🌯参数:
path:带路径的可执行程序
argv[]:传递给可执行程序的命令行参数,以指针数组的方式传递。第一个参数是可执行程序本身,最后一个参数是NULL,中间的参数用 , 隔开
🥩返回值:调用成功直接执行新的代码 不返回,失败则返回-1
excev("/usr/bin/ls",argv[]); //栗子
execvp
int execvp(const char* file,const char*arg[ ]); //可变参数列表
🌯参数:
path:可执行程序,可带路径,也可不带路径(需要在环境变量中)
argv[]:传递给可执行程序的命令行参数,以指针数组的方式传递。第一个参数是可执行程序本身,最后一个参数是NULL,中间的参数用 , 隔开
🥩返回值:调用成功直接执行新的代码 不返回,失败则返回-1
excvp("ls",argv[]); //栗子
execve
int exeve(const char* path,const char*argv[ ],char *const envp[],); //可变参数列表
🌯参数:
path:带路径的可执行程序
arg:传递给可执行程序的命令行参数,第一个参数是可执行程序本身,最后一个参数是NULL,中间的参数用 , 隔开
envp:需要程序员自己提供环境变量
🥩返回值:调用成功直接执行新的代码 不返回,失败则返回-1
excel("/usr/bin/ls",argv[],envp[]); //栗子
表格总结
🍋🍋4.3代码演示
#include<unistd.h> int main(){ execlp("ls","ls","-l","-r",NULL); printf("会不会打印我?") return 0; }
结论:我们将主程序替换成ls程序,且未执行exclp后边的代码。