Linux 进程间通信
linux进程间通信
IPC:(Inter-Process Communication,进程间通信)
进程间通信的本质:让不同进程之间传播或交换信息。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。
进程间通信的目的:
- 数据传输: 一个进程需要将它的数据发送给另一个进程。
- 资源共享: 多个进程之间共享同样的资源。
- 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
- 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
现今常用的进程间通信方式有:
- 管道(使用最简单)
- 信号(开销最小)
- 消息队列
- 共享映射区(无血缘关系)
- 本地套接字(最稳定)
进程间通信的分类:
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
1)管道
管道概念:
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。
①匿名管道
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
pipe函数
pipe函数用于创建匿名管道,pip函数的函数原型如下:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
数组元素 | 含义 |
---|---|
pipefd[0] | 管道读端的文件描述符 |
pipefd[1] | 管道写端的文件描述符 |
pipe函数调用成功时返回0,调用失败时返回-1。
匿名管道使用步骤
1、父进程调用pipe函数创建管道
2、父进程创建子进程
3、父进程关闭写端,子进程关闭读端。
//child->write, father->read
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0){
//child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
//父进程从管道读取数据
char buff[64];
while (1){
ssize_t s = read(fd[0], buff, sizeof(buff));
if (s > 0){
buff[s] = '\0';
printf("child send to father:%s\n", buff);
}
else if (s == 0){
printf("read file end\n");
break;
}
else{
printf("read error\n");
break;
}
}
close(fd[0]); //父进程读取完毕,关闭文件
waitpid(id, NULL, 0);
return 0;
}
注意:
管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。
管道读写规则
pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
1、当没有数据可读时:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2、当管道满的时候:
O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
管道的特点:
1、管道内部自带同步与互斥机制。
2、管道的生命周期随进程。
3、管道提供的是流式服务。
4、管道是半双工通信的。
- 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
- 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
管道的四种特殊情况(读写行为)
1、写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
2、读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
3、写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
4、读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
②命名管道
命名管道可以实现两个毫不相关进程之间的通信
- 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
- 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
在centos中可以使用mkfifo
命名创建一个管道。
$ mkfifo fifo
在程序中创建命名管道:
int mkfifo(const char *pathname, mode_t mode);
命名管道和匿名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,由open函数打开。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。
2)信号
概念:
软中断信号又称信号,用来通知进程发生了事件。进程之间可以通过调用kill库函数发送软中断信号。
Linux内核也可能给进程发送信号,通知进程发生了某个事件(例如内存越界)。
每个进程收到的信号,都是由内核负责发送,内核处理
注意:信号只是用来通知某进程发生了什么事件,无法给进程传递大量数据
作用:
- 服务程序运行在后台,如果想让中止它,强行杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有释放资源,会影响系统的稳定,用Ctrl+c中止与杀程序是相同的效果。
- 如果能向后台程序发送一个信号,后台程序收到这个信号后,调用一个函数,在函数中编写释放资源的代码,程序就可以有计划的退出,安全而体面。
- 信号还可以用于网络服务程序抓包等。
产生信号:
- 按键产生,如:Ctr+c、Ctr+z、Ctr+\
- 系统调用产生:如:kill、raise、abort
- 软件条件产生:如:定时器alarm
- 硬件一场产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生:如:kill命令
递达:递达并且到达进程。
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态
进程对信号的处理方法有三种:
- 忽略信号(除SIGKILL,SIGSTOP信号)
- 执行系统的默认操作,大部分的信号的默认操作是终止进程。
- 调用相应的信号处理函数来捕获信号
Linux内核的进程控制块PCB是一个结构体,task_struct,除了包含id,状态,工作目录,用户id,组id,文件描述符表,还包含信号相关的信息,主要指阻塞信号集和未决信号集
阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
未决信号集:
1、信号产生,未决信号集中描述该信号的位立刻翻转未1,表示信号处于未决状态。当信号被处理对应位翻转回0。这一时刻往往非常短暂
2、信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除之前,信号一直处于未决状态
可靠信号和不可靠信号:
信号值 1 ~ 32 为不可靠信号 信号会丢失
信号值 34 ~ 64 为可靠信号 信号不会丢失
信号的类型:
信号名 | 信号值 | 默认处 理动作 | 发出信号的原因 |
---|---|---|---|
SIGHUP | 1 | A | 终端挂起或者控制进程终止 |
SIGINT | 2 | A | 键盘中断Ctrl+c |
SIGQUIT | 3 | C | 键盘的退出键被按下 |
SIGILL | 4 | C | 非法指令 |
SIGABRT | 6 | C | 由abort(3)发出的退出指令 |
SIGFPE | 8 | C | 浮点异常 |
SIGKILL | 9 | AEF | 采用kill -9 进程编号 强制杀死程序。 |
SIGSEGV | 11 | C | 无效的内存引用 |
SIGPIPE | 13 | A | 管道破裂:写一个没有读端口的管道 |
SIGALRM | 14 | A | 由alarm(2)发出的信号 |
SIGTERM | 15 | A | 采用“kill 进程编号”或“killall 程序名”通知程序。 |
SIGUSR1 | 30,10,16 | A | 用户自定义信号1 |
SIGUSR2 | 31,12,17 | A | 用户自定义信号2 |
SIGCHLD | 20,17,18 | B | 子进程结束信号 |
SIGCONT | 19,18,25 | 进程继续(曾被停止的进程) | |
SIGSTOP | 17,19,23 | DEF | 终止进程 |
SIGTSTP | 18,20,24 | D | 控制终端(tty)上按下停止键 |
SIGTTIN | 21,21,26 | D | 后台进程企图从控制终端读 |
SIGTTOU | 22,22,27 | D | 后台进程企图从控制终端写 |
处理动作一项中的字母含义如下
A 缺省的动作是终止进程。
B 缺省的动作是忽略此信号,将该信号丢弃,不做处理。
C 缺省的动作是终止进程并进行内核映像转储(core dump),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员 提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。
D 缺省的动作是停止进程,进入停止状况以后还能重新进行下去。
E 信号不能被捕获。
F 信号不能被忽略。
信号四要素:
- 编号
- 名称
- 事件
- 默认处理动作
查看:man 7 signal
①kill函数
系统调用kill( )用来向一个进程或者一个进程组发送一个信号,其中第一个参数决定信号发送的对象,声明如下:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
其中,pid可能的选择有以下4种:
-
当pid>0,pid是信号欲送往的进程的标识
-
当pid=0,信号将送往所有与调用kill()的那个进程属于同一个使用组的进程
-
当pid=-1,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)外
-
当pid<-1,信号将送往以-pid为组标识的进程
参数sig表示准备发送的信号代码,如果其值为0,则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为0来检验某个进程是否仍在执行。当函数执行成功,返回0,否则返回-1,此时error可以得到错误码,错误码值EINVAL表示指定的信号码无效(参数sig不合法),错误码值EPERM表示权限不够,无法传递信号给指定进程,错误码值ESRCH表示参数PID所指定的进程或进程组不存在。
②signal库函数
作用:可以设置程序对信号的处理方式。
函数声明:
sighandler_t signal(int signum, sighandler_t handler);
参数signum表示信号的编号。
参数handler表示信号的处理方式,有三种情况:
1)SIG_IGN:忽略参数signum所指的信号。
2)一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数。
3)SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
一般不关心signal的返回值。
应用实例
在实际开发中,在main函数开始的位置,程序员会先屏蔽掉全部的信号。
//屏蔽掉全部的信号
for (int ii=0;ii<100;ii++)
signal(ii,SIG_IGN);
- 这么做的目的是不希望程序被干扰。然后,再设置程序员关心的信号的处理函数。
- 程序员关心的信号有三个:SIGINT、SIGTERM和SIGKILL。
- 程序在运行的进程中,如果按Ctrl+c,将向程序发出SIGINT信号,信号编号是2。
- 采用“kill 进程编号”或“killall 程序名”向程序发出的是SIGTERM信号,编号是15。
- 采用“kill -9 进程编号”向程序发出的是SIGKILL信号,编号是9,此信号不能被忽略,也无法捕获,程序将突然死亡。
- 所以,程序员只要设置SIGINT和SIGTERM两个信号的处理函数就可以了,这两个信号可以使用同一个处理函数,函数的代码是释放资源。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void EXIT(int sig)
{
printf("收到了信号%d,程序退出。\n",sig);
// 在这里添加释放资源的代码
exit(0); // 程序退出。
}
int main()
{
// 屏蔽全部的信号
for (int ii=0;ii<100;ii++)
signal(ii,SIG_IGN);
// 设置SIGINT和SIGTERM的处理函数
// 第一个参数也可以用对应的信号值表示 例如signal(2,EXIT);
signal(SIGINT,EXIT);
signal(SIGTERM,EXIT);
while (1) // 一个死循环
{
sleep(10);
}
}
当一个信号到达后,调用处理函数,如果这时候有其他的信号发生,会中断之前的处理函数,等新的信号处理函数执行完毕后在继续执行之前的处理函数
如果是同一个信号的话会排队阻塞
信号的阻塞
信号的阻塞和忽略信号是不同的,被阻塞的信号也不会影响进程的行为,信号只是暂时被阻止传递
进程忽略一个信号时,信号会被传递出去,但进程会将信号丢失
③ sigaction函数
功能更强大的sigaction 函数也可以实现信号阻塞的功能
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
void hdfunc(int sig)
{
printf("sig=%d\n",sig);
for (int jj=0;jj<5;jj++) {
printf("jj(%d)=%d\n",sig,jj);
sleep(1);
}
}
int main()
{
// signal (2,hdfunc);
// signal (15,hdfunc);
struct sigaction stact;
memset(&stact,0,sizeof(stact)); //初始化
stact.sa_handler = hdfunc; //指定信号处理函数
sigaddset (&stact.sa_mask,2); //指定需要阻塞的信号
sigaddset (&stact.sa_mask,15); //指定需要阻塞的信号
stact.sa_flags=SA_RESTART; //如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
sigaction (2,&stact,NULL); //设置信号2的处理行为
sigaction (15,&stact,NULL); //设置信号15的处理行为
char str[5];
memset (str,0,sizeof(str));
scanf("%s",str);
printf("str=%s\n",str);
/*
for (int ii=1;ii<100;ii++) {
printf("ii=%d\n",ii);
sleep(1);
}
*/
return 0;
}
程序开始在等待用户的输入,这个时候同时给程序发送了两个信号,最后输入字符串,程序结束。
killall -15 sigaction
killall -2 sigaction
运行效果会没有出现信号中断的情况。
3)消息队列
概念:
消息队列是消息的链表,存放在内核中并由消息队列标识符表示。
作用:
消息队列提供了一个从一个进程向另一个不相关进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型。
与有名管道相比,消息队列的优点在于独立发送与接收进程,这减少了在打开与关闭有名管道之间同步的困难
消息队列函数:
#include<sys/msg.h>
#include<sys/types.h>
#include<sys/ipc.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
int msgget(key_t key, int msgflg);
int msgrcv(int maqid, void *msg_ptr, size_t, msg_sz, long int msgtype, int msgflg);
int msgsnd(int msqid, const void *msg)ptr, size_t msg_sz, int msqflg;
①msgget函数
功能:创建和访问一个消息队列
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
参数:
key:某个消息队列的名字,用ftok()产生
msgflag:有两个选项IPC_CREAT和IPC_EXCL,单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。
返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
调用成功返回一个key值,用于创建消息队列,如果失败,返回-1
②msgctl函数
功能:消息队列的控制函数
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
msqid:由msgget函数返回的消息队列标识码
cmd:有三个可选的值,在此我们使用IPC_RMID
IPC_STAT 把msqid_ds结构中的数据设置为消息队列的当前关联值
IPC_SET 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的值
IPC_RMID 删除消息队列
返回值:成功返回0,失败返回-1
③msgsnd函数
功能:把一条消息添加到消息队列中
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msgid:由msgget函数返回的消息队列标识码
msgp:指针指向准备发送的消息
msgze:msgp指向的消息的长度(不包括消息类型的long int长整型)
msgflg:默认为0
返回值:成功返回0,失败返回-1
消息结构一方面必须小于系统规定的上限,另一方面必须以一个long int长整型开始,接受者以此来确定消息的类型
struct msgbuf
{
long mtye;
char mtext[1];
};
④msgrcv
功能:是从一个消息队列接受消息
原型:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:与msgsnd相同
返回值:成功返回实际放到接收缓冲区里去的字符个数,失败返回-1
此外,我们还需要学习两个重要的命令
前面我们说过,消息队列需要手动删除IPC资源
ipcs:显示IPC资源
ipcrm:手动删除IPC资源
这里写图片描述
代码模拟实现client与server之间的通信
comm.h
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
struct msgbuf
{
long mtype;
char mtext[1024];
};
#define SERVER_TYPE 1
#define CLIENT_TYPE 2
int createMsgQueue();
int getMsgQueue();
int destoryMsgQueue(int msg_id);
int sendMsgQueue(int msg_id, int who, char* msg);
int recvMsgQueue(int msg_id, int recvType, char out[]);
#endif
comm.c
#include "comm.h"
static int commMsgQueue(int flags)
{
key_t key = ftok("/tmp", 0x6666);
if(key < 0)
{
perror("ftok");
return -1;
}
int msg_id = msgget(key, flags);
if(msg_id < 0)
{
perror("msgget");
}
return msg_id;
}
int createMsgQueue()
{
return commMsgQueue(IPC_CREAT|IPC_EXCL|0666);
}
int getMsgQueue()
{
return commMsgQueue(IPC_CREAT);
}
int destoryMsgQueue(int msg_id)
{
if(msgctl(msg_id, IPC_RMID, NULL) < 0)
{
perror("msgctl");
return -1;
}
return 0;
}
int sendMsgQueue(int msg_id, int who, char* msg)
{
struct msgbuf buf;
buf.mtype = who;
strcpy(buf.mtext, msg);
if(msgsnd(msg_id, (void*)&buf, sizeof(buf.mtext), 0) < 0)
{
perror("msgsnd");
return -1;
}
return 0;
}
int recvMsgQueue(int msg_id, int recvType, char out[])
{
struct msgbuf buf;
int size=sizeof(buf.mtext);
if(msgrcv(msg_id, (void*)&buf, size, recvType, 0) < 0)
{
perror("msgrcv");
return -1;
}
strncpy(out, buf.mtext, size);
out[size] = 0;
return 0;
}
server.c
#include "comm.h"
int main()
{
int msgid = createMsgQueue();
char buf[1024] = {0};
while(1)
{
recvMsgQueue(msgid, CLIENT_TYPE, buf);
if(strcasecmp("quit", buf) == 0)
break;
printf("client# %s\n", buf);
printf("Please enter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf));
if(s>0)
{
buf[s-1]=0;
sendMsgQueue(msgid, SERVER_TYPE, buf);
printf("send done, wait recv...\n");
}
}
destoryMsgQueue(msgid);
return 0;
}
client.c
#include "comm.h"
int main()
{
int msgid = getMsgQueue();
char buf[1024] = {0};
while(1)
{
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0)
{
buf[s-1]=0;
sendMsgQueue(msgid, CLIENT_TYPE, buf);
if(strcasecmp("quit", buf) == 0)
break;
printf("send done, wait recv...\n");
}
recvMsgQueue(msgid, SERVER_TYPE, buf);
printf("server# %s\n", buf);
}
return 0;
}
}
4)共享内存
查看当前内存共享端:
ipcs -m
头文件:
#include <sys/types.h>
#include <sys/shm.h>
① shmget 函数
作用:如果共享内存不存在,创建共享内存,如果存在就打开共享内存
int shmget(key_t key, size_t. int shmflg);
key:共享内存键值,编号,用十六进制表示比较好
size:带创建共享内存的大小
shflg:共享内存的访问权限 (如果共享内存不存在,就创建一个)
返回值:成功:共享内存标识符 失败:-1
② shmat 函数
作用:把共享内存连接到当前进程的地址空间(挂载)
void *shmat(int shm_id, const void *shm_addr, int shmflg)
shm_id:是由 shmget 函数返回的共享内存标识
shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统选择地址
shmflg:是一组标志位,通常为0
调用成功时返回指向共享内存第一个字节的指针,失败:-1
③ shmdt 函数
作用:将共享内存从当前进程中分离,相当于 shmat 函数的反操作
int shmdt(const void *shmaddr)
shmaddr:shmat 函数返回的地址
返回值:成功:0 失败:-1
④ shmctl 函数
作用:删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
shmid:共享内存标识符
cmd:函数功能的控制,取值:
IPC_RMID:删除。(常用 )
IPC_SET:设置 shmid_ds 参数,相当于把共享内存原来的属性值替换为 buf 里的属性值。 IPC_STAT:保存 shmid_ds 参数,把共享内存原来的属性值备份到 buf 里。 SHM_LOCK:锁定共享内存段( 超级用户 )。 SHM_UNLOCK:解锁共享内存段。
SHM_LOCK 用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其它进程访问。其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。这样做的优势在于让共享内存一直处于内存中,从而提高程序性能。
buf:shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。
返回值:成功:0 失败:-1