本文目录
- 一、linux 进程之间的通信种类
- 二、管道
- 1. 管道的概述
- 2. 什么是管道文件?
- 3. 管道的特点
- 4. 管道类型
- (1)无名管道(pipe)
- (2)有名(命名)管道(fifo)
- 三、信号(signals)
- 1. 信号发送相关函数
- 2. 信号接收处理
- 四、消息队列
- 1. Linux中的消息队列有两种类型
- 2. 消息队列与有名管道(FIFO)的异同点
- 3. System V IPC 机制消息队列相关函数
- 4. 使用例程
- 五、共享内存
- 1. 共享内存原理
- 2. 共享内存特点
- 3. 相关函数
- 4. 使用例程
一、linux 进程之间的通信种类
序号 | 通信方式 | 描述 |
---|---|---|
1 | 管道(无名管道、有名管道) | 无名管道允许亲缘关系进程间的通信。有名命名管道还允许无亲缘关系进程间通信。 |
2 | 信号 signal | 在软件层模拟中断机制,通知进程某事发生。它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。 |
3 | 消息队列 Messge Queue | 是消息的链接表,包括 posix 消息队列和 SystemV 消息队列。它克服了前两种通信方式中信息量有限的缺点。 |
4 | 共享内存 Shared memory | 可以说是最有用的进程间通信方式,是最快的可用 ipc 形式。是针对其他通信机制运行效率较低而设计。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。 |
5 | 信号量Semaphore | 进程间同步。主要作为进程之间以及同一进程的不同线程之间的同步和互斥手段。 |
6 | 套接字 socket | 用于网络中不同机器间进程通信。 |
二、管道
1. 管道的概述
管道好比一条水管,有两个端口,一端进水,另一端出水。 管道是 Linux 进程间通信的一种方式,如管道命令 ls -l | grep anaconda3
,意思是从ls -l中搜索含有anaconda3的文件内容。
2. 什么是管道文件?
我们软件的管道文件也有两个端口,分别是读端和写端。进水可看成数据从写端被写入,出水可看数据从读端被读出。
3. 管道的特点
(1)管道通信是单向的,有固定的读端和写端;
(2)数据被进程在管道读出后,管道中的数据就不存在了;
(3)当进程去读取空管道的时候,进程会阻塞;
(4)当进程往满管道写入数据时,进程会阻塞;
(5)管道容量为 64KB;
4. 管道类型
管道类型分为无名管道、命名(有名)管道两类。无论是哪种管道我们都用 read、 write 函数来对管道进行读写。对于不同的管道类型有不同的方法。
对于无名管道,由于读端和写端处于血缘关系的进程中(同一个main函数中),所以必须要知道读写两端分别对应的文件描述符。
而对于命名(有名)管道,读端和写端处于毫无关系的两个进程中,所以需要创建一个文件作为管道,来对文件进行读写操作。需要注意:管道文件不支持创建在共享目录下,因为共享目录也属于windows。
(1)无名管道(pipe)
无名管道用于在一个main里的进程中,必须是父子进程或兄弟进程(一个父进程创建的多个子进程之间的关系)。
例如创建无名管道时,我们使用pipe()
来创建无名管道。对于无名管道的读写文件描述符我们通常保存在一个有两个整型元素的数组中,如 int fds[2]。然后调用函数 pipe(fds),这个函数会创建一个管道,并且数组 fds 中的两个元素会成为管道读端和写端对应的两个文件描述符。即 fds[0]为读端文件描述符, fds[1]为写端文件描述符。
无名管道的特点:
① 只能在亲缘关系进程间通信(父子或兄弟)。
② 半双工(固定的读端和固定的写端)。
③ 它是特殊的文件,可以用 read、write 等函数操作,这种文件只能在内存中。
管道两端的关闭是有先后顺序的。如果先关闭写端则从另一端读数据时,read 函数将返回 0,表示管道已经关闭;但是如果先关闭读端,则从另一端写数据时,将会使写数据的进程接收到 SIGPIPE 信号,如果写进程不对该信号进行处理,将导致写进程终止,如果写进程处理了该信号,则写数据的 write 函数返回一个负值,表示管道已经关闭。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
// 进程间通信
int main(int argc,char **argv)
{
int rec;
int pipefd[2]; //pipefd[0]读 pipefd[1]写
rec= pipe(pipefd);
if(rec < 0)
{
printf("error!\n");
}
pid_t pid;
pid=fork(); //创建进程
int i=0;
if(pid==0)
{ //子进程写
char buff[64];
while(1)
{
i++;
memset(buff,0,strlen(buff)); //清空数组
fgets(buff, sizeof(buff),stdin); //从终端获取字符给buff
write(pipefd[1], buff, sizeof(buff)); //将buff数组写入管道
if(i==3) break;
}
close(pipefd[1]); //关闭管道
close(pipefd[0]);
exit(0); //退出子进程
}
else if(pid>0)
{ //父进程读
char data[64];
int n;
wait(&n); //等待子进程结束
while(1)
{ i++;
memset(data,0,strlen(data));
read(pipefd[0], data, sizeof(data)); //从管道中读取内容传给data数组
printf("%s",data);
if(i==3) break;
}
close(pipefd[1]);
close(pipefd[0]);
exit(0);
}
else
{
printf("error!\n");
}
}
(2)有名(命名)管道(fifo)
无名管道只能在亲缘关系的进程间通信,这大大限制了管道的使用,有名管道突破了这个限制,通过指定路径名的形式实现不相关进程间的通信。
这里我们使用两个不相关的进程分别来进行通信,即使用两个命令窗口分别运行管道读端的程序和写端的程序。首先我们需要在写端使用命令mkfifo
创建管道。其第一个参数为创建的管道文件名,第二个参数为文件的权限。
问题:既然是创建文件作为管道,那么这个管道文件和普通文件有什么区别呢?
答:普通文件:用于存储数据,数据可以随机访问,可以读写多次,数据在文件关闭后依然存在。命名管道:用于进程间通信,数据是流式的,主要用于一次性读写,数据在被读取后即被移除,不持久存储。数据以流的形式传输,写入的数据只能按顺序读取,通常一个进程写入数据后,另一个进程立即读取。
●有名管道写端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
// 有名管道:用于两个无亲属关系的进程间的通信。 例如fifo_w 和fifo_r 间的通信。
int main(int argc, char **argv)
{
int fd;
char buff[64];
//创建一个有名管道文件,文件权限为可读可写
mkfifo("/tmp/fifo.cmd",0666);
//系统io来打开文件 ,不是标准io。打开有名管道的写端。
fd=open("/tmp/fifo.cmd",O_WRONLY);
if(fd < 0) printf("error!\n");
while(1)
{
memset(buff, 0, sizeof(buff));
fgets(buff, sizeof(buff), stdin); //从终端获取字符给buff
write(fd, buff, strlen(buff));
}
close(fd);
//删除有名管道文件
// unlink("/tmp/fifo.cmd");
return 0;
}
●有名管道读端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
//管道文件不支持创建在共享目录下,因为共享目录也属于windows
//有名管道:无亲属关系的进程间通信(不在同一个main函数中的进程)
// fopen:标准io open:系统io
int main(int argc, char **argv)
{
int fd;
char buff[64];
//打开有名管道的读端(因为写端已经创建了管道文件,所以读端只需要打开就行)
fd = open("/tmp/fifo.cmd", O_RDONLY);
if(fd < 0) printf("error!\n");
while(1)
{
memset(buff,0,sizeof(buff));
read(fd, buff, sizeof(buff)); //read 必须用 sizeof!!
printf("%s",buff);
}
close(fd);
//删除有名管道文件
// unlink("/tmp/fifo.cmd");
}
三、信号(signals)
在Linux中,信号是一种进程间通信机制,用于通知进程某些事件的发生。信号是一种异步的通知机制,当一个进程接收到信号时,可以选择处理该信号、忽略它或执行默认的操作。信号在系统编程中非常重要,常用于控制进程行为、处理异常情况和执行进程间通信。
信号类型如下:
对信号进行处理时,使用信号名称和使用信号编号效果相同。
1. 信号发送相关函数
(1)向指定进程发送信号
在函数中可以使用下面代码,在命令行中可以使用kill -信号编号 进程PID号
,来向指定的进程发送指定的信号。我们可以在命令行使用ps -ef
来查看当前所有进程的详细信息。
int kill(pid_t pid, int sig);
/*当 pid>0 将信号发送给指定进程;
当 pid==0 时,将信号发送给同组进程;
当 pid<0 时,将信号发送给进程组 ID 等于 pid 绝对值的进程;
当 pid==-1 时,将信号发送给所有进程;
int sig:信号指令(类型),如 SIGQUIT
*/
(2) 向进程自己发送信号
int raise(int sig);
(3)挂起调用该函数的进程,直到捕获到了一个信号。
int pause(void);
2. 信号接收处理
函数用于设置一个信号处理函数,当特定信号发生时,该函数会被调用。
sighandler_t signal(int signum, sighandler_t handler);
//int signum:要捕获或处理的信号编号。
//sighandler_t handler :接收到指定信号后要执行的函数。
使用例程:当按下Ctrl+c时会触发自定义函数,打印出signal:2。可以使用Ctrl+z结束该进程。
#include <stdio.h>
#include <signal.h>
void fun(int arg) //自定义函数
{
printf("signal:%d\n",arg);
}
int main(int argc,char **argv)
{
//当进程收到2号信号时,执行自定义的处理函数
signal(2, fun);
while(1)
{
}
}
四、消息队列
1. Linux中的消息队列有两种类型
System V消息队列(传统消息队列)
POSIX消息队列(现代消息队列)
2. 消息队列与有名管道(FIFO)的异同点
(1)相同点:
消息队列与 FIFO 很相似,都是一个队列结构,都可以有多个进程往队列里面写信息,多个进程从队列中读取信息。
(2)不同点:
FIFO 需要读、写的两端事先都打开,才能够开始信息传递工作。而消息队列可以事先往队列中写信息,需要时再打开读取信息。
3. System V IPC 机制消息队列相关函数
(0)包含的头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
(1)定义信息包结构体
//数据结构体
typedef struct{
char buff[10];
int number;
float sorce;
} my_data;
//信息包结构体
struct msgbuf{
long mtype;
my_data data;
};
(2)创建密钥-关键字
用于生成一个唯一的键(key),通常用于创建System V IPC对象,如共享内存、信号量和消息队列。 成功的时候,返回密钥值。失败返回-1。
key_t ftok(const char *pathname, int proj_id);
//const char *pathname :文件的路径全称且文件必须存在。
// int proj_id :非0的唯一识别键值id,不要重复。
(3)创建和访问一个消息队列
成功的时候,返回一个消息队列的唯一标识符 id(跟进程 ID 是一个类型)。失败返回-1。
int msgget(key_t key, int msgflg);
//key_t key:上一步生成的密钥。
//msgflg:指明队列的访问权限和创建标志。创建标志的可选值为 IPC_CREAT 和 IPC_EXCL。队列权限自定义。
(3)将消息添加到消息队列中
int msgsnd(int msqid, struct msgbuf * msgp, size_t msgsz, int msgflg);
//int msqid :步骤3返回的消息队列的id。
//struct msgbuf * msgp :发送信息的结构体
//size_t msgsz :消息包的大小
//int msgflg:可以为 0(通常为 0)或 IPC_NOWAIT。
(4)从消息队列中读取消息
ssize_t msgrcv(int msqid, struct msgbuf * msgp, size_t msgsz, long msgtyp, int msgflg);
//int msqid :步骤3返回的消息队列的id。
//struct msgbuf * msgp :发送信息的结构体
//size_t msgsz :消息包的大小
// long msgtyp :要接收的消息类型。如果指定为0,则接收队列中的第一条消息。如果大于0,则接收队列中第一个类型字段等于 msgtyp 的消息。如果小于0,则接收队列中第一个类型字段小于或等于 msgtyp 的消息。通常,msgtyp 是一个正整数,用于区分不同类型的消息。
//int msgflg:可以为 0(通常为 0)或 IPC_NOWAIT。
(4)删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds * buf);
/*
msqid 是由 msgget 返回的消息队列标识符。
cmd 通常为 IPC_RMID 表示删除消息队列。
buf 通常为 NULL 即可。
*/
4. 使用例程
●消息队列写端:每执行一次该程序就把消息发送出去一次,一直累加到消息队列中,等待读取。
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/msg.h>
//用命令:ipcs 查看消息队列内容
int main(int argc, char ** argv)
{
key_t key;
int ret;
struct msgbuf mbuf;
int msgid;
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("/mnt/hgfs/Share/7.进程间通信/5.消息队列/key.txt", 1);
if(key <0){
perror("error\n");
return -1;
}
//创建或访问消息队列
//消息队列的id,密钥,不存在时创建|读写权限
msgid = msgget(key,IPC_CREAT | 0666);
if(msgid <0){
perror("msgget error\n");
return -1;
}
//消息的类型,用于区分消息包的。
mbuf.mtype=1;
//填充消息体
mbuf.data.number=121;
mbuf.data.sorce=12.31;
strcpy(mbuf.data.buff,"i love you!");
//消息队列的id,待发送消息地址,消息体的大小,0
ret = msgsnd(msgid, &mbuf, sizeof(struct msgbuf), 0);
if(ret < 0){
perror("msgsnd error\n");
return -1;
}
//删除消息队列
//msgctl(msgid, IPC_RMID, NULL);
return 0;
}
●消息队列读端:每执行一次,就从消息队列中读取一次数据,消息队列中消息-1,直至读完。
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/msg.h>
//用命令:ipcs 查看消息队列内容
int main(int argc, char ** argv)
{
key_t key;
int ret;
struct msgbuf mbuf;
int msgid;
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("/mnt/hgfs/Share/7.进程间通信/5.消息队列/key.txt", 1);
if(key <0){
perror("error\n");
return -1;
}
//创建或访问消息队列
//消息队列的id,密钥,不存在时创建|读写权限
msgid = msgget(key,IPC_CREAT | 0666);
if(msgid <0){
perror("msgget error\n");
return -1;
}
//消息队列的id,读取消息的缓冲区,消息体的大小,读到第一个消息,0
ret=msgrcv(msgid,&mbuf, sizeof(struct msgbuf), 0 , 0 );
if(ret < 0){
perror("msgsnd error\n");
return -1;
}
printf("%d\n",mbuf.data.number);
printf("%f\n",mbuf.data.score);
printf("%s\n",mbuf.data.buff);
//删除消息队列
//msgctl(msgid, IPC_RMID, NULL);
return 0;
}
五、共享内存
共享内存也是进程间(进程间不需要有继承关系)通信的一种常用手段。一般 OS 通过内存映射与页交换技术,使进程的内存空间映射到不同的物理内存,这样能保证每个进程运行的独立性,不至于受其它进程的影响。但可以通过共享内存的方式,使不同进程的虚拟内存映射到同一块物理内存,一个进程往这块物理内存中更新的数据,另外的进程可以立即看到这块物理内存中修改的内容。多个进程可以直接读写共享的内存区域,不需要进行数据的复制或者传递。
1. 共享内存原理
(1)进程间需要共享的数据被放在该共享内存区域中。
(2)所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。
(3)这样一个使用共享内存的进程可以将信息写入该空间,而另一个使用共享内存的进程又可以通过简单的内存读操作获取刚才写入的信息,使得两个不同进程之间进行了一次信息交换,从而实现进程间的通信。
(4)共享内存允许一个或多个进程通过同时出现在它们的虚拟地址空间的内存进行通信,而这块虚拟内存的页面被每个共享进程的页表条目所引用,同时并不需要在所有进程的虚拟内存都有相同的地址。
(5)进程对象对于共享内存的访问通过 key(键)来控制,同时通过 key 进行访问权限的检查。
2. 共享内存特点
共享内存是最快的一种通信方式,适合大量数据的传输。只要创建的密钥一样,就可以共享内存空间。如果没有亲缘关系的进程使用共享文件,则需要密钥。如果有亲缘关系的进程使用共享文件,把key改为IPC_PRIVATE
。
3. 相关函数
(0)包含的头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
(1)定义信息包结构体
//数据结构体
typedef struct{
char buff[10];
int number;
float sorce;
} my_data;
//信息包结构体
struct msgbuf{
long mtype;
my_data data;
};
(2)创建密钥-关键字
用于生成一个唯一的键(key),通常用于创建System V IPC对象,如共享内存、信号量和消息队列。 成功的时候,返回密钥值。失败返回-1。
key_t ftok(const char *pathname, int proj_id);
//const char *pathname :文件的路径全称且文件必须存在。
// int proj_id :非0的唯一识别键值id,不要重复。
(3)创建/打开共享内存
成功则返回一个该共享内存段的唯一标识号(唯一的标识了这个共享内存段)。否则返回-1。
int shmget(key_t key, int size, int shmflg);
/*key :是一个与共享内存段相关联的关键字。
Size:指定共享内存段的大小,以字节为单位。
Shmflg:是一掩码合成值,可以是访问权限值与(IPC_CREAT 或 IPC_EXCL)的合成。
IPC_CREAT 表示如果不存在该内存段,则创建它。
IPC_EXCL 表示如果该内存段存在,则函数返回失败结果(-1)。
*/
(4)映射到进程空间地址
如果调用成功,返回映射后的进程空间的首地址,否则返回(void*)-1。
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
Shmid:共享内存段的标识 通常应该是 shmget 的成功返回值。
Shmaddr:共享内存连接到当前进程中的地址位置。通常是 NULL,表示让系统来选择共享内存出现的地址。
Shmflg:一组位标识,通常为 0 即可。
*/
(5)共享内存段与进程空间分离
将共享内存分离并没删除它,只是使得该共享内存对当前进程不再可用。 成功返回 0,失败时返回-1
int shmdt(const void *shmaddr);
//shmaddr 为 shmat 的成功返回值。
(6)删除共享内存段
成功返回 0,失败时返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
Shmid:共享内存段标识 通常应该是 shmget 的成功返回值。
Cmd:对共享内存段的操作方式。
可选为 IPC_STAT,IPC_SET,IPC_RMID。
通常为 IPC_RMID,表示删除共享内存段。
Buf:共享内存段的信息结构体数据,通常为 NULL。
*/
4. 使用例程
●写入共享内存
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#define SHM_MAX 1024
int main(int argc, char **argv)
{
key_t key;
int shmid;
char *p;
my_data *data;
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("/home/qjl/1.sh", 0);
if(key <0){
perror("error\n");
return -1;
}
//所创建/打开的共享内存的id,(密匙,空间大小,打开的空间不存在时创建)
shmid=shmget(key, SHM_MAX, IPC_CREAT);
if(shmid<0) {
perror("error\n");
return -1;
}
//将共享内存映射到本进程的空间中
//映射后的起止地址,待操作的空间,映射起止地址,0
data=(my_data *)shmat(shmid, NULL, 0);
if(data ==(my_data *)-1){
perror("error\n");
return -1;
}
//方式一:
//往共享的地址空间写东西
//结构体指针用->访问
/* data->n=10;
data->f=12.33;
strcpy(data->buff,"hello");
*/
//方式二:(安全,防止数据混乱)
my_data d;
d.n=222;
d.f=123.1;
strcpy(d.buff,"world");
memcpy(data,&d,sizeof(my_data)); //将d的数据拷贝到data中
//将共享内存空间从本进程中分离
shmdt(data);
//删除共享内存空间
// shmctl(shmid,IPC_RMID,NULL);
return 0;
}
●读取共享内存
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#define SHM_MAX 1024
int main(int argc, char **argv)
{
key_t key;
int shmid;
char *p;
my_data *data;
my_data d;
char buff[64]={0};
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("/home/qjl/1.sh", 0);
if(key <0){
perror("error\n");
return -1;
}
//所创建/打开的共享内存的id,(密匙,空间大小,打开的空间不存在时创建)
shmid=shmget(key,SHM_MAX,IPC_CREAT);
if(shmid<0) {
perror("error\n");
return -1;
}
//将共享内存映射到本进程的空间中
//映射后的起止地址,待操作的空间,映射起止地址,0
data=(my_data *)shmat(shmid, NULL, 0);
if(data== (my_data *)-1){
perror("error\n");
return -1;
}
//从共享空间读取
memcpy(&d, data, sizeof(my_data)); //将共享空间data的数据读到d中
printf("%d\n",d.n);
printf("%f\n",d.f);
printf("%s\n",d.buff);
//将共享内存空间从本进程中分离
shmdt(data);
//删除共享内存空间
// shmctl(shmid,IPC_RMID,NULL);
return 0;
}