1. 概念
- Linux进程通信的方法一共有五种,分别是:
-
- 管道、信号、信号灯集、共享内存、消息队列、套接字
- 作用:
-
- 数据传输、资源共享、通知事件、控制进程
linux进程通信看这一篇就够了【管道、信号量、共享内存、消息队列】(超级详细、不骗人)_linux 共享内存 信号量_WolfOnTheWay的博客-CSDN博客
1.1. 发展历史(了解)
- 早期通信
- AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了"system V IPC",其通信进程主要 局限 在单个计算机内。【IPC:InterProcess Communication 译为:进程间通信 】
- BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了只能在同一计算机通信的限制,形成了基于套接字(socket)的进程间通信机制
1.2. 进程间通信方式
- 早期通信:无名管道(pipe)、有名管道(fifo)、信号(sem)
- system V IPC:共享内存(shared memory)、信号灯集(semaphore)、消息队列(message queue)
- BSD:套接字(socket)(在网络编程中专门学习)
1.3. 原理
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制
2. 早期通信:管道
2.1. 对比:无名管道 有名管道
有名管道 | 无名管道 | |
使用场景 | 不相关进程 | 具有亲缘关系的进程 |
特点 | 1)通过文件IO进行操作 2)遵循先进先出,不支持lseek操作 3)在文件系统中会存在管道文件,数据存放在内核空间 | 1)半双工的通信方式 2)固定读端fd[0]和写端fd[1] 3)看做一种特殊文件,通过文件IO操作 |
函数 | mkfifo() 先打开open,再读写read/write | pipe() 直接read write |
注意事项 | 1)只写方式,写阻塞,一直到另一个进程把读打开 2)只读方式,读阻塞,一直到另一个进程把写打开 3)可读可写,如果管道中没有数据,读阻塞; | 1)当管道中没有数据时,读阻塞 2)当管道写满数据时,写阻塞 3)当管道中无数据,关闭写端,读立即返回 4)关闭读端,写会导致管道破裂 |
2.2. 管道 共性特点
- 管道 分为 命名管道 和 无名管道
- 管道(文件) 的大小 永远为0
- 管道存在于 内存之中,不会 永久保存
- 管道的 传送方式 是半双工的(一边进,一边出)
- 无名管道通过函数pipe创建,只能用于父子进程之间
- 有名管道可以通过mkfifo+文件名的方式在终端进行创建,有名管道可以在任意两个进程之间进行通信
- 都是一个有简单流控制的先进先出的队列,自带同步机制
2.3. 无名管道 pipe
2.3.1. 说明:
- 无名管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
- 无名管道 基于文件描述符的通信方式。当一个无名管道建立时,它会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道
2.3.2. 函数:
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端 fd[1]:写端
返回值:
成功 0
失败 -1
2.3.3. 无名管道 使用特点:
- 当管道中 无数据时,读操作会阻塞;
管道中 无数据,将写端关闭,读操作会立即返回
- 管道中装满(默认管道大小64K)数据时,写阻塞,一旦余有4k空间,写继续,直到写满为止,再满再循环。
- 只有在管道的 读端存在时,向管道中写入数据才有意义。否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号 (通常Broken pipe错误)。
2.4. 有名管道 fifo
2.4.1. 说明:
- 有名管道可以使 互不相关的两个进程 互相通信。
- 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。
- 进程通过文件IO来操作有名管道
- 有名管道 遵循 先进先出规则,不支持lseek 操作
- 半双工通信
2.4.2. 函数:
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
mode:权限
返回值:成功:0
失败:-1,并设置errno号
2.4.3. 有名管道:使用特点
- 某进程用只写方式打开:写阻塞,一直到另一个进程用open把读打开;
- 某进程用只读方式打开:读阻塞,一直到另一个进程用open把写打开;
- 某进程用可读可写方式打开,如果fifo管道中没有数据,读阻塞,直到有数据写进去。
3. 早期通信:信号(通知:模拟中断)
区别 信号灯集semaphore 和 线程互斥信号量
3.1. 概念
- 信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式。
- 信号可以直接进行 用户空间进程 和 内核进程 之间的交互,内核进程也可以利用它来通知用户空间进程 发生了哪些系统事件
- 如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行 再传递给它;
- 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
3.2. 信号的响应方式
- 忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP
- 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。但是有两个信号不能捕捉:即SIGKILL及SIGSTOP
- 缺省操作:Linux对每种信号都规定了默认操作
3.3. 信号种类
2)SIGINT:结束进程,对应快捷方式ctrl+c
3)SIGQUIT:退出信号,对应快捷方式ctrl+\
9)SIGKILL:结束进程,不能 被忽略 不能 被捕捉
15)SIGTERM:结束终端进程,kill 使用时不加数字默认是此信号
17)SIGCHLD:子进程状态改变时给父进程发的信号
19)SIGSTOP:暂停进程,不能 被忽略 不能 被捕捉
20)SIGTSTP:暂停信号,对应快捷方式ctrl+z
26)SIGALRM:闹钟信号,alarm函数设置定时,当到设定的时间时,内核会向进程发送此信号结束进程。
3.4. 函数接口
- 发送信号
int kill(pid_t pid, int sig);
功能:信号发送
参数:
pid:指定进程
sig:要发送的信号
返回值:
成功 0
失败 -1
int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:
成功 0
失败 -1
- alarm定时
unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器
alarm倒计时结束,默认是结束进程,或者 设置捕捉操作
参数:
seconds:定时时间,单位为秒
传参为0,则取消定时
返回值:
如果调用此alarm()前,进程中已经设置了闹钟时间,则
返回上一个闹钟时间的剩余时间,否则返回0。
注意:
一个进程只能有一个闹钟时间。如果在调用alarm时,
已设置过闹钟时间,则之前的闹钟时间被新值所代替
- 信号捕捉
#include <signal.h>
typedef void (*sighandler_t)(int); //参数2的,函数原型
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:
signum:要处理的信号
handler:信号处理方式
SIG_IGN:忽略信号
SIG_DFL:执行默认操作
handler:捕捉信号 void handler(int sig){} //函数名可以自定义
返回值:
成功:指向 之前的信号处理函数 的指针
失败:-1
int pause(void);
功能:
用于将调用进程挂起,直到收到信号为止。
等待收到信号,因为挂起是 休眠暂停状态,需要被唤醒
pause收到一次信号后,就向下运行了;想循环使用要用while让其持续挂起
使用案例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数:arg为 收到的信号宏
void handler(int arg)
{
if (SIGINT == arg)
puts("-----ctrl + c");
else if (SIGQUIT == arg)
puts("-----ctrl + \\");
}
int main(int argc, char *argv[])
{
// 该进程,收到信号的处理方式
// 处理方式1:忽略操作
// signal(SIGINT, SIG_IGN); //ctrl + c
// 处理方式2:缺省操作(默认操作)
// signal(2, SIG_DFL); //2即SIGINT
// 处理方式3:捕捉信号 自定义处理方式
// 对收到的不同信号,设置处理函数
// 这样,执行完语句后,不执行命令默认功能,pause后继续向下
signal(SIGINT, handler);
signal(SIGQUIT, handler);
/* 进行挂起,收到信号才会 返回执行态 */
while (1) //pause收到一次信号后,就向下运行了,用while让其持续挂起
pause(); //等待收到信号,因为挂起是 休眠暂停状态,需要被唤醒
printf("process run end\n");
return 0;
}
4. IPC system V:共享内存 shared memory
4.1. 特点
- 共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝
- 为了在多个进程间交换信息,内核专门留出 一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间
- 进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
- 由于多个进程 共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等
4.2. 步骤
- 创建key值 ftok
- 创建或打开共享内存 shmget
- 映射共享内存到用户空间(拿地址)shmat
- 撤销映射 shmdt
- 删除共享内存 shmctl
4.3. 函数
4.3.1. 产生一个独一无二的key值 ftok
key_t ftok(const char *pathname, int proj_id);
功能:产生一个独一无二的key值
参数:
Pathname:已经存在的可访问文件的名字
Proj_id:一个字符(因为只用低8位)
返回值:
成功:key值
失败:-1
ftok的构成:十六进制下:字符为'a'
比如:0x61012cb9
'a'为oct:97,其hex:61
01为系统中对第一个这个的编号(不晓得 不必深究)
2cb9为文件inode号的hex格式
4.3.2. 创建或打开共享内存 shmget
int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
key 键值
size 共享内存的大小
shmflg IPC_CREAT|IPC_EXCL(判错)|0666
返回值:
成功 shmid
出错 -1
4.3.3. 映射、撤销映射、删除共享内存
映射共享内存:shmat
void *shmat(int shmid,const void *shmaddr,int shmflg);
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
shmid 共享内存的id号
shmaddr 一般为NULL,表示由系统自动完成映射
如果不为NULL,那么由用户指定
shmflg: SHM_RDONLY就是对该共享内存只进行读操作
为0:可读可写
返回值:
成功:完成映射后的地址,
失败:-1的地址
用法:
if( (p = (char *)shmat(shmid,NULL,0)) == (char *)(-1) )
取消映射内存:shmdt
int shmdt(const void *shmaddr);
功能:取消映射
参数:要取消的地址
返回值:
成功0
失败的-1
删除共享内存:shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:(删除共享内存),对共享内存进行各种操作
参数:
shmid 共享内存的id号
cmd IPC_STAT 获得shmid属性信息,存放在第三个参数
IPC_SET 设置shmid属性信息,要设置的属性放在第三个参数
IPC_RMID:删除共享内存,此时 *第三个参数* 为NULL 即可
返回:
成功0
失败-1
用法: shmctl(shmid,IPC_RMID,NULL);
4.4. shell命令
ipcs -m: 查看系统中的共享内存
ipcrm -m shmid:删除共享内存
4.5. 案例代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#define Buff_Size 128
typedef char DataType;
int main(int argc, char *argv[])
{
// 1、创建key
key_t key = ftok("./DoNotRemove.KEY", 'a');
if (key < 0)
{
perror("ftok err");
return -1;
}
printf("Key:%d\n", key);
// 2、创建或者打开 共享内存
// key值、内存长度大小、IPC_EXCL(判错,带着别管)
int shmid = shmget(key, Buff_Size, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (errno == shmid) //存在则,更新一下即可
shmid = shmget(key, Buff_Size, 0666);
else
{
perror("shmget err");
return -1;
}
}
// 3、映射 从内核到用户空间
// id、自动映射、可读可写
DataType *buf = shmat(shmid, NULL, 0);
if (buf == (DataType *)(-1))
{
perror("shmat err");
return -1;
}
// 4、操作 共享内存
// 写: fgets连\n都读进去,scanf还有\n在stdin缓冲区
if (fgets(buf, Buff_Size, stdin) != NULL)
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
// 读:
printf("buf:%s\n", buf);
// 5、撤销映射
shmdt(buf);
// 6、删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
5. IPC system V:信号灯集(进程 信号量) semaphore
5.1. 概念
信号灯(semaphore),也叫信号量。
- 它是不同进程间,或一个给定进程内部不同线程间 同步的机制。
- System V的信号灯是 一个或者多个信号灯 的一个集合。
- 其中的每一个都是单独的计数信号灯。
- 而Posix信号灯指的是单个计数信号灯。
- 通过信号灯集实现共享内存的同步操作。
5.2. 步骤
- 创建key值 ftok
- 创建或打开信号灯集 semget
- 初始化信号灯 semctl
- pv操作 semop
- 删除信号灯集 semctl
5.3. 函数
常配合 共享内存使用 + (信号灯集)
5.3.1. 创建/打开信号灯(集) semget
int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:
key: ftok产生的key值
nsems: 信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限,通常为IPC_CREAT |IPC_EXCL |0666
返回值:
成功:信号灯集ID
失败:-1
注意:// 可能为0,系统原因,0不能正常使用,要避免
// 创建 或 打开 信号灯集
// 灯集编号,里面几个灯,参数
int semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
// 可能为0,系统原因,0不能正常使用,要避免
if (semid <= 0)
{
if (errno == 17)
semid = semget(key, 2, 0666);
}
else
{
perror("semget err");
return -1;
}
5.3.2. 信号灯:初始化、删除 semctl
int semctl ( int semid, int semnum, int cmd…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:
semid: 信号灯集ID
semnum: 要操作的集合中的信号灯编号
cmd:
GETVAL:获取信号灯的值,返回值是获得值
SETVAL:设置信号灯的值,需要用到 第四个参数:共用体
IPC_RMID:从系统中删除信号灯集合
返回值:
成功 0
失败 -1
用法:
1.初始化:
union semun //自己去定义,系统man提供了模版
{
int val;
} mysemun;
mysemun.val = 10;
semctl(/*灯集ID*/semid, /*哪个灯的编号*/0, /*哪个操作*/SETVAL, /*设置用*/mysemun);
semctl(semid, 0, SETVAL, mysemun);
2.获取信号灯值:
函数semctl(semid, 0, GETVAL)的返回值
3.删除信号灯集:
semctl(semid, 0, IPC_RMID);
5.3.3. 信号灯PV操作
int semop ( int semid, struct sembuf *opsptr, size_t nops);
功能:
对信号灯集合中的信号量进行PV操作
参数:
semid: 信号灯集ID
opsptr: 操作方式:对集合中的哪个灯
nops: 要操作的信号灯的个数 1个
返回值:
成功 : 0
失败 :-1
操作方式的结构体:
struct sembuf {
short sem_num; // 要操作的信号灯的编号
short sem_op; // 0 : 等待,直到信号灯的值变成0
// 1 : 释放资源,V操作
// -1 : 申请资源,P操作
short sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
// 0 申请不到就阻塞
};
用法:
1.申请资源 P操作:
mysembuf.sem_num = 0; //灯编号
mysembuf.sem_op = -1; //P操作 资源量-1
mysembuf.sem_flg = 0; //阻塞形式
semop(semid, &mysembuf, 1);//灯集ID,结构体,操作几个灯数目
2.释放资源 V操作:
mysembuf.sem_num = 0;
mysembuf.sem_op = 1;
mysembuf.sem_flg = 0;
semop(semid, &mysembuf, 1);
5.4. shell命令
- ipcs -s:查看信号灯集
- ipcrm -s semid:删除信号灯集
5.5. 基础用例
#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <errno.h>
int main(int argc, char *argv[])
{
// 创建key
key_t key = ftok("./DoNotRemove.KEY", 'a');
if (key < 0)
{
perror("ftok err");
return -1;
}
printf("key:%d\n", key);
// 创建 或 打开 信号灯集
// 灯集编号,里面几个灯,参数
int semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
// 可能为0,系统原因,0不能正常使用,要避免
if (semid <= 0)
{
if (errno == EEXIST)
semid = semget(key, 2, 0666);
else
{
perror("semget err");
return -1;
}
}
else
{
// 只有在 创建成功后 初始化
union semun {
int val; /* Value for SETVAL */
} mysem;
/* 灯集ID,哪个编号的灯,干嘛操作,操作的共用体 */
mysem.val = 10; // 给灯赋值:资源数目
semctl(semid, 0, SETVAL, mysem); // 给灯赋值初始化
/* 灯的数目,从创建灯集的代码看到 */
mysem.val = 0;
semctl(semid, 1, SETVAL, mysem); // 给灯赋值初始化
}
printf("灯集的ID,semid:%d\n", semid);
printf("[0]的资源值:%d\n", semctl(semid, 0, GETVAL)); //参数2:哪个灯的编号
printf("[1]的资源值:%d\n", semctl(semid, 1, GETVAL)); //参数2:哪个灯的编号
/* -----PV操作----- */
struct sembuf mybuf; //操作灯集 用的结构体
mybuf.sem_num = 0; //具体哪个灯
mybuf.sem_op = -1; //Post -1
mybuf.sem_flg = 0; //0:申请不到就阻塞
semop(semid, &mybuf, 1); //操作灯集里面的几个灯
printf("havd: %d\n", semctl(semid, 0, GETVAL));
mybuf.sem_num = 1; //具体哪个灯
mybuf.sem_op = 1; //Wait +1
mybuf.sem_flg = 0; //0:这里不重要意义
semop(semid, &mybuf, 1); //操作灯集里面的几个灯
printf("havd: %d\n", semctl(semid, 1, GETVAL));
// 删除灯集
semctl(semid, 0, IPC_RMID); //第二个参数为0即可,不必深究
return 0;
}
6. IPC system V:消息队列 message queue
6.1. 概念
- 消息队列 是IPC对象的一种
- 消息队列 由消息队列ID来唯一标识
- 消息队列 就是一个消息的列表。
-
- 用户可以在消息队列中添加消息、读取消息等。
- 消息队列 可以按照类型来发送(添加)/接收(读取)消息
6.2. 步骤
- 创建key值 ftok
- 创建或打开消息队列 msgget
- 添加消息 :按照消息类型将消息添加到消息队列末尾 msgsnd
- 读取消息 :按照类型将消息从消息队列中读走 msgrcv
- 删除消息队列 msgctl
6.3. 函数
6.3.1. 创建或打开一个消息队列 msgget
int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数:
key值
flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:
成功:msgid
失败:-1
6.3.2. 添加消息 msgsnd
int msgsnd(int msqid, const void *msgp, size_t size, int flag);
功能:添加消息
参数:
msqid:消息队列的ID
msgp:指向消息的指针。
常用消息结构msgbuf如下:自己去定义
struct msgbuf{
long mtype; //消息类型 必须是long
char mtext[N]; //消息正文 可以是其他
};
size:发送的 消息正文 的字节数
flag:
IPC_NOWAIT消息没有发送完成函数也会立即返回
0:直到发送完成函数才返回
返回值:
成功:0
失败:-1
使用:
msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
注意:
消息结构除了第一个成员 必须 为long类型外,其他成员可以根据应用的需求自行定义。
6.3.3. 读取消息 msgrcv
int msgrcv(int msgid, void* msgp, size_t size, long msgtyp, int flag);
功能:读取消息
参数:
msgid:消息队列的ID
msgp: 存放读取消息的空间
size: 接受的消息正文的字节数
msgtype:
0:接收消息队列中第一个消息。
大于0:接收消息队列中第一个类型为msgtyp的消息.
小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
flag:
0:若无消息函数会一直阻塞
IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:
成功:接收到的消息的长度
失败:-1
6.3.4. 删除消息队列 msgctl
int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,删除消息队列
参数:
msqid:消息队列的队列ID
cmd:
IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
IPC_SET:设置消息队列的属性。这个值取自buf参数。
IPC_RMID:从系统中删除消息队列。
buf:消息队列缓冲区
返回值:
成功:0
失败:-1
用法:
msgctl(msgid, IPC_RMID, NULL);
6.4. shell命令
ipcs -q :查看消息队列
ipcrm -q msgid :删除消息队列
6.5. 代码用例:
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <string.h>
#define N 32
struct msgbuf
{
/* 消息类型 必须是long */
long mtype; /* message type, must be > 0 */
/* 消息正文 可以是其他类型 */
char mtext[32]; /* message data */
int age;
};
int main(int argc, char *argv[])
{
// 创建key值
key_t key = ftok("./DoNotRemove.KEY", 'a');
if (key < 0)
{
perror("ftok err");
return -1;
}
printf("key:%d\n", key);
// 创建 或者 打开 消息队列
int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid <= 0) //和信号等集一样,系统对编号0的,不能用
{
if (errno == EEXIST)
msgid = msgget(key, 0666);
}
else
{
perror("msgget err");
return -1;
}
printf("msgid:%d\n", msgid);
/* 添加消息 */
struct msgbuf mymsg_send;
// 消息结构体 赋值
mymsg_send.mtype = 1;
mymsg_send.age = 22;
strcpy(mymsg_send.mtext, "name_1");
// 添加消息到队列:哪个队列,信息结构体,消息正文长度,格式用0即可
msgsnd(msgid, &mymsg_send, sizeof(struct msgbuf) - sizeof(long), 0);
/* 读取消息 */
struct msgbuf mymsg_read;
// 从哪个消息队列读取,读到哪里,正文长度,
// msgtype 可以实现一种简单的接收优先级:0是同类型,即结构体long的值代表类型
// 格式用0即可
msgrcv(msgid, &mymsg_read, sizeof(struct msgbuf) - sizeof(long), 1, 0);
printf("****%s %d\n", mymsg_read.mtext, mymsg_read.age);
// 删除消息队列
// 哪个消息队列,操作,空就行 别管
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
7. IPC system V:BSD 套接字(socket)(这里不谈)
在网络编程中专门学习