linux进程间的通信那点事-详解
前言
本章主要对linux下常见的几种进程间通信方式进行分享,通过此章节我们可以了解并掌握linux下的几种进程通信的特点和区别:管道(无名、有名)、 消息队列、信号量、信号、共享内存、socket套接字。
提示:以下是本篇文章正文内容,下面案例可供参考
一、进程间通信概述
在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication),它是多个进程之间相互沟通的一种方法。在linux下有多种进程间通信的方法:半双工管道、命名管道、消息队列、信号、信号量、共享内存、内存映射文件,套接字等等。使用这些机制可以为linux下的网络服务器开发提供灵活而又坚固的框架。熟悉掌握各种通信方式对处理不同进程之间的交流是十分重要的。
1.1 管道介绍
管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。
管道的特点:
一、无名管道
1、它只能用于亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)
2、它是一个半双工的通信模式,也就是说读写的时候只能进行一个,具有固定的读端和写端
3、管道也可以看做是一种特殊文件,对于它的读写也可以使用普通的read write等函数,但它又不是普通的文件,它不属于任何文件系统,并且只存在于内存中。
二、有名管道
1、它可以使互不相关的两个进程实现彼此间的通信
2、该管道可以通过路径名来指出、并且在文件系统中是可以看见的,建立管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用比较方便
3、FIFO严格遵循先进先出的原则,对管道及FIFO的读写操作总是从开始处返回数据,对它们的读写操作则把数据添加末尾,不支持如Lseek()等文件定位操作
相关函数原型介绍
int pipe(int pipefd[2]); //无名管道 fd[0]表示读 fd[1] 表示写
int mkfifo(const char *pathname, mode_t mode);//有名管道的创建
1.2 消息队列介绍
消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容,消息顺序地发送到消息队列中,并以几种不同的方式从队列中获得,每个消息队列可以用 IPC标识符 唯一地进行识别。内核中的消息队列是通过IPC的标识符来区别,不同的消息队列直接是相互独立的。每个消息队列中的消息,又构成一个 独立的链表。
消息队列克服了信号承载信息量少,管道只能承载无格式字符流。
消息队列的实现包括创建或打开消息队列、添加消息、读取消息和控制消息队列这4种 操作。
简单的来说消息队列就是譬如一个班进行分组每10个人为一个小组进行排队,那么我们所分的每一个小组就是一个队列,而小组中的每一个成员就是该队列中的一个序号,我们在进行处理的时候就通过不同的序号来进行队列里面消息的处理(0x1234 0x2345 0x4567 0x5678)就代表四个不同的队列,当我们选择其中一个(0x1234)时就表示我们要对该消息队列进行操作然后就是确定我们操作的序号(譬如:888)。在下面消息队列的实验中我们就可以很容易明白。
相关函数原型介绍
int msgget(key_t key, int msgflg); //创建消息队列
//接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
1.3 信号量介绍
信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。
信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号)
p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1
简单理解就是P相当于申请资源,V相当于释放资源
同样对于信号量的理解简单来说,它不是为了存储我们的内容而是控制进程访问的顺序,如果我们指定要子进程先运行然后父进程在运行,那么我们就可以通过对信号量的控制来处理进程,P就相当于一个去拿锁当把锁拿到后就可以进行下一步的操作,v就相当于去放锁。下面实验中会详细介绍二者不同的操作。
//创建/获取信号量
int semget(key_t key, int nsems, int semflg);
//初始化
int semctl(int semid, int semnum, int cmd, ...);
1.4 内存共享介绍
共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在该进程的地址空间(这里的地址空间具体是哪个地方?)中。其他进程可以将 同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。
共享内存是 IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。
注意:共享内存本身并没有同步机制,需要程序员自己控制。
当我们对共享内存操作时一定要处理好同步关系,因为进程之间是共享同一块内存,如果两个不同的进程同时进行读或者写操作那么里面的内容就可能会乱掉。并且我们写入里面的内容是可见性的,就好比你的男朋友在一张纸上写东西,因为是共享该内存空间的,所以他写的同时,你就可以看见该内容。
//创建共享内存
int shmget(key_t key, size_t size, int shmflg);
//映射
void *shmat(int shmid, const void *shmaddr, int shmflg);
//赋值
char *strcpy(char *dest, const char *src);
//挂载
int shmdt(const void *shmaddr);
//删除
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
1.5 信号介绍
信号机制是unix系统中最为古老的进程之间的通信机制,用于一个或几个进程之间传递异步信号。信号可以有各种异步事件产生,比如键盘中断等。shell也可以使用信号将作业控制命令传递给它的子进程。
信号在linux下有很多种类型,我们常使用的一种信号就是通过键盘拿下的ctrl +c结束进程。
typedef void (*sighandler_t)(int);
//信号初始化
sighandler_t signal(int signum, sighandler_t handler);
1.6 socket套接字
socket 套接字网络编程在前面一章已经讲过了,这里就不在重复了。
二、管道编程实战
2.1 无名管道
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
/*------------------------------无名管道--------------------------------*/
int main(void)
{
int fd[2]; //fd[0]表示读 fd[1]表示写
int pid = 0;
char readbuf[128] = {0};
char writebuf[128] = "hello my world";
// int pipe(int pipefd[2]);
if(pipe(fd) == -1)
{
printf("pipe failed\n");
exit(-1);
}
pid = fork(); //创建子进程
if(pid < 0) //创建失败
{
printf("fork error\n");
exit(-1);
}
else if(pid == 0) //子进程读
{
close(fd[1]); //关闭写
printf("this is child\n");
read(fd[0],readbuf,sizeof(readbuf));
printf("read data:%s\n",readbuf);
exit(0);
}
else
{
//父进程写
close(fd[0]);
printf("this is father\n");
write(fd[1],writebuf,sizeof(readbuf));
wait(NULL); //等待子进程退出
}
return 0;
}
2.1.1 实验结果
2.2命名管道
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
/*------------------------------------------有名管道----------------------------------------*/
int main(void)
{
int pipe_fd = 0;
char readbuf[128] = {0};
//char writebuf[128] = "message from write";
//int mkfifo(const char *pathname, mode_t mode);
//创建管道
if(mkfifo("/tmp/cmd_pipe", S_IFIFO | 0666) == -1)
{
printf("mkfifo error\n");
exit(-1);
}
else
{
printf("mkfifo success\n");
}
pipe_fd = open("/tmp/cmd_pipe",O_RDWR);
if(pipe_fd == -1)
{
printf("open error\n");
exit(-1);
}
//读取数据
read(pipe_fd,readbuf,sizeof(readbuf));
printf("get data:%s\n",readbuf);
//写入数据
//write(pipe_fd,writebuf,sizeof(writebuf));
//关闭
close(pipe_fd);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
/*------------------------------------------有名管道----------------------------------------*/
int main(void)
{
int pipe_fd = 0;
//char writebuf[128] = "message from pipe read";
char readbuf[128] = {0};
//int mkfifo(const char *pathname, mode_t mode);
pipe_fd = open("/tmp/cmd_pipe",O_RDWR);
if(pipe_fd == -1)
{
printf("open error\n");
exit(-1);
}
//写入数据
//write(pipe_fd,writebuf,sizeof(writebuf));
//读取数据
read(pipe_fd,readbuf,sizeof(readbuf));
printf("get data:%s\n",readbuf);
//关闭
close(pipe_fd);
//打开管道进行数据处理 open
return 0;
}
2.2.1 实验结果
三、消息队列编程实战
3.1 client 端
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf
{
long mtype; //消息类型
char mtext[128]; //内容
};
int main(void)
{
int msgid = 0;
struct msgbuf readbuf;
struct msgbuf writebuf={888,"msg from client"};
//创建消息队列
msgid = msgget(0x6789,IPC_CREAT | 0777);
if(msgid == -1)
{
perror("why");
exit(-1);
}
//消息发送
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgsnd(msgid,&writebuf,sizeof(writebuf.mtext),0);
//接收消息
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),889,0);
printf("read data from que:%s\n",readbuf.mtext);
return 0;
}
3.2 server 端
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf
{
long mtype; //消息类型
char mtext[128]; //内容
};
int main(void)
{
int msgid = 0;
struct msgbuf readbuf;
struct msgbuf writebuf = {889,"msg from server"};
//创建消息队列
msgid = msgget(0x6789,IPC_CREAT | 0777);
if(msgid == -1)
{
perror("why");
exit(-1);
}
//接收消息
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),888,0);
printf("read data from que:%s\n",readbuf.mtext);
//消息发送
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgsnd(msgid,&writebuf,sizeof(writebuf.mtext),0);
return 0;
}
3.3 实验结果
四、信号量编程实战
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
//P 操作
//如果信号量的值为1,获取资源并将其信号量置-1
//如果信号量的值为0,进程挂起等待
void P_GetKey(int id)
{
// int semop(int semid, struct sembuf *sops, size_t nsops);
struct sembuf set;
set.sem_num = 0; //序号
set.sem_op = -1; //p操作
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("Get Key\n");
}
//V 操作
// 释放资源并将信号量值+1
// 如果有进程正在等待挂起,则唤醒它们
void V_BackKey(int id)
{
// int semop(int semid, struct sembuf *sops, size_t nsops);
struct sembuf set;
set.sem_num = 0; //序号
set.sem_op = 1; //V 操作
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("Back Key\n");
}
/*------------------------------------信号量——-----------------------------*/
int main(int argc,char *argv[])
{
key_t key;
int semid = 0;
int pid = -1;
key = ftok(".",2);
//int semget(key_t key, int nsems, int semflg);
//获取/创建信号量 1->该集合中只有一个信号量
semid = semget(key,1,IPC_CREAT |0666);
union semun initsem;
initsem.val = 0;//赋值
//0 ->操作第0个信号量 SETVAL 设置信号量的值
semctl(semid,0,SETVAL,initsem); //初始化信号量
pid = fork();
if(pid > 0) //父进程
{
//去拿锁
P_GetKey(semid);
printf("this is father\n");
//锁放回去
V_BackKey(semid);
//销毁 删除信号量集
semctl(semid,0,IPC_RMID);
}
else if(pid == 0)
{
printf("this is child\n");
//锁放回去 释放资源
V_BackKey(semid);
}
else
{
printf("fork error\n");
}
return 0;
}
在这里如果不使用信号量操作的话一般都是父进程先执行,然后子进程在执行,通过对信号量的操作我们就可以控制运行的顺序。
五、内存共享编程实战
5.1 创建端
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int shmid = 0;
char *shmaddr = NULL;
key_t key;
key = ftok(".",1);
//创建共享内存
// int shmget(key_t key, size_t size, int shmflg);
shmid = shmget(key,1024*4,IPC_CREAT|0666);
if(shmid == -1)
{
printf("shmget failed\n");
exit(-1);
}
//映射
// void *shmat(int shmid, const void *shmaddr, int shmflg);
shmaddr = shmat(shmid,0,0);
printf("shmat ok\n");
//数据
// char *strcpy(char *dest, const char *src);
strcpy(shmaddr,"hello my world");
sleep(3);
//挂载
//int shmdt(const void *shmaddr);
shmdt(shmaddr);
//删除
// int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid,IPC_RMID,0);
printf("quit\n");
return 0;
}
5.2 读取端
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
//查看共享内存 ipcs -m 删除共享内存ipcrm -m + id
int main(void)
{
int shmid = 0;
char *shmaddr = NULL;
key_t key;
key = ftok(".",1);
//创建共享内存
// int shmget(key_t key, size_t size, int shmflg);
shmid = shmget(key,1024*4,0);
if(shmid == -1)
{
printf("shmget failed\n");
exit(-1);
}
//映射
// void *shmat(int shmid, const void *shmaddr, int shmflg);
shmaddr = shmat(shmid,0,0);
printf("shmat ok\n");
//数据
printf("get data:%s\n",shmaddr);
//挂载
//int shmdt(const void *shmaddr);
shmdt(shmaddr);
//删除
// int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid,IPC_RMID,0);
printf("quit\n");
return 0;
}
5.3 实验结果
六、信号编程实战
6.1 Linux下所有信号的类型 kill -l 查看
下面对信号相关的使用就用几个简单的demo来进行演示,其他相关的信号使用可以根据自己来做小练习。
6.2 信号的捕获
#include <stdio.h>
#include <signal.h>
void handler(int signum)
{
printf("signum = %d\n",signum);
switch(signum)
{
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGLKILL\n");
break;
case 10:
printf("SIGUSR1");
break;
}
}
int main(void)
{
//signal 绑定函数
signal(SIGINT,handler);
signal(SIGKILL,handler);
signal(SIGUSR1,handler);
while(1);
return 0;
}
实验结果
6.3 传入参数 kill信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int signum = 0;
int pid = 0;
char cmd[128] = {0};
if(argc != 3)
{
printf("param is error\n");
exit(-1);
}
// int atoi(const char *nptr);
signum = atoi(argv[1]);
pid = atoi(argv[2]);
sprintf(cmd,"kill -%d %d\n",signum,pid);
system(cmd);
// int kill(pid_t pid, int sig);
//kill(pid,signum);
return 0;
}
实验结果
我们可以通过ps -aux | grep a.out 来查看当前进程运行的Pid号
6.4 信号携带消息处理sigaction
send发送消息
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
/*
union sigval {
int sival_int;
void *sival_ptr;
};
*/
int main(int argc,char *argv[])
{
int signum = 0;
int pid = 0;
if(argc != 3)
{
printf("param error\n");
exit(-1);
}
printf("my pid = %d\n",getpid());
signum = atoi(argv[1]);
pid = atoi(argv[2]);
union sigval value;
value.sival_int = 100;
// int sigqueue(pid_t pid, int sig, const union sigval value);
sigqueue(pid,signum,value);
printf("%d done\n",pid);
return 0;
}
recv 接收消息
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signum,siginfo_t *info,void *context)
{
printf("signum = %d\n",signum);
if(NULL != context)
{
//sprintf("get data = %d\n",info->si_int);
printf("get data = %d\n",info->si_value.sival_int);
printf("get pid = %d\n",info->si_pid);
}
}
int main(void)
{
struct sigaction act;
printf("pid = %d\n",getpid());
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO ;
//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
sigaction(SIGUSR1,&act,NULL);
while(1);
return 0;
}
实验结果
总结
通过该章的学习我们了解了linux下进程间常见的几种通信方式(管道、消息队列、信号量、信号、共享内存、socket套接字。并且通过一些小demo的练习也初步掌握了各自通信方式的简单使用,记录自己学习过程,加油。