进程的通信方式
进程的通信方式有很多种,今天我就为大家介绍各种通讯方式,例如:管道,信号,消息队列,共享内存,信号量
1.管道
1.1 管道的简介:
管道分为无名管道与有名管道
-
无名管道:无名管道用于父子进程之间通讯
-
有名管道:有名管道用于任意进程之间的通讯
1.2 管道的本质:
管道的本质是在内存建立一段缓冲区,由操作系统内核来负责创建与管理。具体通讯模型如下:
1.3 无名管道(详细介绍):
1.3.1有关无名管道的详情:
无名管道的特点:
- 无名管道属于单向通讯
- 无名管道只能用于父子进程通讯
- 无名管道发送端叫做写端,接收端叫做读端
- 无名管道将读端与写端抽象成两个文件进行操作,在无名管道创建成功之后,则会返回将读端与写端的文件描述符存入数组
1.3.2 无名管道的创建函数:
1.功能
管道创建之后,内核会将文件描述符存储到数函数数组
2.头文件
#include <unistd.h>
3.函数原型
int pipe(int pipefd[2]);
4.函数参数
pipefd:用于存储无名管道读端与写端的文件描述符的数组
pipefd[0]:读端文件描述符
pipefd[1]:写端文件描述符**
5.函数返回值:
成功:0
失败:-1,设置 errno
示例代码(cpp代码实现):
sing namespace std;
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
int main(){
int pipe_value[2];//0编号代表读端文件描述符号,1编号代表写端文件描述符号
int ret = pipe(pipe_value);
if(ret == -1){
cout << "无名管道创建失败" << endl;
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if(pid == -1){
cout << "创建失败" << endl;
exit(EXIT_FAILURE);
}else if(pid == 0){
sleep(2);//休眠两秒,以防子进程先抢到
close(pipe_value[1]);//当读取时,要关闭写端
char buff1[120] = { 0 };
ssize_t rbytes = read(pipe_value[0],buff1,sizeof(buff1));
if(rbytes == -1){
cout << "pipe failure" << endl;
close(pipe_value[0]);
exit(EXIT_FAILURE);
}
cout << "输入成功,你要接收字符串是:";
printf("%s\n",buff1);
close(pipe_value[0]);
exit(EXIT_FAILURE);
}else if(pid > 0){
close(pipe_value[0]);//当写入时,要关闭读端
string temp;
cout << "请输入你要写入的字符串:";
char buff[128] = { 0 };
fgets(buff,sizeof(buff) - 1,stdin);
buff[strlen(buff)] = '\0';
ssize_t wbytes = write(pipe_value[1],buff,sizeof(buff));
if(wbytes == -1){
cout << "写入失败" << endl;
close(pipe_value[1]);
exit(EXIT_FAILURE);
}
waitpid(-1,NULL,0);
close(pipe_value[1]);
}
return 0;
}
题目答案:
上面代码的模型图:
1.4 有名管道(详情介绍):
1.4.1有名管道:
有名管道是文件系统中可见的文件,但是不占用磁盘空间,仍然在内存中。可以通过 mkfifo命令创建有名管道(在共享目录不能使用mkfifo)
有名管道与无名管道一样,在应用层是基于文件接口进行操作
有名管道用于任意进程之间的通讯,当管道为空时,读进程会阻塞。
注意,此时管道应该类似于文件.
1.4.2有名管道的创建:
创建函数:
1.函数头文件
#include <sys/types.h>
#include <sys/stat.h>
2.函数原型:
int mkfifo(const char *pathname, mode_t mode);
3.函数参数:
pathname:有名管道路径名
mode:有名管道文件访问权限, 常用0644
4.函数返回值:
成功:返回0
失败:返回-1,并设置errno
示例:
write.cpp :
using namespace std;
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#define PATH "/home/linuxfu/pipe"
int main(){
int ret = access(PATH,F_OK);
if(ret == -1){
mkfifo(PATH,0644);
}
int fd = open(PATH,O_WRONLY);
if(fd == -1){
cout << "open failed" << endl;
exit(EXIT_FAILURE);
}
char buf[128] = { 0 };
cout << "请输入你想要输入的字符串:";
fgets(buf,sizeof(buf) - 1,stdin);
buf[strlen(buf) - 1] = '\0';
ssize_t wtypes = write(fd,buf,size(buf));
if(wtypes == -1){
cout << "write failed" << endl;
close(fd);
exit(EXIT_FAILURE);
}
return 0;
}
read.cpp:
using namespace std;
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#define PATH "/home/linuxfu/pipe"
int main(){
int ret = access(PATH,F_OK);
if(ret == -1){
mkfifo(PATH,0644);
}
int fd = open(PATH,O_RDONLY);
if(fd == -1){
cout << "open failed" << endl;
exit(EXIT_FAILURE);
}
char buf[128] = { 0 };
ssize_t rtypes = read(fd,buf,size(buf));
if(rtypes == -1){
cout << "read failed" << endl;
close(fd);
exit(EXIT_FAILURE);
}
cout << "读取成功,字符串为:";
printf("%s\n",buf);
return 0;
}
- 如果有名管道的一端以只读方式打开,它会阻塞到另一端以写的方式(只写,读写)
- 如果有名管道的一端以只写方式打开,它会阻塞到另一端以读的方式(只读,读写)
2.信号
2.1 信号的定义:
信号是在软件层面上是一种通知机制,对中断机制的一种模拟,是一种异步通信方式。一般具有如下特点:
- 进程在运行过程中,随时可能被各种信号打断
- 进程可以忽略或者去调用相应的函数去处理信号
- 进程无法预测信号到达的精准时间
2.2 信号的来源:
在linux中信号一般来源如下:
- 程序执行错误,如内存访问越界,数学运算除0
- 由其他进程发送
- 通过控制终端发送,如ctrl + c
- 子进程结束时向父进程发送的SIGCHLD信号
- 程序中设定的定时器产生的SIGALRM信号
2.3信号的常用种类:
1.SIGINT 该信号在用户键入INTR字符(通常是Ctrl-C)时发出,终端驱动程序发送此信号并送到前台进
程中的每一个进程。
2.SIGQUIT 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-)来控制。
3.SIGILL 该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出。
4.SIGFPE 该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0
等其它所有的算术的错误。
5.SIGKILL 该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略。//非常常见的信号,常用来杀死进程
6.SIGALRM 该信号当一个定时器到时的时候发出。
7.SIGSTOP 该信号用于暂停一个进程,且不能被阻塞、处理或忽略。//这个也是常用信号,用来暂停信号
8.SIGTSTP 该信号用于交互停止进程,用户可键入SUSP字符时(通常是Ctrl-Z)发出这个信号。
9.SIGCHLD 子进程改变状态时,父进程会收到这个信号
10.SIGABRT 进程异常中止
2.4信号的处理:
信号处理流程包含以下两个方面:
- 信号的发送 :可以由进程直接发送
- 信号投递与处理 : 由内核进行投递给具体的进程并处理
在 Linux 中对信号的处理方式如下:
- 忽略信号,即对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
- 捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。
- 执行缺省操作,Linux对每种信号都规定了默认操作
(注意!注意!注意!)信号处理方式用的最多的是缺省操作,和自定义信号处理函数,一定要记住!!!
这是信号处理流程图:
2.5 信号的发送
当由进程来发送信号时,则可以调用kill()函数与raise ()函数
2.5 .1 kill函数:
1.函数头文件
#include<sys/types.h>
#include<signal.h>
2.函数原型
int kill(pid_t pid,int sig);
3.函数功能
向指定的进程发送一个信号
4.函数参数
pid:进程的id
sig:信号的id
5.函数返回值
成功返回0;
失败返回-1;
2.5.2 raise函数
1.函数头文件
#include <sys/types.h>
#include <signal.h>
2.函数原型
int raise(int sig)
3.函数功能
send a signal to the caller
4.函数参数
sig:信号编号
5.函数返回值:
成功返回0
失败返回-1
2.5.3等待函数
- 在进程没有结束时,进程在任何时间点都可以接受到信号
- 需要阻塞等待信号时,则可以调用 pause() 函数
1.函数头文件
#include <unistd.h>
2.函数原型
int pause(void);
3.函数功能
阻塞进程,直到收到信号后唤醒
4.函数返回值
成功:返回 0
失败:返回 -1,并设置 errno
2.5.4 代码实现(cpp):
using namespace std;
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int main(){
pid_t son1 = fork();
if(son1 == -1){
cout << "son1 create failed" << endl;
exit(EXIT_FAILURE);
}else if(son1 == 0){
cout << "son1 start" << endl;
pause();//暂停,等到信号再开始
cout << "son1 end" << endl;
}else{
pid_t son2 = fork();
if(son2 == -1){
cout << "son2 create failed" << endl;
exit(EXIT_FAILURE);
}else if(son2 == 0){
cout << "son2 start" << endl;
pause();//暂停,等到信号再开始
cout << "son2 end" << endl;
}else{
sleep(2); //让主线程睡眠,让其他线程抢资源
int pid = 0;
int ret = kill(son1,SIGKILL);//给son1发送信号
if(ret == -1){
perror("ret");
exit(EXIT_FAILURE);
}
cout << "杀死" << son1 << "进程" << "成功" << endl;
int ret1 = kill(son2,SIGKILL);//给son2发送信号
if(ret1 == -1){
perror("ret1");
exit(EXIT_FAILURE);
}
cout << "杀死" << son2 << "进程" << "成功" << endl;
}
}
return 0;
}
运行图片:
2.6 自定义信号的处理
首先我们再复习一遍信号处理方式:
信号是由操作系统内核发送给指定进程,进程收到信号后则需要进行处理
处理信号有三种方式:
- 忽略 : 不进行处理
- 默认 : 按照信号的默认方式处理
- 用户自定义 : 通过用户实现自定义处理函数来处理,由内核来进行调用
每种信号都有相应的默认处理方式
每种信号都有默认处理方式:
1.进程退出
SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
2.进程忽略
SIGCHLD,SIGPWR,SIGURG,SIGWINCH
3.进程暂停
SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
!!!然后这里我们会着重学习自定义处理函数
1.实现用户自定义处理函数,必须按照下列格式:
typedef void (*sighandler_t)(int);
typedef void (*)(int) sighandler_t
2.设置信号处理处理方式
通过 signal 函数设置信号处理方式
函数头文件
#include <signal.h>
函数原型
sighandler_t signal(int signum, sighandler_t handler);
函数功能
设置信号的处理方式,如果是自定义处理方式,提供函数地址,注册到内核中
函数参数
signum:信号编号
handler:信号处理方式
SIG_IGN---->忽略信号
SIG_DFL---->按照默认方式处理
自定义处理函数的地址
函数返回值
成功:返回信号处理函数地址
失败:返回SIG_ERR,并设置errno
注意点:三种信号处理方式互斥,一般选择一种即可
此函数案列代码(cpp) :
using namespace std;
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
void sighandler_fun(int signum){
cout << signum << "信号处理自定义成功"<< endl;
}
int main(){
pid_t son1 = fork();
if(son1 == -1){
cout << "son1 create failed" << endl;
exit(EXIT_FAILURE);
}else if(son1 == 0){
cout << "son1 start" << endl;
sighandler_t ret = signal(SIGUSR1,SIG_DFL);//son1自定义默认信号
if(ret == SIG_ERR){
perror("ret");
exit(EXIT_FAILURE);
}
cout << "son1 end" << endl;
}else{
pid_t son2 = fork();
if(son2 == -1){
cout << "son2 create failed" << endl;
exit(EXIT_FAILURE);
}else if(son2 == 0){
cout << "son2 start" << endl;
sighandler_t ret1 = signal(SIGUSR2,sighandler_fun);//son2自定义信号
if(ret1 == SIG_ERR){
perror("ret1");
exit(EXIT_FAILURE);
}
cout << "son2 end" << endl;
}else{
int val = kill(son1,SIGUSR1);
if(val == -1){
perror("val");
exit(EXIT_FAILURE);
}
cout << son1 << "successful" << endl;
int val2 = kill(son2,SIGUSR2);//给son2发送自定义信号
if(val2 == -1){
perror("val2");
exit(EXIT_FAILURE);
}
cout << son2 << "successful" << endl;
int pid=0;
while((pid = waitpid(-1,NULL,0)) == 0);
}
}
return 0;
}
2.7补充:
2.7.1定时器信号
在Linux系统中提供了 alarm 函数,用于设置定时器
1.函数头文件
#include <unistd.h>
2.函数原型
unsigned int alarm(unsigned int seconds);
3.函数功能
设置定时器的秒数
4.函数参数
seconds:定时的时间秒数
5.函数返回值
返回上一次进程设置定时器剩余的秒数,如果进程上一次没有设置定时器,则返回0
tips
- 定时器的定时任务由内核完成的,alarm 函数负责设置定时时间,并告诉内核启动定时器
- 当定时时间超时后,内核会向进程发出 SIGALRM 信号
2.7.2 子进程退出信号
在使用 wait() 函数时,由于阻塞或者非阻塞都非常消耗资源。并且在阻塞情况下,父进程不能执行其他逻辑。
如何解决?
子进程退出是异步事件,可以利用在子进程退出时,会自动给父进程发送 SIGCHLD 信号