Linux 进程通信

进程状态与进程关系

进程状态

Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程;
运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;
不可中断睡眠状态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程系统调度的。
暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。

在这里插入图片描述

进程关系(方便同意管理多个进程)

进程间还存在着其它一些层次关系,譬如进程组和会话;所以,由此可知,进程间存在着多种不同的关系,主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。
1、无关系
两个进程间没有任何关系,相互独立。
2、父子进程关系
两个进程间构成父子进程关系,譬如一个进程 fork()创建出了另一个进程,那么这两个进程间就构成了父子进程关系,调用 fork()的进程称为父进程、而被 fork()创建出来的进程称为子进程;当然,如果“生父”先与子进程结束,那么 init 进程(“养父”)就会成为子进程的父进程,它们之间同样也是父子进程关系。
3、进程组
每个进程除了有一个进程 ID、父进程 ID 之外,还有一个进程组 ID,用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。
假设为了完成一个任务,需要并发运行 100个进程,但当处于某种场景时需要终止这 100 个进程,若没有进程组就需要一个一个去终止,这样非常麻烦且容易出现一些问题;有了进程组的概念之后,就可以将这 100 个进程设置为一个进程组,这些进程共享一个进程组 ID,这样一来,终止这 100 个进程只需要终止该进程组即可。

设置进程组

通过系统调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID
两个函数都用于获取进程组 ID,getpgrp()没有参数,返回值总是调用者进程对应的进程组 ID;而对于 getpgid()函数来说,可通过参数 pid 指定获取对应进程的进程组 ID,如果参数 pid 为 0 表示获取调用者进程的进程组 ID。

#include <unistd.h>
pid_t getpgid(pid_t pid);
pid_t getpgrp(void);

调用系统调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组
setpgid()函数将参数 pid 指定的进程的进程组 ID 设置为参数 pgid。如果这两个参数相等(pid==pgid),则由 pid 指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数 pid 等于 0,则使用调用者的进程 ID;另外,如果参数 pgid 等于 0,则创建一个新的进程组,由参数 pid 指定的进程作为进程组组长进程。

#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);

一个进程只能为它自己或它的子进程设置进程组 ID,在它的子进程调用 exec 函数后,它就不能更改该
子进程的进程组 ID 了。

创建子进程

在诸多的应用中,创建多个进程是任务分解时行之有效的方法某一网络服务器进程可在监听客户端请求的同时,为处理每一个请求事件而创建一个新的子进程
有两种方式,第一种复制父进程的大部分内容,第二种重新开始,不需要去继承父进程的数据段、堆、栈以及继承了父进程打开的文件描述符
fork()函数单独存在为第一种
使用fork()函数后子进程马上使用exec()变为第二种方式

第一种(需要继承)

一个现有的进程可以调用 fork()函数创建一个新的进程,调用 fork()函数的进程称为父进程
由 fork()函数创建出来的进程被称为子进程,每个进程都会从 fork()函数的返回处继续执行,
会导致调用 fork()返回两次值,可通过返回值来区分是子进程还是父进程。
子进程是父进程的一个副本,譬如子进程拷贝了父进程的数据段、堆、栈以及继承了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间
fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0

#include <unistd.h>
pid_t fork(void);
pid = fork();
父子进程文件共享

因为调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本
这也意味着父、子进程对应的文件描述符均指向相同的文件表
在这里插入图片描述
从上面的图知道,父子进程都有相同的文件描述符
一方更新了文件偏移量,另一方也会进行更新(相当于父子进程交替写文件,会进行往下更新,而不是覆盖)

监视子进程

父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等
所以需要父进程对子进程的监视

wait()函数(有些限制最好用waitpid)

wait()可以等待任意子进程终止,获得子进程终止状态信息
调用时和之前没有子进程终止,就会阻塞
调用之前有一个或多个子进程终止,调用wait()不会阻塞,除了返回终止信息,还会给子进程收尸,一次wait()只能处理一次

子进程的终止状态可以用宏来进行检查
WIFEXITED(status):如果子进程正常终止,则返回 true;
WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过WEXITSTATUS 宏转换;
WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

@description : 开始阻塞,等待一个子进程的终止
@param - int *status : 出参,填为NULL表示不关心子进程状态,出参可用其它函数判断子进程终止状态
@param - sig_t handler : 用户自定义,有信号就会调用
	1.用户自定义,有信号就会调用
	2.忽略:  SIG_IGN
	3.系统默认操作: SIG_DFL		
@return : pid_t  
	sucess: 返回子进程的pid号
	err   : -1

但是使用wait()会有一些限制
a.如果有多个子进程,只能按照顺序等待子进程完成,不能指定
b.子进程不终止,一直运行的话,父进程会一直阻塞

waitpid()
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options)

@description : wait()
@param - pid_t pid : 
						大于0,表示要等待的进程号
						等于0,等待父进程这个进程组的所有子进程
						小于-1,等待进程组标识符与 pid 绝对值相等的所有子进程
						等于-1,等待任意子进程

@param - int *status : 出参,填为NULL表示不关心子进程状态,出参可用其它函数判断子进程终止状态
@param - int options :options 是一个位掩码,可以包括 0 个或多个如下标志
						WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮询 poll
						WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
						WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
@return : pid_t  
	sucess: 返回子进程的pid号
	err   : -1

孤儿进程

父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”
在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程
某一子进程的父进程结束后,该子进程调用 getppid()将返回 1,init 进程变成了孤儿进程的“养父”

僵尸进程

进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用
wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。
如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并
自动调用 wait(),故而从系统中移除僵尸进程。
但是父进程有自己的事情做,不能总在wait()阻塞,或者轮询的使用waitpid()
这时候因为子进程的结束是异步的,所以使用信号机制来处理

SIGCHLD 信号

当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。
这时候我们可以用信号捕获它,再调用wait()处理
当调用信号处理函数时,会暂时将引发调用的信号添加到进程的信号掩码中(除非 sigaction()指定了 SA_NODEFER 标志),这样一来,当 SIGCHLD 信号处理函数正在为一个终止的子进程“收尸”时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号,结果是,父进程的 SIGCHLD 信号处理函数每次只调用一次 wait(),那么就会导致有些僵尸进程成为“漏网之鱼”。
对于上面的话的理解
因为父进程收到了SIGCHLD 信号,在使用信号处理函数的时候,会把这个信号放入信号掩码中,
如果再次收到 SIGCHLD 信号,会现阻塞(至于丢不丢弃,会不会和可靠信号与不可靠信号有关),
因为SIGCHLD是不可靠信号,个人觉得就算设置了信号掩码,也会被丢弃,如果被丢弃了
要等到信号函数处理完毕后,才能再次收到SIGCHLD信号执行函数处理,所以在处理的时候,给wait()函数进行一直轮询,直到没有子进程可以释放,才结束信号处理函数

第二种(不用继承)

vfork()快,但是可能有意想不到的bug!!!
因为fork()会复制父进程的数据段和堆栈段中的绝大部分内容,后面又不用了会降低效率
对于 子进程不用继承父进程的场景,就会使用 vfork() 随后子进程马上调用exec()
正式情况下
fork() 创建进程
exec() 独立出这个进程
_exit() 子进程失败或者结束退出这个进程
wait() 父进程给子进程收尸
exit() 父进程失败或者结束退出这个进程

fork()之后的竞争条件

调用 fork 之后,无法确认谁先被系统调用运行,如果对于特定的应用程序有执行要求
可以使用之前的信号阻塞和唤醒来解决

exec 库函数

虽然使用上面的exec()独立出这个进程很好,但是直接把子进程代码放入一个可执行文件会更加方便
exec库函数,使用这些函数可以直接在父进程中执行一个可执行文件,作为子进程运行

exec 族函数包括多个不同的函数,这些函数命名都以 exec 为前缀
包括:execl()、execlp()、execle()、execv()、execvp()、execvpe()

#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

// execl()和 execv(),用于执行一个可执行文件,不同的是第二个传参参数
//execl()
const char *path 表示执行文件的绝对路径和相对路径
execl() 传参const char *arg,为可变参数
执行方法   execl("./newApp", "./newApp", "Hello", "World", NULL);
//execv()
execv() 传参char *const argv[] 参数数组
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execv("./newApp", arg_arr);

//execlp()和 execvp()在之前的函数中多了一个p,p表示PATH,这两个函数只用提供可执行文件的名字
//自行去环境变量中寻找该执行文件运行,当然也支持直接写上绝对路径和相对路径

//execle()和 execvpe()这两个函数在命名上加了一个 e,表示environment
//指定新的环境变量给新程序
//execle 传参
char *env_arr[5] = {"NAME=app", "AGE=25","SEX=man", NULL};
execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);

// execvpe 传参
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
char *env_arr[5] = {"NAME=app", "AGE=25","SEX=man", NULL}
execvpe("./newApp", arg_arr, env_arr);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值