进程间通信方式
IPC,进程间通信,是操作系统为用户提供的几种进程间通信方式,包括:管道、共享内存、消息队列、信号量。
因为进程之间具有独立性,每个进程只能访问并操作自己的虚拟地址空间,进程之间无法直接相通,因此需要操作系统提供公共媒介,同时因为通信场景的不同,操作系统提供了不同的方式。
通信场景 | 通信方式 |
---|---|
数据传输 | 管道、消息队列 |
数据共享 | 共享内存 |
进程控制 | 信号量 |
管道
管道的本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信。
种类:匿名管道、命名管道
匿名管道,一个进程通过系统调用在内核中创建了一个管道(缓冲区),并且调用返回的管道的操作句柄,但是内核中的这块缓冲区没有相应的其他标识符,只能通过句柄访问。所以匿名管道只能用于具有亲缘关系的进程,这样的进程才能获得操作句柄,因为只能通过子进程复制父进程的方式从而获取到同一块管道的操作句柄(故创建管道需要先于创建子进程),操作句柄也即文件描述符,进而才能访问同一块缓冲区。
命名管道,内核中的这块缓冲区有一个标识符,这个标识符是一个可见于文件系统的管道文件。
管道操作句柄是两个文件描述符,一个用于从管道中获取数据,另一个用于想管道中写入数据。
管道是半双工通信,即同一时刻管道的读写放方向是一定的,但是可以由用户决定读写的方向,即是哪个进程写入、哪个进程读取。
管道的生命周期跟随进程。
管道自带同步与互斥。
管道提供的是字节流传输服务(故有可能发生数据粘连)。
#include <unistd.h>
int pipe(int pipefd[2]);
创建一个匿名管道
pipefd[0]:用于从管道中读取数据,读端
pipefd[1]:用于向管道中写入数据,写端
返回值:成功返回0,失败返回-1
// 匿名管道的使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define N 1024
int main()
{
int pid = 0, pipefd[2];
int ret = pipe(pipefd);
if (ret < 0)
{
perror("pope error\n");
return -1;
}
pid = fork(); // 先创建匿名管道再创建子进程
if (pid == 0) // 子进程从管道中读取数据
{
char buf[N] = {0};
int re = read(pipefd[0], buf, N-1);
if (re < 0)
{
perror("read error\n");
return -1;
}
printf("child: %s\n", buf);
}
else // 父进程向管道写入数据
{
int p_size = 0;
while (1)
{
char *ptr = "data";
int r = write(pipefd[1], ptr, strlen(ptr));
if (r < 0)
{
perror("write error\n");
return -1;
}
p_size += r;
printf("write: %d\n", p_size);
}
}
return 0;
}
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
创建一个命名管道
pathname:管道文件的路径名称
mode:管道的操作权限
返回值:成功返回0,失败返回-1
共享内存
共享内存是最快的进程间通信方式,共享内存相较于其他进程间通信方式,在通信过程中少了两次用户态与内核态之间的数据拷贝。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg); // 创建
void* shmat(int shmid, const void* shmaddr, int shmflg); // 建立映射
int shmdt(const void *shmaddr); // 解除映射
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
int shmget(key_t key, size_t size, int shmflg);
key:共享内存在内核中的标识,其他进程通过相同的标识打开同一个内存(可由用户自己定义一个值)
size:当用shmget函数创建一段共享内存时,必须指定其size大小;而如果引用一个已存在的共享内存,则将size指定为0
shmflg:标志位,权限参数等
void* shmat(int shmid, const void* shmaddr, int shmflg);
shmid:操作句柄
shmaddr:指定地址,映射首地址,NULL则由系统自动分配
int shmdt(const void *shmaddr);
shmaddr:指定地址,映射首地址,NULL则由系统自动分配
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:操作句柄
cmd:具体操作,IPC_RMID删除共享内存,最后一块映射取消后删除
buf:属性设置等信息,NULL的不获取它
// 写入共享内存
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#define KEY 0x12345678
#define SIZE 2048
int main()
{
int shmid = shmget(KEY, SIZE, IPC_CREAT | 0664);
if (shmid < 0)
{
perror("shmget error");
return -1;
}
void *shm_start = shmat(shmid, NULL, 0);
if (shm_start == (void*)-1) // 失败返回-1类型强转
{
perror("shmat error");
return -1;
}
int i = 0;
while (1)
{
sprintf(shm_start, "%s-%d\n", "some words", i++);
sleep(1);
}
shmdt(shm_start);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
// 从共享内存中读取
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#define KEY 0x12345678
#define SIZE 2048
int main()
{
int shmid = shmget(KEY, SIZE, IPC_CREAT | 0664);
if (shmid < 0)
{
perror("shmget error");
return -1;
}
void *shm_start = shmat(shmid, NULL, 0);
if (shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
while (1)
{
printf("%s", (char*)shm_start);
sleep(1);
}
shmdt(shm_start);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
消息队列
消息队列的本质是内核中的一个优先级队列,多个进程通过向同一个队列中放置队列节点或获取节点实现通信。
消息队列自带同步与互斥。
传输有数据类型的数据块,数据不会粘连。
消息队列的生命周期随内核。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg); // 在内核中创建消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 向队列中添加节点
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 从队列中获取节点
int msgctl(int msqid, int cmd, struct msqid_ds *buf); // 删除消息队列
信号量
用于实现进程间的同步与互斥。
信号量的本质是内核中的一个原子操作(不可打断)的计数器,等待队列。
实现互斥:
通过一个只有0/1的计数器来实现。
通过一个状态标记临界资源的访问状态,对临界资源进行访问之前需要先判断这个标记,如果状态为可访问,那么将这个状态修改为不可访问然后去访问数据,当数据访问完毕后再将状态改为可访问状态。
实现同步:
通过一个计数判断,以及等待与唤醒功能实现。
通过一个计数器对资源数量进行计数,当想要获取临界资源的时候,先判断计数器,判断出是否有资源可供访问,如果有,则将计数器-1,然后获取一个资源进行操作,如果没有资源(计数<=0,小于0时数字的绝对值即为当前等待队列等待被唤醒的进程数量),则进行等待,等到其它进程释放资源产生数据计数器+1直到计数器>0,唤醒等待的进程。
关于ipc的命令
man ipcs 可以查看该命令的具体参数:
ipcs -h查看该命令的使用帮助:
ipcs -a 查看当前使用的共享内存、消息队列及信号量所有信息。
ipcs -q 查看当前活动的消息队列。
ipcs -m 查看当前活动的共享内存信息。
ipcs -s 查看当前活动的信号量信息。
ipcs -p 查看与共享内存、消息队列相关进程之间的消息。
ipcs -u 查看各个资源的使用总结信息,其中可以看到使用的信号量集的个数、信号量的个数,以及消息队列中当前使用的消息个数总数、占用的空间字节数。
ipcs -l 查看各个资源的系统限制信息,可以看到系统允许的最大信号量集及信号量个数限制、最大的消息队列中消息个数等信息。
man ipcrm 查看命令具体参数:
ipcrm -h 查看命令帮助:
ipcrm -q id 删除消息队列标识 id和其相关的消息队列和数据结构。
ipcrm -Q key 删除由关键字key创建的消息队列和其相关的消息队列和数据结构。
ipcrm -m id 删除共享内存标识。
ipcrm -M key 删除由关键字创建的共享内存标识。
ipcs -s id 删除信号标识符id和其相关的信号量集及数据结构。
ipcs -S key 删除由关键字key创建的信号量标识及其相关的信号量集及数据结构。
封装二元信号量P/V操作
PV操作百度百科
PV操作是与信号量的处理相关,其中P表示通过的意思,V表示释放的意思。这是狄克斯特拉用荷兰文定义的,因为在荷兰文中,通过叫passeren,释放叫vrijgeven,PV操作因此得名。
PV操作是典型的同步机制之一。用一个信号量与一个消息联系起来,当信号量的值为0时,表示期望的消息尚未产生;当信号量的值非0时,表示期望的消息已经存在。用PV操作实现进程同步时,调用P操作(sem_wait,计数器-1等待)测试消息是否到达,调用V操作(sem_post,计数器+1唤醒)发送消息。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
union semun {
int val; // 使用的值
struct semid_ds *buf; // 使用缓存区
unsigned short *arr; // 使用的数组
struct seminfo *_buf; // (Linux特有)使用缓存区
};
int _CreateSem(int nums, int flags)
{
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0)
{
perror("ftok error");
return -1;
}
int semid = semget(key, nums, flags);
if (semid < 0)
{
perror("semget error");
return -1;
}
return semid;
}
int CreateSem(int nums)
{
return _CreateSem(nums, IPC_CREAT);
}
int GetSem(int nums)
{
return CreateSem(nums);
}
int InitSem(int semid, int nums, int initval)
{
union semun un;
un.val = initval;
if (semctl(semid, nums, SETVAL, un) < 0) // SETVAL设置信号量集合中的信号量的计数值
{
perror("semctl error");
return -1;
}
return 0;
}
int _commPV(int semid, int who, int op)
{
struct sembuf sf;
sf.sem_num = who;
sf.sem_op = op;
sf.sem_flg = 0;
if (semop(semid, &sf, 1) < 0) // 在 Linux 下,PV 操作通过调用semop函数来实现
{
perror("semop error");
return -1;
}
return 0;
}
int P(int semid, int who)
{
return _commPV(semid, who, -1);
}
int V(int semid, int who)
{
return _commPV(semid, who, 1);
}
int DeatrocSem(int semid)
{
if (semctl(semid, 0, IPC_RMID) < 0)
{
perror("semctl remid error");
return -1;
}
return 0;
}
同步与互斥
临界资源
系统中同时存在许多进程,他们共享各种资源,有许多资源在某一时刻只能允许一个进程使用。例如打印机,磁带机等硬件设备和变量,队列等数据结构,如果有多个进程同时去使用这些资源就会造成混乱。这样的某段时间只能允许一个进程使用的资源为临界资源。
同步
同步是指通过一种条件的判断,来实现对于临界资源访问的时序和理性,实现有序访问,保证数据访问的合理性。
互斥
互斥是指同一时间内只能有一个执行流能够操作临界资源,各进程要求共享资源,但是有些资源需要互斥使用,因此各进程间竞争使用这些资源,是为了实现数据的安全操作,保证数据的访问、操作安全性。
生产者与消费者模型
生产者与消费者模型是经典的同步与互斥模型。
有两类进程分别为消费者进程和生产者进程,对同一个临界资源进行访问,生产者不断生产产品,并将产品作为资源加入缓冲区,而消费者不断的消费缓冲区中的资源,利用信号量实现两类进程的同步与互斥。
在生产者往缓冲区加入产品时,需要两个信号量,一个互斥信号量,保证在生产者生产产品的时候其他进程不能对临界资源进行操作,另一个信号量指示缓冲区是否已满,从而判断生产者能否继续往缓冲区加入产品;
消费者从缓冲区消耗资源时,也需要两个信号量,一个互斥信号量,保证消费者从缓冲区中取出产品时其他进程不能对临界资源进行操作,另外一个信号量指示缓冲区是否为空,从而判断消费者能否对缓冲区进行进一步操作。
于是本问题中共需要三个信号量,一个用于互斥的信号量;一个用于指示缓冲区空位置数目;一个是指示缓冲区已满。
用于同步的信号量一定位于不同进程中,即进行对其他进程的唤醒;用于互斥的信号量则一定位于同一个进程中,即互斥锁的使用。