Linux 进程间通信的机制,大致来说,Linux 进程间的通信机制可以分为 6 种
一 信号signal
1、信号是进程在运行过程中,由自身产生或由进程外部发来的消息(事件),信号是硬件中断的软件模拟(软中断)。每个信号用一个整形常量宏表示,以SIG开头,比如SIGCHLD、SIGINT等。
它们在系统头文件<signal.h>中定义,也可以通过在shell下键入kill -l查看信号列表,或者键入 man 7 signal 查看更加详细的说明。(具体的信号解析参考https://www.cnblogs.com/frisk/p/11602973.html 很全)
2、信号的生成来自内核,让内核生成信号的请求来自3个地方:
用户:用户通过CTRL + c ctrl +\ ,或者是终端程序驱动分配给信号控制字符的其他任何键来请求内核产生信号;
内核: 当进程执行出错时,内核会给进程发送一个信号,例如非法段存取(内存访问违规),浮点数溢出等;
进程:一个进程可以通过系统调用kill给另外一个进程发送信号,一个进程可以通过信号和另外一个进程进行通信
3、重要函数的理解
(1)、linux信号处理(signal)
原型: void (*signal(int signum, void (*handler))(int)))(int)
参数:
signum信号值
handler 处理函数,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)
返回值:
调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR
代码示例;(当参数SIGINT外部按下 ctrl + c 的时候就会接收到打印的信息)
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>
void hand(int lo) //其中io就是信号发生时传过来的值
{
printf("%d\n",lo);
}
int main()
{
signal(SIGINT,hand);//用signal函数将信号SIGINT跟hand函数绑定起来,当SIGINT信号产生的时候就调用hand函数
while(1)
{
sleep(1);
}
}
(2)、 linux信号处理(sigaction)
原型:
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact))
参数:
signum 信号值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)
act 是一个结构体指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理
其中结构体 struct sigaction 原型为:
struct sigaction{
void(*sa_handler)(int); //老类型的信号处理函数指针
void(*sa_sigaction)(int,siginfo_t *,void *); //新类型的信号处理函数指针
sigset_t sa_mask; //将要被阻塞的信号集合
int sa_flags; //信号处理方式
void(*sa_restorer)(void); //保留,不要使用
}
注意指示结构体的信号处理函数指针是哪个有效,如果sa_flags包含SA_SIGINFO该掩码,则sa_sigaction指针有效,否则是sa_handler指针有效
oldact 第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL
返回值:
调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR
说明:
用于改变进程接收到特定信号后的行为,可用于所有的信号。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性
代码示例;(当参数SIGINT外部按下 ctrl + c 的时候就会接收到打印的信息)
/********sa_flags不是SA_SIGINFO该掩码的情况**********/
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
void hand(int lo)
{
printf("%d\n",lo);
}
int main()
{
struct sigaction st;
st.sa_handler = hand;//
st.sa_flags =0;//sa_flags 设置为0 则将sa_handler作为信号处理函数指针
sigaction(SIGINT,&st,NULL); //将已经改好的结构体写进去,信号为SIGINT
while(1)
{
sleep(1);
}
}
/**sa_flags包含SA_SIGINFO该掩码,则sa_sigaction指针有效****************/
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
void hand(int lo,siginfo_t *pinfo,void * pReserved)
{
printf("%d\n",lo);
}
int main()
{
struct sigaction st;
st.sa_sigaction = hand;//
st.sa_flags =SA_SIGINFO;//sa_flags 设置为SA_SIGINFO 则将sa_sigaction作为信号处理函数指针
sigaction(SIGINT,&st,NULL); //将已经改好的结构体写进去,信号为SIGINT
while(1)
{
sleep(1);
}
}
运行结果:
(3)、linux信号处理(sigqueue信号发送函数与kill信号发送函数)
原型:
int sigqueue(pid_t pid, int signo, const union sigval val)
int kill(pid_t pid,int sig); //pid为要结束的进程号,sig为要发送的信号,一般对应SIGQUIT,表示结束进程号为pid的进程(函数如果成功,返回0,否则为-1)
sigqueue函数参数:
pid 进程pid
signo 即将发送的信号值。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
val 信号传递的参数,即通常所说的4字节值。
**typedef union sigval { //同一个时间只能传递一个,因为是共用体
int sival_int; // 可以传递整数
void *sival_ptr; //其他类型
}sigval_t;**
注意:由sigqueue函数发送的信号的第三个参数sival_ptr的值,可以被进程的信号处理函数的第二个参数info->si_ptr接收到,整形的sival_int 可以被信号处理函数的第二个参数info->si_int接收
返回值:
成功返回 0;否则,返回 -1
kill与sigqueue代码示例;
/*********kill代码历程**************/
#include<sys/types.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
int pid=getpid();//获取当前的进程号
int ret =0;
while(1)
{
ret++;
sleep(1);
if(ret>5)
{
kill(pid,SIGQUIT);//设置5秒将pid进程杀掉
}else{
printf("hello\n");
}
}
}
/*********sigqueue代码历程**************/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<signal.h>
void hand(int io,siginfo_t *pingo,void * pReserved)
{
char *p =(char *)(pingo->si_ptr);
printf("recv mysignbandier sign ino - %d, ingo %s\n",io,p);
}
int main()
{
//先注册信息
struct sigaction st;
st.sa_sigaction = hand;
st.sa_flags = SA_SIGINFO;
sigaction(SIGINT,&st,NULL);
union sigval mysigval;
char data[]="my queue info";//因为这个是共用体,所以如果赋值了字符串,那就不能再赋值sival_int
mysigval.sival_ptr =data;
while (1)
{
//向本进程发送信号,并附带信息
printf("start send meg\n");
sigqueue(getpid(),SIGINT,mysigval);
sleep(2);
}
}
结果示例:
(4)、linux定时器处理
二 管道 pipe
2.1.在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的 管线 pipelines 就是用管道实现的
2.2 创建匿名管道
#include <unistd.h>
int pipe (int fd[2]);
int pipe(int fd[2]);
. fd -传出的参数
. fd[0] - 读端
. fd[1] - 写端
接口:pipe(int fd[2]),其中fd[1]用于读,fd[2]用于写。创建匿名管道必须在创建子进程之前,否则子进程将无法复制。
2.3、管道:
在理解管道通信之前,我们要先理解管道这个概念:
所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,就像现实中的水管,水就像数据。(连接进程,相当于在进程间连接一个通路,用来传递信息)
“Linux下⼀切皆⽂件”,所以我们可以把管道看做是文件,是服务于管道通信的特殊文件,而管道通信是一种通信方式。
**2.4、 有问题就思考:
(1)单个进程是否能使用管道完成读写操作?
答:能
(2) 父进程间通信是否需要sleep函数?
父写 – 写的慢
子读 – 读的快**
2.5、 注意的事项:
父进程读
– 关闭写端
子进程写
–关闭读端
2.5代码编写
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
int fd[2];
int ret = pipe(fd); //创建管道
int pid_id;
printf("%d",ret);
if(ret == -1)
{
perror("pipe error\n");
}
pid_id = fork();
while(1) //循环创建进程进行对管道的读写操作
{
if(pid_id == 0) 子进程写
{
close(fd[0]);//关闭读操作符
char *burf;
burf = (char *)malloc(sizeof(char)*1024 ); //对burf开辟指针空间
//strcpy(burf,"I an child");
printf("输入你的传送的数据\n");
scanf("%s",burf);
write(fd[1],burf,strlen(burf));
sleep(1);
}else{//父进程读
close(fd[1]);//关闭写操作符
char* buf;
buf= (char *)malloc(sizeof(char)*10);
int i = sizeof(buf);
while(1){
ssize_t s = read(fd[0],buf,sizeof(buf)*1024);
if(s< 0)
{
printf(" No pintf\n");
break;
}else if(s>0)
{
// buf[s] = 0;
printf("parent get child: %s \n",buf);
//close(fd[0]);
break;
}else{
//printf("parent get child: %s \n",buf);
break;
}
}
}
}
}
三 共享内存 shared memory
共享内存机制之mmap基础概念
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存。
函数的使用:
1.创建共享内存映射
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
参数:
addr: 指定映射内存的首地址。通常传NULL,表示让系 统自动分配。
length: 共享内存映射区的大小。(文件的实际大小)
prot: 共享内存映射区的读写属性。(PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE)
PROT_READ:页内容可以被读取
PROT_WRITE:页可以被写入
flags: 标注 共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
fd: 用于创建共享内存映射区的那个文件的文件描述符。 offset: 默认为0,表示映射文件全部。偏移位置。需是4k的整数倍
返回值:
返回值:
成功:映射区的首地址。
失败有其下等错误供参考:
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
int munmap(void *addr,size_ length) 释放映射区
addr: mmap 的返回值大小
length: 大小
*代码示例一:(血缘进程父子进程)
用open函数 O_CREAT一个新文件来创建映射区(注意要指定文件大小
),并写入内容
该部分代码要注意部分:
(1):用于创建映射区大小为0,实际指定非0大小创建映射区,出“总线错误”。
(2):用与创建映射区的大小为0,实际制定大小创建映射区,出“无效参数”。
(3):用于创建映射区的文件读写属性为,只读。映射区属性为 读,写。出“无效参数”。
(4):创建映射区,需要read权限,至少要有read的权限,mmap的读写权限,应该 <=文件open函数的权限。(注意mmap只写权限是不行的)
(5): 文件描述符fd,在mmap创建映射之后,便可以关闭,后续访问文件,用地址访问
重要的两个函数:
1、fd = open(“文件名”,O_RDWR);
2、mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<sys/mman.h>
#include<fcntl.h>
void err(char *p)
{
perror(p);
exit(1);
}
int main()
{
char *a =NULL;
int fd;
fd = open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644); //打开创建 testmap文件
if(fd == -1)
err("open error");
/*lseek(fd,10,SEEK_END);
write(fd,"\0",1);*/
//ftruncate():函数功能:改变文件大小
/*函数说明:ftruncate()会将参数fd指定的文件大小改为参数length指定的大小。
参数fd为已打开的文件描述词,而且必须是以写入模式打开的文件。
如果原来的文件件大小比参数length大,则超过的部分会被删去
返 回 值:0、-1*/
ftruncate(fd,20);//需要写权限,才能拓展文件大小
int len = lseek(fd,0,SEEK_END);//函数功能:移动文件读写指针;获取文件长度;拓展文件空间
a = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//建立映射区
if(a == MAP_FAILED) //当返回值错误的时候打印错误信息
{
err("mmap error");
}
//使用a对文件进行读写操作
strcpy(a,"hello mmap"); //写操作
printf("+++++%s\n",a);
int ret = munmap(a,len);
if(ret == -1)
{
err("munmap error");
}
return 0;
}
代码示例二:
无血缘的进程间mmap通信代码
读区代码和写区的代码注意区别是open函数中,读区的代码只要读权限便可,写区代码要写权限和创建文件的权限
读:mmap_r.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<sys/mman.h>
#include<fcntl.h>
void err(char *p)
{
perror(p);
exit(1);
}
int main()
{
char *a_fd =NULL; //定义mmap文件描述符
int fd; //定义open文件描述符
pid_t pid;
char *b=NULL;
char *buf;
fd = open("testmap",O_RDWR); //open只要开启读权限
if(fd == -1)
err("open error");
int len = lseek(fd,0,SEEK_END);
a_fd = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//建立映射区
if(a_fd == MAP_FAILED)//当返回值错误的时候打印错误信息
{
err("mmap error");
}
close(fd);
while(1){ //循环读取时时跟新读取内容
if(a_fd != buf)
{
printf("parent , a = %s \n",a_fd);
usleep(100000);
}else{
buf =a_fd;
printf("no shucu\n");
sleep(2);
}
}
int ret = munmap(a_fd,len);
if(ret == -1)
{
err("munmap error");
}
sleep(1);
return 0;
}
写:mmap_w.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<sys/mman.h>
#include<fcntl.h>
#include <unistd.h>
void err(char *p)
{
perror(p);
exit(1);
}
int main()
{
char *a_fd =NULL;
int fd;
pid_t pid;
char *b=NULL;
char *buf;
buf = (char *)malloc(1024);
fd = open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd == -1)
err("open error");
/*lseek(fd,10,SEEK_END);
write(fd,"\0",1);*/
ftruncate(fd,20);
int len = lseek(fd,0,SEEK_END);
a_fd = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);打开创建 testmap文件
if(a_fd == MAP_FAILED)
{
err("mmap error");
}
close(fd);
while(1)
{
printf("plese shu ru :\n");
scanf("%s",buf);
printf("%s",buf);
strcpy(a_fd,buf);
// printf("+++++%s\n",a);
//
sleep(2);
}
int ret = munmap(a_fd,len);
if(ret == -1)
{
err("munmap error");
}
return 0;
}
示例:
四 先入先出队列 FIFO
五 消息队列 Message Queue
消息队列,Unix的通信机制之一,可以理解为是一个存放消息(数据)容器。将消息写入消息队列,然后再从消息队列中取消息,一般来说是先进先出的顺序。
可以解决两个进程的读写速度不同(处理数据速度不同),系统耦合等问题,而且消息队列里的消息哪怕进程崩溃了也不会消失。
消息队列:
- 进程间传递数据的一种简单的方法
- 把每个消息看作一个记录,具有特定的格式
- 消息队列就是消息的链表
- 对消息队列有写的权限的进程可以按照一定的规则添加新的消息
- 对消息队列有读权限的进程可以从消息队列中读走消息
- 消息队列能够克服管道或命名管道机制的一些缺点,例如实时性差等。
包含的头文件:sys/sypes.h ,sys/ipc.h, sys/msg.h
1、ftok函数生成键值
每一个消息队列都有一个对应的键值(key)相关联(共享内存、信号量也同样需要)。
所需头文件#include<sys/ipc.h>
key_t ftok(const char *path ,int id);
path为一个已存在的路径名
id为0~255之间的一个数值,代表项目ID,自己取
返回值:成功返回键值(相当于32位的int)。出错返回-1
例如:key_t key = ftok( “/buf”, 66);
2、创建消息队列
int msgget(key_t key,int oflag);
^返回值:成功返回创建或打开的消息队列对象ID;出错返回-1
^参数:
-key:创建或打开消息队列对象时指定的key值(提前约定或通过ftok函数创建)
-oflag: 设置访问权限,取值可以为以下一个或多个值
#define IPC_R 000400 读权限
#define IPC_W 000200 写和修改权限
#define IPC_M 010000 改变控制方式
还可以附加以下参数(按位或)
IPC_CREAT(如果消息队列对象不存在则创建,否则打开已经存在的消息队列对象)
IPC_EXCL(只有消息队列对象不存在时,才能创建新的消息队列对象,否则就产生错误)
IPC_NOWAIT(如果操作需要等待,则直接返回错误)
3、发送信息到消息队列
int msgsnd(int msgid,const void *ptr,size_t length,int flag);
* 返回值:成功返回0;出错返回-1
* 参数:
^msgid: 消息队列ID
^ptr:指向msgbuf的结构体指针(其中消息类型mtype必须大于0,小于0的消息类型有特殊的指示)
struct msgbuf{
long mtype;
char mtext[];};
^length:以字节为单位指定待发送消息的长度(msgbuf结构体中消息类型mtype之后的用户自定义数据的长度),该长度可以为0
^flag: 可以是0,也可以是IPC_NOWAIT(该标志可以使函数工作在非阻塞模式)
出现以下情况时:
-指定的消息队列容量已经满
-在系统范围存在太多的消息
若设置了IPC_NOWAIT,则msgsnd立即返回(返回EAGAIN错误)
若未指定该标志,则msgsnd导致调用进程阻塞,直到发送成功为止
4、 从消息队列接收消息
ssize_t msgrcv(int msqid, void *ptr,size_t length long type, int flag);
*返回值: 成功返回实际读取数据的字节数;出错返回-1
*参数:
-msgid: 消息队列ID
-ptr:消息缓冲区指针,指向msgbuf的结构体指针
-length: 消息缓冲区中数据部分的大小(msgbuf结构体中消息类型mtype之后的用户自定义数据的长度)
-flag: 当消息队列中没有期望接收的消息时会如何操作
. 若设置了IPC_NOWAIT标志,则函数立即返回ENOMSG错误
.若未设置IPC_NOWAIT标志,则msgrcv导致调用进程阻塞直到如下事件发生:
有其他进程向消息队列中发送了所期望接收的消息
该消息队列被删除,此时返回EIDRM错误
进程被某个信号中断,此时返回EINTR错误
5、对消息队列进行控制
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
* 返回值:成功返回0;错误返回-1,例如:msgctl(id,IPC_RMID,NULL);删除id号的消息队列
* 参数:
-msgid就是msgget函数返回的消息队列ID
-cmd有三个,常用删除消息队列的为IPC_RMID;IPC_STAT:取此队列的msqid_ds结构,并将它存放在buf指向的结构中;IPC_SET:改变消息队列的状态,把buf所指的msqid_ds结构中的uid、gid、mode复制到消息队列的msqid_ds结构内。
(内核为每个消息队列维护着一个结构,结构名为msqid_ds,这里就不讲啦,里面存放着消息队列的大小,pid,存放时间等一些参数)
-buf就是结构体msqid_ds
代码现实:
消息队列写端msgget1_w.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
struct msgbuf{ //定义msgbuf的结构体指针
long int msg_type;//读端该变量大于0.可以为0
char text[1024];
};
int main()
{
int id=0;
struct msgbuf chumsg;
id = msgget((key_t)1234,IPC_CREAT|0002); //两个进程用相同的key,就能共享了。 之后就能通讯了。例如下面用1234做key,0002为写权限
//当然也可以通过ftok获取,例如key_t key =ftok("/tmp",66);
if(id == -1)
{
perror("msgget \n");
}
while(1)
{
char mag[512];
memset(mag,0,sizeof(mag)); //每次循环都初始化mag
chumsg.msg_type = 1; //主要要大于或等于0
printf("请输入:\n");
fgets(mag,sizeof(mag),stdin); //从键盘输入消息
strcpy(chumsg.text,mag);
printf("%s",chumsg.text);
if(msgsnd(id,(void *)&chumsg,sizeof(chumsg.text),0) <0) //发送信息到消息队列,失败返回-1
{
printf("发送错误\n");
perror("msgsnd \n");
return 0;
}
//msgctl(id,IPC_RMID,NULL);
if(strncmp(mag,"end",3) == 0) //输入end结束循环
break;
}
if(msgctl(id,IPC_RMID,NULL)<0) //删除消息队列的为IPC_RMID
{
printf("del msg error \n");
return 0;
}
return 0;
}
消息队列读端msgget1_r.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
struct msgbuf{
long int msg_type;//定义msgbuf的结构体指针
char text[1024];
};
int main()
{
int id=0;
struct msgbuf chumsg;
// key_t key =ftok("/tmp",66);
id = msgget((key_t)1234,0004|IPC_CREAT);//两个进程用相同的key,就能共享了。 之后就能通讯了。例如下面用1234做key,0004为读权限
if(id == -1)
{
perror("msgget \n");
}
printf("%d\n",id);
while(1)
{
if(msgrcv(id,(void *)&chumsg,1024,0,0) <0)
{
// perror("msgrcv \n");
printf("发送错误\n");
return 0;
}
printf("data:%s\n",chumsg.text);
if(strncmp(chumsg.text,"qqqq",4) == 0)
break;
}
return 0;
}
效果图:
注意有时候会出现权限错误所以用 sudo 来运行
六 套接字 Socket
客户端代码实现:
#include<stdio.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int s_fd;
int c_fd;
int n_read;
//1.socket
s_fd=socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in s_addr;
s_addr.sin_family= AF_INET;
s_addr.sin_port = htons(8000); //端口号自己设置
inet_aton("IP地址",&s_addr.sin_addr);
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
//4.accept
c_fd =accept(s_fd,NULL,NULL);
if(c_fd == -1){
perror("accept");
}
printf("sdfasdf\n"); //记得要加\n不然大印不出来
while(1);
return 0;
}
运行结果图: