进程间通信
在前面我们学习了如何创建进程,但是如何让我们创建的这些进程相互联系起来,那么就需要利用进程间通信来完成。
概述
进程间通信就是在不同进程之间传播或交换信息,但是之前我们学过,每个进程在创建的时候都会分配自己独有的 4G 虚拟地址空间,进程之间都是相互独立的,所以一般而言是不能互相访问的,但是也有例外,那就是共享存储映射区。并且系统空间也是公共的,每个进程都可以访问,所以内核也可以提供进程间通信的条件。
目的
进程间通信的目的 :
- 数据传输 :一个进程需要将它的数据发送给另一个进程
- 资源共享 :多个进程之间共享相同的资源
- 通知事件 :一个进程需要向另一个进程或进程组发送消息,通知他们发生了某种事件
4. 进程控制 :有些进程希望完全控制另一个进程,此时控制进程希望能够拦截另一个进程的所有陷入和异常。
分类
- 管道
- 匿名管道
- 命名管道
- System V 进程间通信
- 消息队列
- 共享内存
- 信号量
- POSIX 进程间通信
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
我们把一个进程连接到另一个进程的一个数据流称为一个 ’ 管道 ‘。
匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
其中 pipefd[0] 表示读端
pipefd[1] 表示写端
管道是单向的,先进先出的,无结构的,固定大小的字节流。
它把一个进程的标准输出端和另一个进程的标准输入端连接在一起。
写进程在管道的尾端写入数据,读进程在管道的首部读取数据。数据读出后将从管道中移除,其他读进程不能再读到这些数据。
管道提供简单的流控制机制,进程在试图读空管道时,在有数据写入管道之前 ,读进程将一直阻塞。当管道已经满时,进程再试图写入数据,在其他进程将数据从管道中移除前,写进程将一直阻塞。
管道可存放数据上限 512 bytes * 8 = 4096 bytes
例子1
从键盘读取数据,写入管道,读取管道,输出到屏幕上。
相当于我们将本进程的标准输入写入到管道里 ,然后从管道中读取,再将读取到的数据输出到标准输出中。
实现 :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main( void ){
int ret = 0;
int len = 0;
int fd[2];
char buf[1024] = {0};
ret = pipe(fd );
if(ret < 0)
perror("pipe"),exit(1);
// read from stdin
while(fgets(buf, 1024, stdin)){
len = strlen(buf);
// write into pipe
if( write(fd[1] ,buf, len ) != len){
perror("write into pipe");break;
}
memset(buf, 0x00, 1024);
// read fron pipe
if((len = read(fd[0], buf, len)) < 0){
perror("read");break;
}
// write into stdout
if(write(1, buf, len) < 0){
perror("write");break;
}
}
}
测试 :
例子2
两个进程共享一个管道,其中一个管道负责写入数据,一个管道负责读取数据。
需要两个进程所以我们需要调用 fork,由于子进程会继承父进程的文件描述符表,所以此时两个进程公用同一个管道。
所以会出现同时有两个输入端,同时有两个输出端的情况
此时我们需要关闭输入进程的读取端和输出进行的输入端。
在这里我们使用父进程写入,子进程读取并输出到屏幕上。
实现 :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main( void ){
int fd[2];
if(pipe(fd) < 0)
perror("pipe"),exit(1);
int pid = fork();
if(pid < 0)
perror("fork"),exit(1);
else if(pid > 0){
//parent
close(fd[0]); // 关闭读取端
int ret = write(fd[1], "new", 3);
if(ret < 0)
perror("write"),exit(1);
}else{
//child
close(fd[1]); // 关闭输入端
char buf[10] = {0};
int ret = read(fd[0], buf, 10);
if(ret < 0)
perror("read"),exit(1);
printf("%s\n", buf);
}
}
特点
- 当没有数据可读时,read 调用阻塞,暂停进程,等待数据到达。
- 当管道已满时,wirte 调用阻塞,暂停进程,直到有进程读走数据。
- 如果所读取的管道对应的文件描述符被关闭,则 read 返回 0。
- 如果所写入的管道对应的文件描述符被关闭,则 write 触发异常,发出 SIGPIPE 信号,随后write 退出。
- 如果写入的数据小于管道的容量,则保证写入操作的原子性,否则不保证。
- 只能用于具有血缘关系的进程之间通信。
- 流式服务
- 进程退出,管道释放,管道的生命周期与进程同步。
- 半双工,只能单向流动,如果想要双向通信,只能创建两个管道。
匿名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
实现 :
#include <stdio.h>
#include <sys/types.h>
int main( void ){
mkfifo("test",0644);
}
运行后会创建一个名为 test 的管道文件。
匿名管道与命名管道的区别
匿名管道由函数 pipe 创建并打开
命名管道由函数 mkfifo 创建,由 open 打开
它们之间的区别只有打开和创建的方式不同,一旦这些工作完成后,它们将具有相同的语义。
缺点
管道发送的消息无法控制,是一次性全部发送过去的。
消息队列
概述
消息队列提供从一个进程向另一个进程发送数据块的方法。
每个数据块都被认为是有类型的,接收进程接收的数据可以有不同的类型。
消息队列缺点 :每条消息的最大长度是有上限的 ,每个消息队列可存放消息的总字节数是有上限的,系统上可以创建的消息队列的总数也是有上限的。
我们可以通过指令 ipcs -ql 进行查看消息队列的限制
消息队列可以将消息进行分片,保证获得的消息是完整的 。
内核为每个进程间通信对象维护一个数据结构
struct ipc_perm
{
__kernel_key_t key; //xxxget 函数提供
__kernel_uid_t uid; // 拥有者 ID
__kernel_gid_t gid; // 拥有者 组ID
__kernel_uid_t cuid; // 创建者 ID
__kernel_gid_t cgid; // 创建者组 ID
__kernel_mode_t mode; // 权限
unsigned short seq; // 序列号
};
消息队列结构
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
// 消息队列链表头部
struct msg *msg_last; /* last message in queue,unused */
// 消息队列链表尾部
__kernel_time_t msg_stime; /* last msgsnd time */
// 最后一次发送时间
__kernel_time_t msg_rtime; /* last msgrcv time */
// 最后依次接收时间
__kernel_time_t msg_ctime; /* last change time */
// 最后一次修改时间
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
// 消息队列消息总大小 --- 字节
unsigned short msg_qnum; /* number of messages in queue */
// 当前消息队列中消息的个数
unsigned short msg_qbytes; /* max number of bytes on queue */
// 每个队列可存放消息的最大字节数
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
// 最后一个发送消息的队列的 pid
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
// 最后一个接收消息的队列的 pid
};
消息队列控制
创建
msgget() 函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
key
- 写死 1234 , a
- IPC_PRIVATE
- ftok() 函数
如果 key 是 IPC_PRIVATE ,创建一个新的消息队列。
如果 key 不是IPC_PRIVATE,同时 msgflg 设定为 IPC_CREAT ,如果所要创建的对象已经存在则检查相应的访问权限,若允许访问则返回该对象的标识符,如果不存在则创建新的消息队列。
函数 ftok()
key_t ftok ( const char *path ,path 是一个路径, 一般使用当前目录(必须存在)
int pro_id ) pro_id 是子序号 低 8 位不能位 0
我们默认 ftok 函数的返回值是唯一的
返回值
次设备号查看
文件索引查看
将 920587转换为十六进制 0x000e 0c0b
现在我们使用 ftok() 函数创建一个消息队列
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/msg.h>
int main( void ){
int id = msgget(ftok(".",'a'),IPC_CREAT);
if( id == -1)
perror("msgget"),exit(1);
printf("msgget ok\n");
}
在这里我们给出的 pro_id 是 ‘a’, —–> 97 —-16进制 —- 61
次设备号为 2 —-16进制 —- 02
之前的文件描述符的低16位为 0c0b
组合起来我们应该产生会产生一个key值为 0x 6102 0c0b 的消息队列。
通过指令 ipcs -q 查看我们创建的消息队列
删除
我们可以通过 ipcrm -Q + key值手动删除一个消息队列。
msgctl() 函数 删除一个消息队列
int msgctl( int msqid, 相应的消息队列号
int cmd, IPC_STAT
IPC_RMID 删除该消息队列
struct msqid_ds *buf ) 每一个消息队列都有一个 msqid_ds 来描述队列当前状态
没事就填 NULL
实现 :我们利用之前创建的消息队进行删除
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <string.h>
int main( void )
{
int id = msgget(ftok(".",'a'), 0);
if( id == -1)
perror("ftok"),exit(1);
msgctl(id, IPC_RMID, 0);
}
接收/发送消息
msgsnd() 函数 —- 接收消息
int msgsnd( int msqid, 要写入的消息队列标识符
const void *msgp, 指向消息缓冲区的指针 msgbuf,用户可定义的通用结构
size_t msgsz, 消息大小,不包含 channel 字段
int msgflg ) IPC_NOWAIT 当消息队列写满时马上返回 并置错误码 errno = EAGAIN
如果我们第一次发送消息是向 3 号管道发送,则系统同时会打开 1,2号管道。
管道实际标号为 管道号 -1
msgrcv() 函数 —- 发送消息
ssize_t msgrcv ( int msqid , 要读取的消息队列标识符
void *msgp , 指向消息缓冲区的指针 msgbuf
size_t msgsz , 消息大小
long msgtyp , > 0 从指定的通道收取数据
= 0 返回消息队列中的第一条
< 0 读取 <= |tpye|,并且从最小的开始读取
int msgflg ) 一般写 0 / IPC_NOWAIT
现在我们用前面的函数来制作一个环形消息队列,一个进程负责向消息队列中发送消息,另一个进程负责从消息队列中读取消息。
发送 :
#include <sys/msg.h>
#include <unistd.h>
struct msgbuf{
long channel;
char text[1024];
};
int main( int argc, char* argv[] ){
if(argc != 3)
printf("usage :%s channel msg\n",argv[0]);
int id = msgget(1234, 0);
if(id == -1)
perror("msgget"),exit(1);
struct msgbuf mb;
mb.channel = atol(argv[1]);
strcpy(mb.text, argv[2]);
if(msgsnd(id, &mb, strlen(mb.text), 0) == -1)
perror("msgsnd"),exit(1);
}
接收 :
#include <sys/msg.h>
#include <unistd.h>
struct msgbuf{
long int channel;
char text[1024];
};
int main(int argc, char* argv[]){
long type;
struct msgbuf mb = { };
if(argc != 2)
printf("usage :%s channel msg%\n", argv[0]);
int id = msgget(1234, 0);
if(id == -1)
perror("msgget"),exit(1);
type = atol(argv[1]);
if(msgrcv(id, &mb, 1024, type, 0) < 0)
perror("msgrcv"),exit(1);
printf("%s\n",mb.text);
}
现在我们向 1 3 5号消息队列发送消息
我们对所创建的消息队列进行查看
可以看到,我们发送数据的总大小为 13个字节,总共有三条消息在等待接收
接收消息
但是这里要注意,如果我们接收的消息队列中没有消息,那么接收端将阻塞等待,直到有消息被写入
缺点
系统限制了消息队列的大小,没有共享内存更高效。
共享内存
概述
共享内存是进程间通信中最快的一种,当我们创建的共享内存映射到不同的进程之间后,当这些进程之间需要进行数据交换时,就不再需要内核的参与,效率变得更高。
而我们所创建的共享内存就位于栈和堆之间的共享存储映射区,在同一位置的还有相应的内存映射和共享库。
函数
创建
shmget()函数
int shmget(key_t key, size_t size, int shmflg);
key 要获取/创建的共享内存标识符
size 创建时输入大于0 的整数,所要创建的共享内存的大小
shmflg IPC_CREAT|0644
例
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
struct buf{
int nu;
char name[20];
};
int main(){
int ret = shmget(1234, sizeof(struct buf), IPC_CREAT|0644);
if(ret == -1)
perror("shmget"),exit(1);
printf("shmget ok\n");
}
通过 ipcs -m 查看创建的共享内存
删除
shmctl()函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid shmget返回的标识符
cmd IPC_RMID 删除共享内存
buf 指向保存着共享内存的数据结构
同时我们也可以使用 ipcrm -M + key 删除共享内存
挂载
shmat()函数
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid shget返回的标识符
shmaddr 指定链接的地址 填 NULL 由系统自动分配
shmflg 暂时填 NULL
取消挂载
int shmdt(const void *shmaddr);
shmat 返回的地址
例 :
我们使用共享内存进行简单的数据交换
输入
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
struct buf{
int nu;
char name[20];
};
int main(){
int ret = shmget(1234, 0, 0);
if(ret == -1)
perror("shmget"),exit(1);
struct buf* mybuf = (struct buf*)shmat(ret, NULL,0);
mybuf->nu = 10;
strcpy(mybuf->name, "hekzl");
shmdt(mybuf);
}
获取
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
struct buf{
int nu;
char name[20];
};
int main(){
int id = shmget(1234, 0 ,0);
if(id == -1)
perror("shmget"),exit(1);
struct buf * p = (struct buf *)shmat(id, NULL, 0);
printf("nu = %d name = %s\n", p->nu, p->name);
}
信号量
信号量在进程间通信主要用于进程间的同步与互斥
互斥 :
某一资源在同一时间只允许一个访问者进行访问,具有唯一性和排他性,但互斥无法限制访问者对资源的访问顺序,即访问是无效的。
同步 :
通过其他机制实现访问者对资源的有序访问,在大多数情况下,同步已经实现了互斥,特别是所有写入操作都必须互斥,少数情况可以允许多个访问者同时访问资源。
在进程中,涉及互斥资源的程序段称为临界区。
函数
semget()函数
int semget(key_t key, int nsems, int semflg)
key 要获取/创建时对应的标识符
nsemes 信号量集中的信号量个数
semflg 创建 IPC_CREAT|0644
获取 填 0
例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
int main( void )
{
int id = semget(1234, 1, IPC_CREAT|0644);
if( id == -1)
perror("semget"),exit(1);
printf("semget ok\n");
}
控制
semctl()函数
int semctl(int semid, int semnum, int cmd, ...);
semid semget返回的标识符
semnum 所要操作的信号量编号
cmd 操作 : SETVAL 设置信号量的初值
GETVAL 返回信号量的当前值
IPC_RMID 删除信号量
....
这个函数是否有第四个参数取决于 cmd
当有第四个时,第四个参数是一个联合类型
union semun {
int val; /* Value for SETVAL */
// val 的初值
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
// IPC_STAT,IPC_SET对应的结构体
unsigned short *array; /* Array for GETALL, SETALL */
// GETALL SETLL 对应的数组
struct seminfo *__buf; /* Buffer for IPC_INFO
// IPC_INFO 对应的结构体 (Linux-specific) */
};
PV操作
semop()函数
在信号量的数据结构 semun中有一个val值和一个指针,指针指向下一个等待该信号量的进程。信号量的值与相应资源的使用相关。当他大于 0 时,表示当前可用资源数,当小于零时,则表示当前等待资源的进程数。
而信号量的值只能通过 PV 操作来更改。
执行一个 P 操作,意味着请求分配一个资源,信号量的值 -1。
执行一个 V 操作,意味着请求释放一个资源,信号量的值 +1。
要注意,P V操作必须成对使用,从而不会造成死循环。
int semop(int semid, struct sembuf *sops, unsigned nsops)
semid semget返回的标识符
sops 指向存储信号操作结构的数组指针
nsops 信号操作结构的数量
sembuf 结构体
struct sembuf{
unsigned short sem_num; /* semaphore number */
// 信号量编号
short sem_op; /* semaphore operation */
// -1 p 操作 +1 v 操作
short sem_flg; /* operation flags */
// IPC_NOWAIT 不阻塞 SEM_UNDO 保证信号值会被重设,避免异常情况
};