Linux进程间通信之管道与消息队列实践
2021-01-09 20:42:57 阿炯
本文摘抄自杨源鑫的个人博客,文章较为详细地总结上目前Linux下进程间通信方式,包括本地的管道套接字与跨主机消息队列相关实践,理论说明与代码齐上阵,同时也有较为全面的分析说明,非常感谢原作者。
第1节概述进程间通信的主要方式及其演变的过程
1、进程间通信简述
进程间通信的几种方式:无名管道、有名管道、消息队列、共享内存、信号、信号量、套接字(socket)。
进程间通信是不同进程直接进行的一些接触,这种接触有简单,有复杂。机制不同,复杂度也不同。通信是一个广义上的意义,不仅指大批量数据传送,还包括控制信息的传送,但是使用的方法都是大同小异的。
如图所示进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。
2、管道
管道分为无名管道和有名管道两种方式。管道是一种半双工的通信方式,数据只能单向流动,但是无名管道和有名管道的区别是无名管道只能在具有亲缘关系的进程间通信,有名管道则是在无亲缘关系进程间通信。进程的亲缘关系通常是指父子进程关系。管道是Linux支持的最初Unix IPC形式之一,管道与管道之间通信其实就是一个文件,但它不是一个普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统而且只存在内存中。当一个进程向管道中写的内容被管道另一端的进程读出;写入的内容每次都会被添加到管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。如下图所示。
那么,如何创建一条管道呢?下面,我们就来了解下FIFO函数。
FIFO不同于pipe函数,因为它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径就能够彼此通过FIFO互相通信,因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进后出,和栈的原则一样,对管道以及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
需要包含的头文件如下:
#include<sys/types.h>
#include<sys/stat.h>
#incldue<fcntl.h>
#include<unistd.h>
FIFO函数创建:
函数原型:
int mkfifo(const char *pathname,mode_t mode);
函数返回值 :
成功0,失败-1
参数含义:
pathname为路径名,创建管道的名字(该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字)。mode为创建fifo的权限(第二个参数与打开普通文件的open()函数中的mode参数相同)。
注:如果mkfido的第一个参数已经是一个已经存在的路径名时,就会返回EEXIST错误,所以当我们调用的时候首先会检查是否返回该错误,如果返回该错误那我们只需要直接调用打开FIFO的函数即可。
FIFO比pipe函数打开的时候多了一个打开操作open;如果当时打开操作时为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将返回成功;否则,可能阻塞到有相应进程为写而打开该FIFO;或者,成功返回。另一种情况就是为写而打开FIFO时,若已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则可能会阻塞直到有相应进程为读而打开该FIFO;或者,返回ENIO错误。
下面我们使用FIFO实现进程间的通信。
(1)打开一个文件,管道的写入端向文件写入数据;管道的读取端从文件中读取出数据。
fifo_write.c
#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define P_FIFO "txt"
int main(){
int fd;
//要写入有名管道的数据
char buf[20] = "hello write_fifo";
int ret=0;
//创建有名管道,并赋予访问有名管道的权限
ret = mkfifo(P_FIFO,0777);
//创建失败
if(ret < 0){
printf("create named pipe failed.\n");
return -1;
}
fd = open(P_FIFO,O_WRONLY);
if(fd < 0){
printf("open failed.\n");
return -2;
}
//写入数据到有名管道
//第一个参数为有名管道文件描述符
//第二个参数为写入有名管道的数据
//第三个参数为写入有名管道的数据长度
write(fd,buf,sizeof(buf));
//关闭有名管道
close(fd);
return 0;
}
fifo_read.c
#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define P_FIFO "txt"
int main(){
int ret;
int fd;
char buf[20];
//打开有名管道
//第一个参数为有名管道文件路径
//第二个参数表明是以读取方式并以非阻塞方式打开有名管道
//O_RDONLY读取模式
//O_NONBLOCK非阻塞方式
fd = open(P_FIFO,O_RDONLY);
if(fd<0){
printf("open fail\n");
return -1 ;
}
//循环读取有名管道
while(1){
memset(buf,0,sizeof(buf));
if(read(fd,buf,sizeof(buf)) == 0){
printf("nodata.\n");
}
else{
printf("getdata:%s\n",buf);
break;
}
}
close(fd);
return 0;
}
下面先将fifo_write.c和fifo_read.c分别编译成fifo_write和fifo_read两个可执行程序:
接下来,先运行fifo_write,然后打开另一个终端,接着运行fifo_read,运行fifo_write的时候,可以看到程序阻塞在终端:
下面打开另外一个终端运行fifo_read
切换到另外一个终端,在终端输入ls –l可以看到由于fifo_write中创建了管道文件txt,从前面的字串prwxr-xr-x中的p可以知道,这是一个管道文件,如下图所示:
运行fifo_read,这时候,可以看到从管道中获取的字符串hello write_fifo,如下图所示:
管道读取结束后,fifo_write这个程序也就不会在阻塞在终端了,如下图所示:
写管道程序还要注意,一旦我们创建了FIFO,就可以用open去打开它,可以使用open、read、close等去操作FIFO和pipe有相同之处,当打开FIFO时,非阻塞标志(O_NONBLOCK)将会对读写产生如下影响:
1)、没有使用O_NONBLOCK:访问要求无法满足时进程将阻塞。如试图读取空的FIFO,将导致进程阻塞;
2)、使用O_NONBLOCK:访问要求无法满足时不阻塞,立即出错返回,errno是ENXIO。
3、消息队列
消息队列(也叫做报文队列)提供了一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
打开或者创建消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需要提供该消息队列的键值即可。
消息读写操作非常简单,对于开发人员来说,每个消息都类似如下的数据结构:
struct msgbuf{
long mtype;
char mtext[1];
};
3.1、msgget函数
该函数用来创建或者访问一个消息队列。
int msgget(key_t key, int msgflg);
与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,成功则返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1。
3.2、msgsnd函数
该函数用来向消息队列发送一个消息。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
将发送的消息存储在msgp指向的msgbuf结构中,消息大小由msgsz指定。对发送的消息来说,有意义的msgflg标准为IPC_NOWAIT,指明在消息队列没有足够的空间容纳要发送的消息时,msgsnd是否等待。造成msgsnd()等待的条件有两种:当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;当前消息队列的消息数不小于消息队列的总容量,此时,虽然消息队列中的消息数目并不多,但基本上都只有一个字节。调用成功的时候返回0,失败返回-1。
3.3、msgrcv函数
该函数用来从一个消息队列获取消息。
int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgrcv函数前面三个参数和msgsnd函数的三个参数一样不做讲解。msgtype可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。当调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中对应的消息;失败则返回-1。
3.4、msgctl函数
该函数用来控制消息队列。
int msgctl(int msgid, int command, struct msgid_ds *buf);
该系统调用对由msqid标识的消息队列执行cmd操作,共有三种cmd操作:
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值。
IPC_RMID:删除消息队列。buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。成功返回0,否则返回-1。
通过上面的函数我们清楚如何去创建一个消息队列那我们简单的来看一个案例。
(1)创建一条消息队列msg_get.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
//创建消息队列,注意,创建后面要有IPC_CREAT标志
msgid = msgget(0x123456 , IPC_CREAT | 0777);
if(msgid < 0){
perror("msgget fail");
return -1 ;
}
printf("success ... ! \n");
return 0 ;
}
运行结果:
那消息队列呢?怎么查看?使用ipcs –q命令可以查看到刚刚我们创建的消息队列0x123456。
(2)向消息队列发送消息 msgsend.c
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
msgid = msgget(0x123456 , 0);
if(msgid == -1){
perror("create msg queue fail");
return -1 ;
}
printf("open msg success ... \n");
int ret ;
char *p = "hello world" ;
//发送hello world到消息队列0x123456
//在这里可以直接发送
ret = msgsnd(msgid , p , strlen(p) , 0);
if(ret == -1){
perror("send msgid fail");
return -2 ;
}
return 0 ;
}
运行结果:
使用ipcs –p命令查看:
(3)获取消息队列中的信息 msgrecv.c 在上面msgsend.c的基础上,这个例程将上面发送到消息队列的信息读取回来。
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
msgid = msgget(0x123456 , 0);
if(msgid == -1){
perror("create msg queue fail");
return -1 ;
}
printf("open msg success ... \n");
int ret ;
char buffer[1024] = {0};
//接收消息队列中的信息
ret = msgrcv(msgid , buffer , 11 , 0 , 0);
if(ret == -1){
perror("recv msgid fail");
return -2 ;
}
printf("ret: %d buffer:%s \n" , ret , buffer);
return 0 ;
}
运行结果,如图所示:
那么,如何删除一个消息队列呢?先用ipcs –q查看消息队列,如图所示:
有两种方法:
1)、使用命令ipcrm –q msqid 删除消息队列,如图所示
2)、使用msgctl函数,写IPC_RMID标志删除消息队列
(4)删除消息队列 msgrm.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
msgid = msgget(0x123456 , 0);
if(msgid < 0){
perror("msgget fail");
return -1 ;
}
printf("success ... ! msgid:%d \n" , msgid);
//写IPC_RMID标志
if(msgctl(msgid , IPC_RMID , NULL) == 0){
printf("remove success ... \n");
}
return 0 ;
}
运行结果,如图所示:
使用系统提供的API的方式,可以将消息队列删除。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,相对于管道通信有很大的改观,而且消息队列对数据的顺序处理也是非常有条理性的不会产生混杂性。
第2节我们就来分享一下Linux的另外两种进程间通信的方式:信号、信号量。
1、信号
我们使用过windows的都知道,当一个程序被卡死的时候不管怎样都没反应,这样我们就可以打开任务管理器直接强制性的结束这个进程,这个方法的实现就是和Linux上通过生成信号和捕获信号来实现相似的,运行过程中进程捕获到这些信号做出相应的操作使最终被终止。
信号的主要来源是分为两部分,一部分是硬件来源,一部分是软件来源;进程在实际中可以用三种方式来响应一个信号:一是忽略信号,不对信号做任何操作,其中有两个信号是不能别忽略的分别是SIGKILL和SIGSTOP。二是捕捉信号,定义信号处理函数,当信号来到时做出响应的处理。三是执行缺省操作,Linux对每种信号都规定了默认操作。注意,进程对实时信号的缺省反应是立即终止。
发送信号的函数有很多,主要使用的有:kill()、raise()、abort()、alarm()。
先来熟悉下kill函数,进程可以通过kill()函数向包括它本身在内的其它进程发送一个信号,如果程序没有发送这个信号的权限,对kill函数的调用将会失败,失败的原因通常是由于目标进程由另一个用户所拥有。
kill函数的原型为:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
它的作用是把信号sig发送给进程号为pid的进程,成功时返回0。kill调用失败返回-1,调用失败通常有三大原因:
1、给定的信号无效
2、发送权限不够
3、目标进程不存在
还有一个非常重要的函数,信号处理signal函数。程序可以用signal函数来处理指定的信号,主要通过恢复和忽略默认行为来操作。signal函数原型如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
我们来看一个例程了解一下signal函数。signal.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
//函数ouch对通过参数sig传递进来的信号作出响应。
void ouch(int sig){
printf("signal %d\n", sig);
//恢复终端中断信号SIGINT的默认行为
(void) signal(SIGINT, SIG_DFL);
}
int main(){
//改变终端中断信号SIGINT的默认行为,使之执行ouch函数
(void) signal(SIGINT, ouch);
while(1){
printf("Hello World!\n");
sleep(1);
}
return 0;
}
运行结果:
可以看出当我按下ctrl+c的时候并不会退出,只有当再次按下ctrl+c的时候才会退出。造成的原因是因为SIGINT的默认行为被signal函数改变了,当进程接受到信号SIGINT时,它就去调用函数ouch去处理,注意ouch函数把信号SIGINT的处理方式改变成默认的方式,所以当你再按一次ctrl+c时,进程就像之前那样被终止了。
下面是几种常见的信号:
SIGHUP :从终端上发出的结束信号
SIGINT :来自键盘的中断信号 ( ctrl + c )
SIGKILL :该信号结束接收信号的进程
SIGTERM:kill 命令发出的信号pcpmq22.pnghttp://
SIGCHLD:标识子进程停止或结束的信号
SIGSTOP:来自键盘 ( ctrl + z ) 或调试程序的停止执行信号。
信号发送主要函数有kill和raise。上面我们知道kill函数的用法也清楚kill函数是可以向自身发送信号和其它进程发送信号,raise与之不同的是只可以向本身发送信号。
通过raise函数向自身发送数据,使子进程暂停通过测试如下: raise.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(){
pid_t pid;
int ret;
if((pid=fork())<0){
printf("Fork error\n");
exit(1);
}
//子进程
if(pid==0){
//在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停
printf("I am child pid:%d.I am waiting for any signal\n",getpid());
raise(SIGSTOP);
printf("I am child pid:%d.I am killed by progress:%d\n",getpid(),getppid());
exit(0);
}
//父进程
else{
sleep(2);
//在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作
if((waitpid(pid,NULL,WNOHANG))==0) {
//若pid指向的子进程没有退出,则返回0,且父进程不阻塞,继续执行下边的语句
if((ret=kill(pid,SIGKILL))==0){
printf("I am parent pid:%d.I am kill %d\n",getpid(),pid);
}
}
//等待子进程退出,否则就一直阻塞
waitpid(pid,NULL,0);
exit(0);
}
}
当调用raise的时候子进程就会暂停:
信号是对终端机的一种模拟,也是一种异步通信方式。
2、信号量
主要作为进程间,以及同一进程不同线程之间的同步手段。信号量是用来解决进程之间的同步与互斥问题的一种进程之间的通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作。信号量对应于某一种资源,取一个非负的整形值。信号量的值是指当前可用的资源数量。
由于信号量只有两种操作,一种是等待信号,另一种是发送信号。即P和V,它们的行为如下:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
Linux特别提供了一组信号量接口来对信号操作,它们不只是局限的针对二进制信号量,下面我们来对每个函数介绍,需要注意的是这些函数都是用来成对组的信号量值进行操作的。
2.1、semget函数
它的作用是创建一个新信号量或取得一个已有信号量。
int semget(key_t key, int nsems, int semflg);
第一个参数是key整数型,不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
第二个参数是制定需要的信号数量,通常情况下为1。
第三个参数是一组标志位,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semget函数成功返回一个相应信号标识符(非零),失败返回-1。
2.2、semop函数
它的作用是改变信号量的值。
int semop(int semid, struct sembuf *sops, unsigned nsops);
sops是一个指针,它指向这样一个数组:元素用来描述对semid代表的信号量集合中第几个信号进行怎么样的操作。nops规定该数组中操作的数量。
semop函数返回0表示成功,返回-1表示失败。
2.3、semctl函数
该函数用来直接控制信号量信息。
int semctl(int semid, int semnum, int cmd, …);
semget并不会初始化每个信号量的值,这个初始化必须通过SETVAL命令或SETALL命令调用semctl来完成。
例程:semctl.c
#include <stdio.h>
#include <linux/sem.h>
#define NUMS 10
int get_sem_val(int sid,int semnum){//取得当前信号量
return(semctl(sid,semnum,GETVAL,0));
}
int main(void){
int I ;
int sem_id;
int pid;
int ret;
struct sembuf sem_op;//信号集结构
union semun sem_val;//信号量数值
//建立信号量集,其中只有一个信号量
sem_id = semget(IPC_PRIVATE,1,IPC_CREAT|0600);
//IPC_PRIVATE私有,只有本用户使用,如果为正整数,则为公共的;1为信号集的数量;
if (sem_id==-1){
printf("create sem error!\n");
exit(1);
}
printf("create %d sem success!\n",sem_id);
//信号量初始化
sem_val.val=1;
//设置信号量,0为第一个信号量,1为第二个信号量,...以此类推;SETVAL表示设置
ret = semctl(sem_id,0,SETVAL,sem_val);
if (ret < 0){
printf("initlize sem error!\n");
exit(1);
}
//创建进程
pid = fork();
if (pid < 0){
printf("fork error!\n");
exit(1);
}
else if(pid == 0){
//一个子进程,使用者
for ( i=0;i<NUMS;i++){
sem_op.sem_num=0;
sem_op.sem_op=-1;
sem_op.sem_flg=0;
semop(sem_id,&sem_op,1);//操作信号量,每次-1
printf("%d 使用者: %d\n",i,get_sem_val(sem_id,0));
}
}
else{
//父进程,制造者
for (i=0;i<NUMS;i++){
sem_op.sem_num=0;
sem_op.sem_op=1;
sem_op.sem_flg=0;
semop(sem_id,&sem_op,1);//操作信号量,每次+1
printf("%d 制造者: %d\n",i,get_sem_val(sem_id,0));
}
}
exit(0);
}
运行结果:
信号量的出现就是保证资源在一个时刻只能有一个进程(线程),所以例子当中只有制造者在制造(+1操作)过程中,使用者这个进程是无法随sem_id进行操作的。也就是说信号量是协调进程对共享资源操作的,起到了类似互斥锁的作用,但却比锁拥有更强大的功能。
第3节我们就来分享一下Linux的最后一种进程间通信的方式:共享内存。
3、什么是共享内存
共享内存就是两个不相关的进程之间可以直接访问同一段内存,共享内存在两个正在运行的进程之间共享和传递数据起到了非常有效的方式。在不同的进程之间共享的内存通常安排为同一段物理内存,进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以直接访问共享内存中的地址。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程;其实就是映射一段能够被其它内存所访问到的内存,这段内存由一个进程创建,但是多个进程都可以去访问。共享内存是最快的IPC方式,它是通过其它通信方式的效率不足而专门设计的。往往都是和其它通信机制配合使用,来实现进程间的同步和通信。
共享内存的使用和信号量其实也是差不多的,都是使用接口的形式,共享内存的接口比信号量的接口更加的简单,我们一起去了解下共享内存的使用。
共享内存函数由shmget、shmat、shmdt、shmctl四个函数组成。我们下面来分析每一个函数的用法。
3.1、创建共享内存
int shmget(key_t key, size_t size, int shmflg);
第一个参数是共享内存段的命名,shmget成功时返回一个关于key相关的标识符,用于后续的共享内存函数。当调用失败返回-1。其它进程也可以通过shmget函数返回值访问同一个共享内存。第二个参数是指定共享内存的容量;第三个shmflg是一个权限标志,它的作用和open和mode函数都是相同的,当共享内存不存在的时候则通过IPC_CREAT来创建。共享内存的权限标准和文件读写的权限一样。
3.2、启动对共享内存的访问
void *shmat(int shm_id, const void *shm_addr, int shmflg);
当我们第一次创建完共享内存时,它还不能被任何进程访问,shmat函数就是用来启动对共享内存的访问,并把共享内存连接到当前进程的地址空间。
shm_id是由shmget函数返回的共享内存标识;shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。最后一个参数是标志位通常都是0。调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1。
3.3、共享内存从当前内存中分离
int shmdt(const void *shmaddr);
这个函数只是从共享内存中分离而不是删除,这一点要分清楚,对于初学者而言这里很容易掉坑,使共享内存在当前进程中不可再用。
参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1。
3.4、控制共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);
第一个参数是shaget函数返回的共享内存标识符;command参数是要采取的操作,它由 IPC_STAT、IPC_SET和IPC_RMID组成,分别IPC_STAT代表把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值;IPC_SET代表如果进程有足够的权限,就可以把共享内存的当前关联值设置为shmid_ds结构中给出的值;IPC_RMID代表删除共享内存段。第三个参数buf代表一个结构指针,它指向共享内存的模式或访问权限的结构。
shmid_ds结构至少包括以下成员:
struct shmid_ds{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
3.6、共享内存案例
shm_snd.c
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main(){
int shmid;
char *shmptr;
//创建共享内存
shmid = shmget(0x66, SHM_SIZE, IPC_CREAT|0666);
//创建失败
if(shmid < 0){
perror("shmget");
return -1 ;
}
//对共享内存的访问
shmptr = shmat(shmid, 0, 0);
if (shmptr == (void *)-1){
perror("shmat");
return -2 ;
}
// 往共享内存写数据
strcpy(shmptr, "shmat write ok");
shmdt(shmptr);
return 0 ;
}
shm_rcv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main(){
int shmid;
char *shmptr;
shmid = shmget(0x66, SHM_SIZE, IPC_CREAT|0666);
if(shmid < 0){
perror("shmget");
return -1 ;
}
shmptr = shmat(shmid, 0, 0);
if (shmptr == (void *)-1){
perror("shmat");
return -2 ;
}
// 从共享内存读数据
printf("read:%s\n", shmptr);
shmdt(shmptr);
return 0 ;
}
运行结果:
先分别编译shm_snd.c和shmrcv.c这两个程序,生成shmrcv和shmsnd这两个可执行程序。
接下来,首先执行shmsnd,会得到以下结果:
什么都没有?共享内存创建成功了吗?当然是成功了,可以通过ipcs –m命令查看:
如图上图所示,nattch项下的数字为0那个就是刚刚使用shmsnd这个可执行程序创建的一段共享内存。当然,我们还往共享内存发了shmat write ok这个字符串,下面运行shmrcv这个程序,看看是否能把写进共享内存的数据读出来。
成功读出。同样的,也可以删除共享内存,如何删除?也一样有两种方法。
(1)使用ipcrm –m shmid可以删除共享内存
如上图,我们已经知道0x66的shmid为1835021,所以只要执行ipcrm –m 1835021命令即可删除,如下图所示,成功删除。
(2)使用shmctl 函数写入IPC_RMID指令删除共享内存
shmrm.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(void){
int shmid ;
//同样,首先先打开共享内存
shmid = shmget(0x66 , 0 , 0);
if(-1 == shmid){
perror("open shmkey 0x66 fail");
return -1 ;
}
//成功的话,向shmctl写入参数,IPC_RMID表示立刻删除,后面的参数被忽略,为0
int ret ;
//写入的是参数
ret = shmctl(shmid , IPC_RMID , NULL);
if(ret < 0){
perror("remove shm fail");
return -2 ;
}
printf("remove key:%d success ... \n" , 0x66);
return 0 ;
}
运行结果:
Linux系统中的进程间通信方式主要以下几种:
同一主机上的进程通信方式:
* UNIX 进程间通信方式:包括管道 (PIPE), 有名管道 (FIFO), 和信号 (Signal)
* System V 进程通信方式:包括信号量 (Semaphore), 消息队列 (Message Queue), 和共享内存 (Shared Memory)
网络主机间的进程通信方式:
* RPC: Remote Procedure Call 远程过程调用
* Socket: 当前最流行的网络通信方式,基于 TCP/IP 协议的通信方式.
各自的特点如下:
管道 (PIPE):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系 (父子进程) 的进程间使用。另外管道传送的是无格式的字节流,并且管道缓冲区的大小是有限的(管道缓冲区存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
有名管道 (FIFO): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号 (Signal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
信号量 (Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列 (Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存 (Shared Memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
套接字 (Socket):套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。
线程间通信方式主要以下几种:
*锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法;读写锁允许多个线程同时读共享数据,而对写操作是互斥的;条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
*信号量机制(Semaphore):包括无名线程信号量和命名线程信号量。
*信号机制(Signal):类似进程间的信号处理。
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
第1节概述进程间通信的主要方式及其演变的过程
1、进程间通信简述
进程间通信的几种方式:无名管道、有名管道、消息队列、共享内存、信号、信号量、套接字(socket)。
进程间通信是不同进程直接进行的一些接触,这种接触有简单,有复杂。机制不同,复杂度也不同。通信是一个广义上的意义,不仅指大批量数据传送,还包括控制信息的传送,但是使用的方法都是大同小异的。
如图所示进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。
2、管道
管道分为无名管道和有名管道两种方式。管道是一种半双工的通信方式,数据只能单向流动,但是无名管道和有名管道的区别是无名管道只能在具有亲缘关系的进程间通信,有名管道则是在无亲缘关系进程间通信。进程的亲缘关系通常是指父子进程关系。管道是Linux支持的最初Unix IPC形式之一,管道与管道之间通信其实就是一个文件,但它不是一个普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统而且只存在内存中。当一个进程向管道中写的内容被管道另一端的进程读出;写入的内容每次都会被添加到管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。如下图所示。
那么,如何创建一条管道呢?下面,我们就来了解下FIFO函数。
FIFO不同于pipe函数,因为它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径就能够彼此通过FIFO互相通信,因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进后出,和栈的原则一样,对管道以及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
需要包含的头文件如下:
#include<sys/types.h>
#include<sys/stat.h>
#incldue<fcntl.h>
#include<unistd.h>
FIFO函数创建:
函数原型:
int mkfifo(const char *pathname,mode_t mode);
函数返回值 :
成功0,失败-1
参数含义:
pathname为路径名,创建管道的名字(该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字)。mode为创建fifo的权限(第二个参数与打开普通文件的open()函数中的mode参数相同)。
注:如果mkfido的第一个参数已经是一个已经存在的路径名时,就会返回EEXIST错误,所以当我们调用的时候首先会检查是否返回该错误,如果返回该错误那我们只需要直接调用打开FIFO的函数即可。
FIFO比pipe函数打开的时候多了一个打开操作open;如果当时打开操作时为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将返回成功;否则,可能阻塞到有相应进程为写而打开该FIFO;或者,成功返回。另一种情况就是为写而打开FIFO时,若已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则可能会阻塞直到有相应进程为读而打开该FIFO;或者,返回ENIO错误。
下面我们使用FIFO实现进程间的通信。
(1)打开一个文件,管道的写入端向文件写入数据;管道的读取端从文件中读取出数据。
fifo_write.c
#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define P_FIFO "txt"
int main(){
int fd;
//要写入有名管道的数据
char buf[20] = "hello write_fifo";
int ret=0;
//创建有名管道,并赋予访问有名管道的权限
ret = mkfifo(P_FIFO,0777);
//创建失败
if(ret < 0){
printf("create named pipe failed.\n");
return -1;
}
fd = open(P_FIFO,O_WRONLY);
if(fd < 0){
printf("open failed.\n");
return -2;
}
//写入数据到有名管道
//第一个参数为有名管道文件描述符
//第二个参数为写入有名管道的数据
//第三个参数为写入有名管道的数据长度
write(fd,buf,sizeof(buf));
//关闭有名管道
close(fd);
return 0;
}
fifo_read.c
#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define P_FIFO "txt"
int main(){
int ret;
int fd;
char buf[20];
//打开有名管道
//第一个参数为有名管道文件路径
//第二个参数表明是以读取方式并以非阻塞方式打开有名管道
//O_RDONLY读取模式
//O_NONBLOCK非阻塞方式
fd = open(P_FIFO,O_RDONLY);
if(fd<0){
printf("open fail\n");
return -1 ;
}
//循环读取有名管道
while(1){
memset(buf,0,sizeof(buf));
if(read(fd,buf,sizeof(buf)) == 0){
printf("nodata.\n");
}
else{
printf("getdata:%s\n",buf);
break;
}
}
close(fd);
return 0;
}
下面先将fifo_write.c和fifo_read.c分别编译成fifo_write和fifo_read两个可执行程序:
接下来,先运行fifo_write,然后打开另一个终端,接着运行fifo_read,运行fifo_write的时候,可以看到程序阻塞在终端:
下面打开另外一个终端运行fifo_read
切换到另外一个终端,在终端输入ls –l可以看到由于fifo_write中创建了管道文件txt,从前面的字串prwxr-xr-x中的p可以知道,这是一个管道文件,如下图所示:
运行fifo_read,这时候,可以看到从管道中获取的字符串hello write_fifo,如下图所示:
管道读取结束后,fifo_write这个程序也就不会在阻塞在终端了,如下图所示:
写管道程序还要注意,一旦我们创建了FIFO,就可以用open去打开它,可以使用open、read、close等去操作FIFO和pipe有相同之处,当打开FIFO时,非阻塞标志(O_NONBLOCK)将会对读写产生如下影响:
1)、没有使用O_NONBLOCK:访问要求无法满足时进程将阻塞。如试图读取空的FIFO,将导致进程阻塞;
2)、使用O_NONBLOCK:访问要求无法满足时不阻塞,立即出错返回,errno是ENXIO。
3、消息队列
消息队列(也叫做报文队列)提供了一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
打开或者创建消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需要提供该消息队列的键值即可。
消息读写操作非常简单,对于开发人员来说,每个消息都类似如下的数据结构:
struct msgbuf{
long mtype;
char mtext[1];
};
3.1、msgget函数
该函数用来创建或者访问一个消息队列。
int msgget(key_t key, int msgflg);
与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,成功则返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1。
3.2、msgsnd函数
该函数用来向消息队列发送一个消息。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
将发送的消息存储在msgp指向的msgbuf结构中,消息大小由msgsz指定。对发送的消息来说,有意义的msgflg标准为IPC_NOWAIT,指明在消息队列没有足够的空间容纳要发送的消息时,msgsnd是否等待。造成msgsnd()等待的条件有两种:当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;当前消息队列的消息数不小于消息队列的总容量,此时,虽然消息队列中的消息数目并不多,但基本上都只有一个字节。调用成功的时候返回0,失败返回-1。
3.3、msgrcv函数
该函数用来从一个消息队列获取消息。
int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgrcv函数前面三个参数和msgsnd函数的三个参数一样不做讲解。msgtype可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。当调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中对应的消息;失败则返回-1。
3.4、msgctl函数
该函数用来控制消息队列。
int msgctl(int msgid, int command, struct msgid_ds *buf);
该系统调用对由msqid标识的消息队列执行cmd操作,共有三种cmd操作:
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值。
IPC_RMID:删除消息队列。buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。成功返回0,否则返回-1。
通过上面的函数我们清楚如何去创建一个消息队列那我们简单的来看一个案例。
(1)创建一条消息队列msg_get.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
//创建消息队列,注意,创建后面要有IPC_CREAT标志
msgid = msgget(0x123456 , IPC_CREAT | 0777);
if(msgid < 0){
perror("msgget fail");
return -1 ;
}
printf("success ... ! \n");
return 0 ;
}
运行结果:
那消息队列呢?怎么查看?使用ipcs –q命令可以查看到刚刚我们创建的消息队列0x123456。
(2)向消息队列发送消息 msgsend.c
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
msgid = msgget(0x123456 , 0);
if(msgid == -1){
perror("create msg queue fail");
return -1 ;
}
printf("open msg success ... \n");
int ret ;
char *p = "hello world" ;
//发送hello world到消息队列0x123456
//在这里可以直接发送
ret = msgsnd(msgid , p , strlen(p) , 0);
if(ret == -1){
perror("send msgid fail");
return -2 ;
}
return 0 ;
}
运行结果:
使用ipcs –p命令查看:
(3)获取消息队列中的信息 msgrecv.c 在上面msgsend.c的基础上,这个例程将上面发送到消息队列的信息读取回来。
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
msgid = msgget(0x123456 , 0);
if(msgid == -1){
perror("create msg queue fail");
return -1 ;
}
printf("open msg success ... \n");
int ret ;
char buffer[1024] = {0};
//接收消息队列中的信息
ret = msgrcv(msgid , buffer , 11 , 0 , 0);
if(ret == -1){
perror("recv msgid fail");
return -2 ;
}
printf("ret: %d buffer:%s \n" , ret , buffer);
return 0 ;
}
运行结果,如图所示:
那么,如何删除一个消息队列呢?先用ipcs –q查看消息队列,如图所示:
有两种方法:
1)、使用命令ipcrm –q msqid 删除消息队列,如图所示
2)、使用msgctl函数,写IPC_RMID标志删除消息队列
(4)删除消息队列 msgrm.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void){
int msgid ;
msgid = msgget(0x123456 , 0);
if(msgid < 0){
perror("msgget fail");
return -1 ;
}
printf("success ... ! msgid:%d \n" , msgid);
//写IPC_RMID标志
if(msgctl(msgid , IPC_RMID , NULL) == 0){
printf("remove success ... \n");
}
return 0 ;
}
运行结果,如图所示:
使用系统提供的API的方式,可以将消息队列删除。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,相对于管道通信有很大的改观,而且消息队列对数据的顺序处理也是非常有条理性的不会产生混杂性。
第2节我们就来分享一下Linux的另外两种进程间通信的方式:信号、信号量。
1、信号
我们使用过windows的都知道,当一个程序被卡死的时候不管怎样都没反应,这样我们就可以打开任务管理器直接强制性的结束这个进程,这个方法的实现就是和Linux上通过生成信号和捕获信号来实现相似的,运行过程中进程捕获到这些信号做出相应的操作使最终被终止。
信号的主要来源是分为两部分,一部分是硬件来源,一部分是软件来源;进程在实际中可以用三种方式来响应一个信号:一是忽略信号,不对信号做任何操作,其中有两个信号是不能别忽略的分别是SIGKILL和SIGSTOP。二是捕捉信号,定义信号处理函数,当信号来到时做出响应的处理。三是执行缺省操作,Linux对每种信号都规定了默认操作。注意,进程对实时信号的缺省反应是立即终止。
发送信号的函数有很多,主要使用的有:kill()、raise()、abort()、alarm()。
先来熟悉下kill函数,进程可以通过kill()函数向包括它本身在内的其它进程发送一个信号,如果程序没有发送这个信号的权限,对kill函数的调用将会失败,失败的原因通常是由于目标进程由另一个用户所拥有。
kill函数的原型为:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
它的作用是把信号sig发送给进程号为pid的进程,成功时返回0。kill调用失败返回-1,调用失败通常有三大原因:
1、给定的信号无效
2、发送权限不够
3、目标进程不存在
还有一个非常重要的函数,信号处理signal函数。程序可以用signal函数来处理指定的信号,主要通过恢复和忽略默认行为来操作。signal函数原型如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
我们来看一个例程了解一下signal函数。signal.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
//函数ouch对通过参数sig传递进来的信号作出响应。
void ouch(int sig){
printf("signal %d\n", sig);
//恢复终端中断信号SIGINT的默认行为
(void) signal(SIGINT, SIG_DFL);
}
int main(){
//改变终端中断信号SIGINT的默认行为,使之执行ouch函数
(void) signal(SIGINT, ouch);
while(1){
printf("Hello World!\n");
sleep(1);
}
return 0;
}
运行结果:
可以看出当我按下ctrl+c的时候并不会退出,只有当再次按下ctrl+c的时候才会退出。造成的原因是因为SIGINT的默认行为被signal函数改变了,当进程接受到信号SIGINT时,它就去调用函数ouch去处理,注意ouch函数把信号SIGINT的处理方式改变成默认的方式,所以当你再按一次ctrl+c时,进程就像之前那样被终止了。
下面是几种常见的信号:
SIGHUP :从终端上发出的结束信号
SIGINT :来自键盘的中断信号 ( ctrl + c )
SIGKILL :该信号结束接收信号的进程
SIGTERM:kill 命令发出的信号pcpmq22.pnghttp://
SIGCHLD:标识子进程停止或结束的信号
SIGSTOP:来自键盘 ( ctrl + z ) 或调试程序的停止执行信号。
信号发送主要函数有kill和raise。上面我们知道kill函数的用法也清楚kill函数是可以向自身发送信号和其它进程发送信号,raise与之不同的是只可以向本身发送信号。
通过raise函数向自身发送数据,使子进程暂停通过测试如下: raise.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(){
pid_t pid;
int ret;
if((pid=fork())<0){
printf("Fork error\n");
exit(1);
}
//子进程
if(pid==0){
//在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停
printf("I am child pid:%d.I am waiting for any signal\n",getpid());
raise(SIGSTOP);
printf("I am child pid:%d.I am killed by progress:%d\n",getpid(),getppid());
exit(0);
}
//父进程
else{
sleep(2);
//在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作
if((waitpid(pid,NULL,WNOHANG))==0) {
//若pid指向的子进程没有退出,则返回0,且父进程不阻塞,继续执行下边的语句
if((ret=kill(pid,SIGKILL))==0){
printf("I am parent pid:%d.I am kill %d\n",getpid(),pid);
}
}
//等待子进程退出,否则就一直阻塞
waitpid(pid,NULL,0);
exit(0);
}
}
当调用raise的时候子进程就会暂停:
信号是对终端机的一种模拟,也是一种异步通信方式。
2、信号量
主要作为进程间,以及同一进程不同线程之间的同步手段。信号量是用来解决进程之间的同步与互斥问题的一种进程之间的通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作。信号量对应于某一种资源,取一个非负的整形值。信号量的值是指当前可用的资源数量。
由于信号量只有两种操作,一种是等待信号,另一种是发送信号。即P和V,它们的行为如下:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
Linux特别提供了一组信号量接口来对信号操作,它们不只是局限的针对二进制信号量,下面我们来对每个函数介绍,需要注意的是这些函数都是用来成对组的信号量值进行操作的。
2.1、semget函数
它的作用是创建一个新信号量或取得一个已有信号量。
int semget(key_t key, int nsems, int semflg);
第一个参数是key整数型,不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
第二个参数是制定需要的信号数量,通常情况下为1。
第三个参数是一组标志位,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semget函数成功返回一个相应信号标识符(非零),失败返回-1。
2.2、semop函数
它的作用是改变信号量的值。
int semop(int semid, struct sembuf *sops, unsigned nsops);
sops是一个指针,它指向这样一个数组:元素用来描述对semid代表的信号量集合中第几个信号进行怎么样的操作。nops规定该数组中操作的数量。
semop函数返回0表示成功,返回-1表示失败。
2.3、semctl函数
该函数用来直接控制信号量信息。
int semctl(int semid, int semnum, int cmd, …);
semget并不会初始化每个信号量的值,这个初始化必须通过SETVAL命令或SETALL命令调用semctl来完成。
例程:semctl.c
#include <stdio.h>
#include <linux/sem.h>
#define NUMS 10
int get_sem_val(int sid,int semnum){//取得当前信号量
return(semctl(sid,semnum,GETVAL,0));
}
int main(void){
int I ;
int sem_id;
int pid;
int ret;
struct sembuf sem_op;//信号集结构
union semun sem_val;//信号量数值
//建立信号量集,其中只有一个信号量
sem_id = semget(IPC_PRIVATE,1,IPC_CREAT|0600);
//IPC_PRIVATE私有,只有本用户使用,如果为正整数,则为公共的;1为信号集的数量;
if (sem_id==-1){
printf("create sem error!\n");
exit(1);
}
printf("create %d sem success!\n",sem_id);
//信号量初始化
sem_val.val=1;
//设置信号量,0为第一个信号量,1为第二个信号量,...以此类推;SETVAL表示设置
ret = semctl(sem_id,0,SETVAL,sem_val);
if (ret < 0){
printf("initlize sem error!\n");
exit(1);
}
//创建进程
pid = fork();
if (pid < 0){
printf("fork error!\n");
exit(1);
}
else if(pid == 0){
//一个子进程,使用者
for ( i=0;i<NUMS;i++){
sem_op.sem_num=0;
sem_op.sem_op=-1;
sem_op.sem_flg=0;
semop(sem_id,&sem_op,1);//操作信号量,每次-1
printf("%d 使用者: %d\n",i,get_sem_val(sem_id,0));
}
}
else{
//父进程,制造者
for (i=0;i<NUMS;i++){
sem_op.sem_num=0;
sem_op.sem_op=1;
sem_op.sem_flg=0;
semop(sem_id,&sem_op,1);//操作信号量,每次+1
printf("%d 制造者: %d\n",i,get_sem_val(sem_id,0));
}
}
exit(0);
}
运行结果:
信号量的出现就是保证资源在一个时刻只能有一个进程(线程),所以例子当中只有制造者在制造(+1操作)过程中,使用者这个进程是无法随sem_id进行操作的。也就是说信号量是协调进程对共享资源操作的,起到了类似互斥锁的作用,但却比锁拥有更强大的功能。
第3节我们就来分享一下Linux的最后一种进程间通信的方式:共享内存。
3、什么是共享内存
共享内存就是两个不相关的进程之间可以直接访问同一段内存,共享内存在两个正在运行的进程之间共享和传递数据起到了非常有效的方式。在不同的进程之间共享的内存通常安排为同一段物理内存,进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以直接访问共享内存中的地址。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程;其实就是映射一段能够被其它内存所访问到的内存,这段内存由一个进程创建,但是多个进程都可以去访问。共享内存是最快的IPC方式,它是通过其它通信方式的效率不足而专门设计的。往往都是和其它通信机制配合使用,来实现进程间的同步和通信。
共享内存的使用和信号量其实也是差不多的,都是使用接口的形式,共享内存的接口比信号量的接口更加的简单,我们一起去了解下共享内存的使用。
共享内存函数由shmget、shmat、shmdt、shmctl四个函数组成。我们下面来分析每一个函数的用法。
3.1、创建共享内存
int shmget(key_t key, size_t size, int shmflg);
第一个参数是共享内存段的命名,shmget成功时返回一个关于key相关的标识符,用于后续的共享内存函数。当调用失败返回-1。其它进程也可以通过shmget函数返回值访问同一个共享内存。第二个参数是指定共享内存的容量;第三个shmflg是一个权限标志,它的作用和open和mode函数都是相同的,当共享内存不存在的时候则通过IPC_CREAT来创建。共享内存的权限标准和文件读写的权限一样。
3.2、启动对共享内存的访问
void *shmat(int shm_id, const void *shm_addr, int shmflg);
当我们第一次创建完共享内存时,它还不能被任何进程访问,shmat函数就是用来启动对共享内存的访问,并把共享内存连接到当前进程的地址空间。
shm_id是由shmget函数返回的共享内存标识;shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。最后一个参数是标志位通常都是0。调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1。
3.3、共享内存从当前内存中分离
int shmdt(const void *shmaddr);
这个函数只是从共享内存中分离而不是删除,这一点要分清楚,对于初学者而言这里很容易掉坑,使共享内存在当前进程中不可再用。
参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1。
3.4、控制共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);
第一个参数是shaget函数返回的共享内存标识符;command参数是要采取的操作,它由 IPC_STAT、IPC_SET和IPC_RMID组成,分别IPC_STAT代表把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值;IPC_SET代表如果进程有足够的权限,就可以把共享内存的当前关联值设置为shmid_ds结构中给出的值;IPC_RMID代表删除共享内存段。第三个参数buf代表一个结构指针,它指向共享内存的模式或访问权限的结构。
shmid_ds结构至少包括以下成员:
struct shmid_ds{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
3.6、共享内存案例
shm_snd.c
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main(){
int shmid;
char *shmptr;
//创建共享内存
shmid = shmget(0x66, SHM_SIZE, IPC_CREAT|0666);
//创建失败
if(shmid < 0){
perror("shmget");
return -1 ;
}
//对共享内存的访问
shmptr = shmat(shmid, 0, 0);
if (shmptr == (void *)-1){
perror("shmat");
return -2 ;
}
// 往共享内存写数据
strcpy(shmptr, "shmat write ok");
shmdt(shmptr);
return 0 ;
}
shm_rcv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main(){
int shmid;
char *shmptr;
shmid = shmget(0x66, SHM_SIZE, IPC_CREAT|0666);
if(shmid < 0){
perror("shmget");
return -1 ;
}
shmptr = shmat(shmid, 0, 0);
if (shmptr == (void *)-1){
perror("shmat");
return -2 ;
}
// 从共享内存读数据
printf("read:%s\n", shmptr);
shmdt(shmptr);
return 0 ;
}
运行结果:
先分别编译shm_snd.c和shmrcv.c这两个程序,生成shmrcv和shmsnd这两个可执行程序。
接下来,首先执行shmsnd,会得到以下结果:
什么都没有?共享内存创建成功了吗?当然是成功了,可以通过ipcs –m命令查看:
如图上图所示,nattch项下的数字为0那个就是刚刚使用shmsnd这个可执行程序创建的一段共享内存。当然,我们还往共享内存发了shmat write ok这个字符串,下面运行shmrcv这个程序,看看是否能把写进共享内存的数据读出来。
成功读出。同样的,也可以删除共享内存,如何删除?也一样有两种方法。
(1)使用ipcrm –m shmid可以删除共享内存
如上图,我们已经知道0x66的shmid为1835021,所以只要执行ipcrm –m 1835021命令即可删除,如下图所示,成功删除。
(2)使用shmctl 函数写入IPC_RMID指令删除共享内存
shmrm.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(void){
int shmid ;
//同样,首先先打开共享内存
shmid = shmget(0x66 , 0 , 0);
if(-1 == shmid){
perror("open shmkey 0x66 fail");
return -1 ;
}
//成功的话,向shmctl写入参数,IPC_RMID表示立刻删除,后面的参数被忽略,为0
int ret ;
//写入的是参数
ret = shmctl(shmid , IPC_RMID , NULL);
if(ret < 0){
perror("remove shm fail");
return -2 ;
}
printf("remove key:%d success ... \n" , 0x66);
return 0 ;
}
运行结果:
Linux系统中的进程间通信方式主要以下几种:
同一主机上的进程通信方式:
* UNIX 进程间通信方式:包括管道 (PIPE), 有名管道 (FIFO), 和信号 (Signal)
* System V 进程通信方式:包括信号量 (Semaphore), 消息队列 (Message Queue), 和共享内存 (Shared Memory)
网络主机间的进程通信方式:
* RPC: Remote Procedure Call 远程过程调用
* Socket: 当前最流行的网络通信方式,基于 TCP/IP 协议的通信方式.
各自的特点如下:
管道 (PIPE):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系 (父子进程) 的进程间使用。另外管道传送的是无格式的字节流,并且管道缓冲区的大小是有限的(管道缓冲区存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
有名管道 (FIFO): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号 (Signal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
信号量 (Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列 (Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存 (Shared Memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
套接字 (Socket):套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。
线程间通信方式主要以下几种:
*锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法;读写锁允许多个线程同时读共享数据,而对写操作是互斥的;条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
*信号量机制(Semaphore):包括无名线程信号量和命名线程信号量。
*信号机制(Signal):类似进程间的信号处理。
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。