Linux|Linux之进程创建/进程终止/进程等待/程序替换

一、进程创建 在linux中系统给我们提供了一个调用接口,用于进程的创建:

pid_t fork(void) 返回值:可以通过返回值来区分父子进程,比如父进程的返回值是子进程的pid而子进程的返回值是0;

特点:通过复制子进程,创建一个子进程;代码共享,数据独有。
可以通过以下图示来进行理解
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

数据独有指的是:
当子进程要修改数据时,再给他开辟新的物理空间,这是一种写时拷贝技术
【Linux|Linux之进程创建/进程终止/进程等待/程序替换】写时拷贝技术:子进程创建出来后,各个虚拟地址字物理内存中的指向与父进程完全相同,这是为了提高子进程的创建效率(否则要为子进程开辟物理内存,拷贝数据过来,更新页表,整个过程比较慢,并且有可能子进程根本就不使用这些数据)
若某一块物理内存中数据即将发生改变(某个进程要对这块内存进行写操作),则给子进程相应数据开辟物理内存,拥有自己的数据。
扩展:与fork函数相似的还有一个
pid_t vfork(void) 也是用来创建子进程并阻塞父进程,特点是创建子进程会比较的快。 直到子进程exit退出或者程序替换之后,父进程才会运行 因为同时运行会造成调用栈混乱 vfork创建子进程效率比较高,因为vfrok创建子进程之后父子进程共用同一个虚拟地址空间

二、进程终止 – 进程的退出 (1)在main()函数中return退出 例如:
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

可以看到终端没有任何的信息打印,是因为进程已经退出。
也可以通过查看进程信息来查看:
#include #include int main() { sleep(20); //这里让他先睡上个20秒 return 0; printf("helloworld\n"); }

再在另一个终端查看该进程的信息:
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

可以看到进程在运行过了一会就退出了。
注意:return 退出只能在main() 函数中退出进程,而函数中的return只是函数的退出,而不是进程的退出。
(2)调用库函数 exit() 退出 这里可能会有疑惑,为什么有一个exit() 还有一个系统调用接口 _exit() 首先这里可以说明一下两者之间的关系:
库函数封装了系统调用接口 系统调用接口不太好用
这里使用手册 man 查看一下两者之间的关系:
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

这里详细介绍了 exit函数,这里左上角部分显示的是一个 exit(3)
这里是因为 如果我们直接 man exit 是不会出现这个结果的。
man 1 exit 查看exit命令的手册 man 2 exit 查看系统调用接口的手册 man 3 exit 查看库函数的手册

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

可以看到
函数:void exit(int status);
头文件:
返回值:无返回值
参数 :status 将status作为返回值返回给父进程
这里的返回值的作用会在后面的进程等待中的 wait 函数中介绍
exit() 例如:
#include #include void Test1() { printf("hello\n"); exit(); } int main() { Test1(); printf("helloworld\n"); }

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

exit与return 又有什么区别
return 只有在main函数中的时候才会退出进程,然而exit是在任意位置调用都会退出调用进程
而return在其他函数 中是退出函数 ,exit在任意位置都会退出进程 而不会再进行其他的进程的调度。
(3)调用系统调用接口 _exit() 头文件:
参数与返回值等与exit相同;
那么前面已经有了两种退出进程的方式了,而 _exit() 又与其他两种有何区别呢? 这里要说到 printf 输出时换不换行的问题。
printf 不换行会怎么样? 会不刷新缓冲区
看一个例子:
#include int main() { printf("helloworld"); return 0; }

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

再看 使用 _exit() 退出时:
#include #include int main() { printf("helloworld"); _exit(0); }

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

这里可以看到 当使用 _exit()退出时 并没有输出 helloworld 这是因为:
exit 以及 return 都会刷新缓冲区 但是_exit会直接释放资源,不刷新缓冲区
注:
以上几种进程退出方式,都属于进程的正常退出 — 但是会根据返回值向父进程表示为什么退出了
但是进程退出也有可能异常退出 --通常指程序崩溃 – 程序没有运行完毕突然因为某种错误退出,具体的会在下面的进程等待解释。
三、进程等待 父进程等待子进程的退出,获取子进程的返回值 ,释放子进程资源,避免产生僵尸进程
当父进程不等待子进程的退出,就会产生僵尸进程。注意只有当进程是正常退出才可以获取返回值,如果进程是异常退出的,那么这个返回值是没有意义的。
例如:
#include #include int main() { pid_t pid=fork(); if(pid==0) { printf("我先睡5秒钟,然后退出\n"); sleep(5); exit(0); } while(1) { printf("------父进程正在打游戏---------\n"); sleep(1); }return 0; }

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

使用 ps -aux | grep 进程名 查看该进程的详细信息
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

可以看到当 父进程不等待子进程的退出时,就会成为僵尸进程,至于僵尸进程的危害,就不再这里阐述了,因此引入了进程等待的概念,避免僵尸进程的产生。
进程等待的两个方法:
前提了解一下阻塞与非阻塞的概念:
阻塞:为了完成某个功能发起一个调用,若当前不具备完成功能的条件,则调用不返回一直等待
非阻塞:为了完成某个功能发起一个调用,若当前不具备完成功能的条件,则立即报错返回 非阻塞操作通常需要进行循环操作 是一个相对于阻塞较好的操作,对cpu利用更加的充分,因为不在一直等待了,但是必须循环进行操作
(1)pid_t wait(int *status); --阻塞等待任意一个子进程的退出,获取返回值 返回值: 成功返回被等待进程pid,失败返回-1。
参数: 传入一个int 空间的首地址 ,获取子进程的返回值,若父进程不关心则可以设置为NULL
例如:
#include #include int main() { pid_t pid=fork(); if(pid==0) { sleep(5); printf("休息一会子进程退出\n"); exit(0); } printf("等待子进程的退出\n"); wait(NULL); int i=0; for(i=0; i<5; i++) { sleep(1); printf("父进程正在运行\n"); } return 0; }

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

如上图所示,当父进程等待子进程退出之后,就不会产生僵尸进程。
(2)pid_t waitpid(pid_t pid, int *status, int options); 这两个函数意义一样不过只是参数不一样
参数:
1.pid: Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。
2.status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
3.options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该 子进程的ID
返回值 :若没有子进程退出,则返回0;若出错则返回-1;否则返回退出的子进程的pid
其实也就是说:wait(status)=waitpid(-1,status,0);
例如:
#include #include #include int main() { pid_t pid=fork(); if(pid ==0 ) { //表示为子进程 sleep(5); exit(0); //等待5秒钟后再退出 ,父进程什么都不做就会成为僵尸进程 } int ret; //这里还可以定义 status 进行一个传入一个int 空间的首地址 ,获取子进程的返回值 //例如 int sattus 但是不要定义一个 int *status 然后在传递参数时写成: // waitpid(pid,status,WHOHANG) 这样是违法的 :正确的应该是如下的格式: //int status; //waitpid(pid,&status,WHOHANG) while(ret=waitpid(pid,NULL,WNOHANG)==0) { printf("现在子进程还没有退出 --- 等一下在来看\n"); sleep(1); } while(1) { printf("------父进程正在干别的事情-----\n"); sleep(1); } return 0; }

wait 和waitpid并不只是处理刚退出的进程 ,而是对已经退出的进程进行处理(不管什么时候退出的 )并且每次只能处理一个退出的进程。
返回值的获取:子进程的退出返回值通过wait /waitpid函数的status返回给用户
注意:
int status ----status 这个变量有四个字节的空间 ,向函数传入地址
int *status — 定义了一个指针变量 ,有8个字节空间 ,存放的是另一块空间的地址
status 四个字节 但是只使用了两个字节的空间用来存放子进程的退出返回值 ,但是实际上只使用了一个字节的存储空间,也就意味值返回值最好不要大于255;
这一个字节的返回值,在status四个字节中存放的时候是存放在低16位中的高8位中
相当于真实的数据右移 8 位 & 0xff
例如:
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

异常退出: 如何判断子进程是否正常退出?
我们刚了解了,在status中只有一个字节用来保存正常退出的子进程的退出码,在8-16这个比特位之间存储,而在低八位中用来保存一些特殊的数据
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

core dump 核心转储–程序异常退出时,则保存程序的运行信息,便于事后调试,默认是关闭的。
信号:通知进程发生了某个事件,中断进程当前的操作,去处理这个事件。
而操作系统中的信号的体现实际是一个数字,某个数字表示某个异常事件,程序因为异常退出的事后,则会将这个异常退出事件的信号值放到低7位中
当异常退出时:获取低七位。异常退出型号值,通过是否为0判断是否进程正常退出
00000000 00000000 00000000 01111111 & status 得到低七位 也就是 0x7f & status
这是一个手动的方式判断其是否正常退出,但是也可以使用函数中的参数:
WIFEXITED(status) // 正常终止子进程返回的状态,则为真 例如: if((WIFEXITED(status)) { printf("正常退出的status为:\n",WEXITSTATUS(status)); //查看进程的退出码 }

程序替换: 在创建子进程的目的中我们知道,大致有两点
  1. 子进程可以帮父进程分担压力
  2. 子进程可以干其他的事情,不对父进程产生影响。
    这是两个基本的特点,我们也知道通过进程的返回值来进程父子进程的一个区分例如:
pid_t pid=fork(); if(pid==0) { //这是子进程 //干一些其他的事情 }

但是这种方式存在缺陷:
程序的耦合度非常的强,因为所有功能代码都是在当前程序中编写的 — 如果想要改变子进程的功能处理流程,需要修改 整个代码,然后再重新编译,导致整个程序变得非常大因此引入了程序替换的概念:
程序替换:替换一个进程正在调度的程序信息(进程是pcb, 负责调度管理一个程序的运行,运行的数据和代码都在内存中,将另一个程序加载到内存中,然后让原有的pcb不再调度原程序,而去调度新的程序)
程序替换图示:
本质来说就是替换一个pcb在内存中对应的代码和数据(加载另一个程序到内存中,然后更新页表信息,初始化虚拟地址空间)这个进程pcb将重新开始调度的程序运行
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

如何完成程序替换? exec函数族 --实现进程的替换 使用 man 3 exec 查看手册:
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

可以看到这里有很多的函数:
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 execev(const char *path,char *const argv[],char *const envp[]);

演示例如:
**int execl(const char path, const char arg, …);
参数介绍:
const char *path 带有路径的新程序的名称,就是使用这个程序替换进程正在调度运行的程序const char *arg 程序参数
而后面的省略号表示这是一个不定参的参数,可以将新程序的运行参数,通过不定参的形式传递进入新的程序,以NULL作为结尾
注意:程序替换之后,当前进程运行替换后的程序就会退出,并不会再回去运行原先的程序
这个程序主要演示将程序替换为 ls 程序,我们可以通过以下指令找到他的位置下面绿色的就是ls程序,我们通过程序替换ls来看一下程序替换效果:
Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

//演示程序替换 #include #include #include int main(int argc, char *argv[]) {printf("helloworld\n"); execl("/usr/bin/ls","ls","-l",NULL); return 0; }

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

通过这个演示 我们可以看到程序被替换为了 ls -l 这里是调用了 ls 指令 参数是 -l
execl("/usr/bin/ls",“ls”,"-l",NULL);
而这里的参数,可以通过一个小程序来来演示一下:
#include #include #include int main(int argc, char *argv[]) { //argc 这个main函数的第一个参数 -- 用于表示当前程序有多少个运行参数 //argv这个字符串指针数组用于存储当前程序的运行参数 int i=0; for(i=0; i

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

我们可以看到 这里 prcretest 程序的第一个参数 也就是 第0个参数是 ./prcretest 我们再以空格进行参数输入时 可以看到 程序的 若干个参数,例如 ls 指令 ls -a / ls -l 就是通过程序不同参数调用不同的功能。
以上是第一个函数:int execl(const char *path, const char *arg, …); 的使用方法
函数详细介绍: 1.
1.int execl(const char *path, const char *arg, ...); 4.int execv(const char *path, char *const argv[])

而这两个 第一个 和第二个的区别就是 第一个是不定参,而第二个是一个字符串数组,将通过以下代码进行演示:
通过 myprctest 程序 调用prcretest 程序 打印出其参数
//prcretest 程序 #include #include #include int main(int argc, char *argv[]) { //argc 这个main函数的第一个参数 -- 用于表示当前程序有多少个运行参数 //argv这个字符串指针数组用于存储当前程序的运行参数 int i=0; for(i=0; i

//myprctest 程序 #include #include #include int main(int argc, char *argv[]) { //传递一个字符串数组 表示参数 char *new_argv[]={"prcretest","-l","-a","-p",NULL}; execv("./prcretest",new_argv); return 0; }

Linux|Linux之进程创建/进程终止/进程等待/程序替换
文章图片

注意 :无论是不定参还是指定的字符串数组的最后一个元素必须是 NULL
函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 不是
execlp 列表
execle 列表 不是 不是,需要自己组装环境变量
execv 数组 不是
execvp 数组
execev 数组 不是 不是,需要自己组装环境变量
其中前五个 是库函数 而 最后一个系统调用接口

    推荐阅读