进程之间的通信(System V IPC)
在系统运行的过程中,常常需要让多个进程互相通信。实现这样功能的方式有很多种,例如文件锁和管道,这里介绍一组由Linux系统提供的机制,称为System V IPC
。这一组机制中包含了三种方式,他们分别是信号量,共享内存和消息队列。
信号量
信号量用来实现系统资源的互斥访问,这里讲的是进程之间的资源互斥访问,线程之间的互斥访问不能使用这样的方式,关于如何使用线程之间的信号量,可以看[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pZE6rmyb-1614935877834)(https://blog.youkuaiyun.com/tusdddd/article/details/112838425)]。
Linux提供了一组函数用来对信号量进行操作,因为这里的信号量是用来实现一组进程之间的相互交互,所有所创建的信号量将由系统进行管理,而不从属于任何一个进程,这样就可以让这个信号量可以被多个进程访问。同时系统中的信号量资源是有限的,所以在使用信号量的时候应该节约,并且使用完之后要进行释放,否则会导致程序下次运行的时候混乱。系统提供的函数如下所示:
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
使用信号量之前,需要先用semget()
创建一个信号量,并且用sem_ctl()
对信号量进行设置,之后就可以正式使用信号量了,在使用的过程中使用semop()
对信号量的数值进行加减,使用完成之后需要再次用sem_ctl()
删除信号量。下面是对这三个函数的介绍:
-
semget()
:创建一个新的信号量或取得一个已有的信号量,第一个参数key
表示这个信号量的键值,这个键值在系统中是唯一的,如果多个程序中都用这个函数获取键值相同的信号量,则这些程序得到的实际上是同一个信号量。这个函数会返回一个信号量标识符,所以这个函数的用法跟fopen()
差不多,若函数执行失败则返回-1
。num_sems
表示要获取的信号量的个数,几乎总是取1
。sem_flags
是一组标志,它与open()
函数的标志非常相似,起作用类似于文件的访问权限。此外,这个参数还可以与IPC_CREAT
与操作来创建一个新的信号量,即使给予的键值key
已经存在,也不会出现错误。更进一步,可以使用sem_flags
跟IPC_CREAT
和IPC_EXCL
与操作来创建一个新的唯一的信号量。 -
semop()
:用于该表信号量的值,sem_id
是semget()
返回的信号量标识符,第二个参数sem_ops
结构体至少包含三个成员:short sem_num;short sem_op;short sem_flg;
。sem_num
是信号量的编号,当使用一组信号量的时候才有用,否则一般是0
。sem_op
表示本次操作改变信号量的值,通常将其设置为1
表示V操作,-1
表示P操作。sem_flg
通常被设置为SEM_UNDO
,这样设置之后,系统就会自动追踪该信号量,即使程序退出的时候没有释放信号量,系统也会将这个信号量释放。该函数执行失败时返回-1
。最后一个参数表示第二个参数中sembuf
结构体的个数,只使用一个信号量的时候设置成1
即可。 -
semctl()
:用来直接控制信号量信息。第一个参数sem_id
是semget()
函数返回的信号量标识符。第二个参数sem_num
是信号量编号,只有使用一组信号量的时候才使用,否则一般设置成0
。第三个参数command
是将要采取的动作。如果还有第四个参数,它将会是一个union,它至少包含三个成员int val
,struct semid_ds *buf
,unsigned short * array
,根据第四个参数的取值这个参数取其中的一种类型。第四个参数有两个最常用的值,SETVAL
表示将信号量设置成command
中val
的值,IPC_RMID
用于删除一个信号量标识符,注意不是删除信号量,相当于fclose()
。
下面是一个例子,这个例子使用了这一套机制实现了PV操作,从而使两个进程互斥进入临界区。这两个进程是同一个程序的两个实例,程序会将参数作为分隔符以区分两个实例:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/sem.h>
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
int sem_id; //打开信号量之后的信号量标识符
int set_semvalue(void); //初始化信号量为零
void del_semvalue(void); //删除信号量标识符
int semaphore_p(void); //实现P操作
int semaphore_v(void); //实现V操作
int main(int argc, char *argv[]){
int pause_time; //进程在临界区停留的时间
char op_char; //表示进程进入临界区的分隔符
srand((unsigned int)getpid());
if(argc < 2){
fprintf(stderr, "Please enter a delimiter\n");
exit(EXIT_FAILURE);
}
op_char = *argv[1];
//创建或打开一个信号量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if(!set_semvalue()){
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
//让另一个进程也打开信号量
sleep(2);
//进入和离开临界区10次,每次进入和离开都会打印分隔符
for(int i = 0; i < 10; i++){
//进入临界区
if(!semaphore_p()) exit(EXIT_FAILURE);
printf("%c", op_char); fflush(stdout); //一定要fflush(),否则打印会存在终端缓存中,从而导致显示异常
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char); fflush(stdout);
//退出临界区
if(!semaphore_v()) exit(EXIT_FAILURE);
pause_time = rand() % 3;
sleep(pause_time);
}
printf("\n%d - finished\n", getpid());
del_semvalue();
exit(EXIT_SUCCESS);
}
int set_semvalue(void){
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1) return 0;
return 1;
}
void del_semvalue(void){
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphor\n");
}
int semaphore_p(void){
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1){
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
int semaphore_v(void){
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1){
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
共享内存
上面的信号量机制只是用来让不同的进程同时互斥访问资源,而这里的共享内存则用来不同进程之间互相传输大块的数据。共享内存顾名思义就是让几个进程都同时有权利访问同一块内存块,这样就可以实现多个进程之间传输数据。与管道机制不同的是,共享内存在使用的时候系统不会对多个同时访问内存的进程进行协调,所以在使用的时候需要额外注意,否则将导致数据错乱,一般在使用的时候都是用共享内存来进行数据的传输,而使用一些消息来进行内存访问的同步。
在Linux中,提供了一组函数来实现共享内存,它们的使用跟IPC信号量机制提供的函数相似:
#include <sys/shm.h>
void * shmat(int shm_id, const void * shm_addr, int shmflg);
int shmctl(int shm_id, int cmd, struct shmid_ds * buf);
int shmdt(const void * shm_addr);
int shmget(key_t key, size_t size, int shmflg);
在使用共享内存之前,需要先用shmget()
创建一块共享内存,创建成功之后还不能立即使用,还必须用shmat()
将这个共享内存连接到本进程的逻辑地址中,之后才能正常使用,使用完成之后,需要用shmdt()
将共享内存与程序的逻辑地址断开,但是这个时候这块共享内存还没有被释放,只有使用shmctl()
才能将共享内存块彻底删除。
下面是对这些函数的解释:
-
shmget()
:用来请求系统分配一块共享内存,第一个参数key
是这块内存在系统中的序号,如果在多个程序中用这个函数请求分配key
值相同的共享内存,那么这几个程序就会共享同一块内存。第二个参数size
是请求的共享内存的大小,以字节为单位。第三个参数shmflg
是这块内存的访问权限,它的取值规则跟文件的访问权限规则一样,由IPC——CREAT
定义的一个特殊比特必须和权限标志按位或才能创建一个新的内存共享块。该函数返回一个共享内存标识符,失败则返回-1
。 -
shmat()
:用来将系统已经分配哈的共享内存连接到程序的逻辑地址中。第一个参数shm_id
是由shmget()
返回的共享内存标识符。第二个参数*shm_addr
只有在需要让共享内存连接到指定地址时才使用,一般我们让系统为我们决定地址,所以一般为空指针。第三个参数shmflg
有两种取值SHM_RND
(与shm_addr
联合使用指定共享内存连接地址)和SHM_RDONLY
(使连接的内存只读)。函数执行成功时,返回指向共享内存块第一个字节的指针,否则返回-1
。 -
shmdt()
:用来断开程序与共享内存的连接,但是这一块共享内存并没有被释放,而是交给系统管理。 -
shmctl()
:用来对共享内存进行设置,第一个参数shm_id
是由shmget()
返回的标识符。第二个参数command
有3个取值,分别为IPC_STAT
(把当前共享内存的属性存入shmid_ds
中),IPC_SET
(如果进程有权限,将当前共享内存的状态设置成shmid_ds
)和PIC_RMID
(删除共享内存块)。第三个参数shmid_ds
是一个结构体,至少包含三个成员uid_t shm_perm.uid
,uid_t shm_perm.uid
和shm_perm.mode
。
下面是一个例子:
===========================shm_com.h=========================
#define TEXT_SZ 2048
struct shared_use_st{
int written_by_you; //表示当前状态是否已经被写入新数据
char some_text[TEXT_SZ]; //写入的数据
};
=============================================================
======================shm_consumer.c=========================
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include "shm_com.h"
int main(void){
int running = 1; //循环判断标志
void * shared_memory = (void*)0; //用来指向共享内存
struct shared_use_st *shared_stuff; //也用来指向共享内存
int shmid; //共享内存标识符
srand((unsigned int)getpid());
//向系统申请共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if(shmid == -1){
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
//将共享内存与程序的逻辑地址连接,并返回指向这块内存第一字节的指针
shared_memory = shmat(shmid, (void*)0, 0);
if(shared_memory == -1){
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %X\n", (int)shared_memory);
shared_stuff = (struct shared_use_st *)shared_memory;
shared_stuff->written_by_you = 0;
while(running){
if(shared_stuff->written_by_you){
printf("You wrote: %s", shared_stuff->some_text);
sleep(rand() % 4); //让生产者有时间再次写入数据
shared_stuff->written_by_you = 0; //数据已经被读取,将标志再次设为0
//当读取的单词为end时,退出循环
if(strncmp(shared_stuff->some_text, "end", 3) == 0){
running = 0;
}
}
}
//分离内存
if(shmdt(shared_memory) == -1){
fprintf(stderr, "shamdt failed\n");
exit(EXIT_FAILURE);
}
//删除内存
if(shmctl(shmid, IPC_RMID, 0) == -1){
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
=============================================================
======================shm_producer.c=========================
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include "shm_com.h"
int main(void){
int shmid; //共享内存标识符
struct shared_use_st * shared_stuff; //用来指向共享内
void * shared_memory; //也用来指向共享内存
char buffer[BUFSIZ];
int running = 1; //循环判断标志
//用shmget()获取标识符
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if(shmid == -1){
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
//将共享内存连接到程序
shared_memory = shmat(shmid, (void *)0, 0);
if(shared_memory == -1){
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
shared_stuff = (struct shared_use_st*)shared_memory;
printf("Memory attached at %X\n", (int)shared_memory);
while(running){
//当标志为1时,表示消费者还没有读取程序,所以等待
while(shared_stuff->written_by_you == 1){
sleep(1);
printf("waiting for client...\n");
}
//消费者读取完毕,开始写入新数据
printf("Enter text: ");
fgets(buffer, BUFSIZ, stdin);
strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
shared_stuff->written_by_you = 1;
if(strncmp(shared_stuff->some_text, "end", 3) == 0){
running = 0;
}
}
//分离内存
if(shmdt(shared_memory) == -1){
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
=============================================================
消息队列
这是IPC
三个机制中的最后一个,这个机制会创建一个消息队列,然后不同的进程通过这个消息队列互相通信。与管道一样,消息队列每次发送一个数据块的长度是有限制的,并且系统中所有队列所包含的全部数据块的总长度也有限制。这个两个限制分别由Linux中的宏MSGMAX
和MSGMNB
确定。
用于实现消息队列的函数有:
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
int msgrcv(int msgid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
int msgctl(int msgid, int cmd, struct msgid_ds *buf);
与IPC其他机制的操作类似,在使用消息队列之前,要先使用msgget()
创建一个消息队列,并且创建方式跟之前的IPC
共享内存机制创建方式也类似,创建之后就可以使用msgrcv()
和msgsnd()
收发消息,使用完成之后用msgctl()
删除消息队列。下面是对这几个函数的解释:
-
msgget()
:这个函数与其他IPC机制创建函数一样,用一个序列号key
作为参数来作为消息队列的名字,msgflg
是访问权限,如果要创建一个新的消息队列,就需要将这个访问权限跟IPC_CREAT
按位或操作。这个函数也返回消息队列的标识符,失败时返回-1
。 -
msgsnd()
:用于向消息队列中发送消息,其中msgid
是由msgget()
返回的标识符,msg_ptr
指向要发送的数据,这个数据一般定义为一个结构体,并且结构体的第一个成员必须是long int
类型,这个成员会被用来表示消息的类型。msg_sz
表示发送消息的长度,这个长度不包括第一个long int
成员的长度。msgflg
可以为IPC_NOWAIT
,这时如果不能向消息队列中发送消息则会放弃发送这个消息立即返回并且函数的返回值为-1
。函数执行成功时返回0
,失败时返回-1
。 -
msgrcv()
:用于从消息队列中取出消息,其中msgid
是由msgget()
返回的标识符,msg_ptr
指向要存放消息的内存,该内存的安排必须跟前面msgsnd()
中msg_ptr
一样,第一个成员是long int
类型。第三个参数msg_sz
表示消息的长度,这个长度同样不包括第一个long int
类型成语的长度。第四个参数msgflg
表示要接收的消息类型,取0
时接受所有消息类型,取正数时只接受该类型的消息,而取负数时之接受类型号小于等于这个负数的绝对值的消息。第五个参数msgflg
作用跟msgsnd()
中的msgflg
一样。函数执行成功时返回接收缓存区中的字节数
,失败时返回-1
。 -
msgctl()
:这个函数的使用方式跟共享内存的设置函数shmctl()
一样。函数执行成功时返回0
,失败时返回-1
。
下面是一个例子:
===================msg_receive.c======================
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/msg.h>
//用来存放消息的结构体
struct my_msg_st{
long int my_msg_type;
char some_text[BUFSIZ];
};
int main(void){
int running = 1; //控制是否继续接受消息
int msgid; //消息队列标识符
struct my_msg_st some_data; //用于存放消息的缓存区
long int msg_to_receive = 0; //指明要接受的消息类型
//建立消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1){
fprintf(stderr, "msgget failed with errno %d\n", errno);
exit(EXIT_FAILURE);
}
//从消息队列中获取消息,知道见到end为止
while(running){
if(msgrcv(msgid, (void*)&some_data, BUFSIZ, msg_to_receive, 0) == -1){
fprintf(stderr, "msgrcv failed with errno %d\n", errno);
exit(EXIT_FAILURE);
}
printf("You wrote: %s\n", some_data.some_text);
if(strncmp("end", some_data.some_text, 3) == 0){
running = 0;
}
}
//删除消息队列
if(msgctl(msgid, IPC_RMID, 0) == -1){
fprintf(stderr, "msgctl failed with errno %d\n", errno);
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
======================================================
=====================msg_send.c=======================
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/msg.h>
#define MAX_TEXT 52
//用来存放消息的结构体
struct my_msg_st{
long int my_msg_type;
char some_text[BUFSIZ];
};
int main(void){
int running = 1; //控制是否继续接受消息
int msgid; //消息队列标识符
struct my_msg_st some_data; //发送消息的结构
char buffer[BUFSIZ]; //用于缓存消息的数据体
//打开消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1){
fprintf(stderr, "msgget failed with errno %d\n", errno);
exit(EXIT_FAILURE);
}
//从消息队列中获取消息,知道见到end为止
while(running){
//处理消息结构的数据
printf("Enter some text:");
fgets(buffer, BUFSIZ, stdin);
some_data.my_msg_type = 1;
strcpy(some_data.some_text, buffer);
if(msgsnd(msgid, (void *)&some_data, MAX_TEXT, 0) == -1){
fprintf(stderr, "smgsnd failed\n");
exit(EXIT_FAILURE);
}
if(strncmp("end", buffer, 3) == 0){
running = 0;
}
}
exit(EXIT_SUCCESS);
}
======================================================
可以看出上面的例子中不需要自己手动对两个进程进行同步,这也是消息队列相较于管道机制的优势。