linux_6.进程间通信
基本概念
进程:运行着的程序 进程间通信 :管道、信号量、共享内存、消息队列、套接字(网络编程) 临界资源:同一时刻,只允许一个进程(线程)访问的资源。 临界区:访问临界资源的代码段。 原子操作:不可被分割(中断)的操作
管道
1. 分类: 有名管道 无名管道 2. 管道的实现方式:通过头指针写,尾指针读 头指针到尾指针为管道现有数据 3. 有名和无名管道的区别:1.无名管道必须是父子进程之间才能使用,有名管道可在任意进程间使用 (tip:数据是在内存中存放,打开管道文件后,在内存中分配了一块空间,读写数据就是对内存进行操作) 4. 传输方式:管道是半双工(单工模式是指两台计算机传输信息时只准一台机子发送数据,另一台只准接受数据 ;半双工模式是指两台计算机传输信息时同一时间内只准一台计算机发送数据;全双工模式是指两台计算机传输信息时不论什么时间都可以任意传输数据) 5. 无名管道实现:fd[0]固定的读端 fd[1]固定的写端 pipe fork()之后文件描述符引用加 1 父进程关闭fd[0],子进程还可以使用 管道的写端彻底关闭,读端返回一个 0值 ; 管道的读端彻底关闭,写端引发异常,也就收到SIGPIPE 异常退出一般为信号,某个事件触发信号 6.管道特点:1. 写端关闭,读端停止返回0 2.两端任一端未运行,则另一段阻塞 7.文件操作 :1.打开文件 2.或读或写 3.关闭文件 1. int open(const char *pathname, in flags); 第一个参数为文件路径,第二个参数为O_RDONLY, O_WRONLY, or O_RDWR 2. int open(const char *pathname, int flags, mode_t mode); 前两个参数同上,第三个参数一般创建文件时需要O_CREAT, O_EXCL,O_NOCTTY, and O_TRUNC. 3. ssize_t read(int fd, void *buf, size_t count); 返回值为实际读到的字节数,fd为文件描述符,count为期望读到的字节数 4. ssize_t write(int fd, const void *buf, size_t count);返回值为实际写到的字节数,fd为文件描述符,count为期望读到的字节数 8.tips: mkfifo FIFO 创建管道文件 exit(0)为正常退出 exit(1)为非正常退出 fd文件描述符从下标最小未用的开始 0 标准输入 1 标准输出 2 标准错误输出 strlen()用来计算指定的字符串s 的长度,不包括结束字符"\0" length()包括结束字符"\0" 创建的管道文件放在磁盘中,打开管道文件的数据放在内存中,一旦关机,数据消失不保存 普通文件在磁盘中
代码实现:
- 无名管道
1 #include< stdio.h> 2 #include< string.h> 3 #include< stdlib.h> 4 #include< assert.h> 5 #include< unistd.h> 6 #include< fcntl.h> 7 8 int main() 9 { 10 int fd[2] ; 11 pipe(fd); 12 13 pid_t pid = fork(); //fork之后父子进程的读段和写段均可使用,最后只关闭一次,不能将父子进程所有都关闭,无法完全结束进程 14 15 if(pid == 0) 16 { 17 close(fd[1]); //不用写端,关闭写段 18 char buff[128]= {0}; 19 int n = 0; 20 while(n = read(fd[0],buff,127) > 0) 21 { 22 printf("child buff=%s",buff); 23 memset(buff,0,128); 24 } 25 close(fd[0]); //用完读端,再关闭读段 26 } 27 else 28 { 29 close(fd[0]); //不用读端,关闭读段 30 char buff[128] = {0}; 31 while(1) 32 { 33 sleep(1); 34 printf("input:"); 35 fgets(buff,128,stdin); 36 if(strncmp(buff,"end",3)==0) 37 { 38 break; 39 } 40 write(fd[1],buff,strlen(buff)); 41 } 42 close(fd[1]);//用完写端,关闭写段 43 } 44 }
- 有名管道
写端: 1 #include< stdio.h> 2 #include< string.h> 3 #include< stdlib.h> 4 #include< assert.h> 5 #include< unistd.h> 6 #include< fcntl.h> 7 8 int main() 9 { 10 int fd = open("fifo",O_WRONLY); 11 assert(fd != -1); 12 13 printf("fd=%d\n",fd); 14 while(1) 15 { 16 char buff[128]={0}; 17 printf("input:\n"); 18 19 fgets(buff,128,stdin); 20 21 if(strncmp(buff,"end",3)==0) 22 { 23 break; 24 } 25 write(fd,buff,strlen(buff)); 26 } 27 close(fd); 28 exit(0); 29 30 } 读端: 1 #include< stdio.h> 2 #include< string.h> 3 #include< stdlib.h> 4 #include< assert.h> 5 #include< unistd.h> 6 #include< fcntl.h> 7 8 int main() 9 { 10 int fd = open("fifo",O_RDONLY); 11 assert(fd != -1); 12 printf("fd=%d\n",fd); 13 char buff[128]={0}; 14 int n = 0; 15 while((n = read(fd,buff,127))>0) 16 { 17 printf("read:(n=%d):%s",n,buff); 18 memset(buff,0,128); 19 } 20 close(fd); 21 exit(0); 22 23 }
- 信号量
- 定义:信号量:特殊的变量,只能取正整数值,并且+1(v操作 )和-1(p操作)是原子操作,如果信号量为0,再进行-1操作时会阻塞。 作用:同步进程 方法:控制程序的推进速度
相关函数
<1 semget():全新创建一个信号量,或者获取一个已有的信号量 int semget(key_t key, int nsems, int semflg);第一个参数 key 是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有 semget 函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。 第二个参数 num_sems 指定需要的信号量数目,它的值几乎总是1。 第三个参数 sem_flags 是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值 IPC_CREAT 做按位或操作。设置了 IPC_CREAT 标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL 则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。 返回值:semget 函数成功返回一个相应信号标识符(非零),失败返回-1.
<2 semop():对信号量值进行修改 +1 -1 int semop(int semid, struct sembuf *sops, unsigned nsops);
第一个参数 sem_id 是由 semget 返回的信号量标识符; 第二个参数 sembuf结构的定义如下:
struct sembuf{ short sem_num;//除非使用一组信号量,否则它为0 short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作。 short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量 };
第三个参数默认值为 1
<3 semctl():对信号量做控制,为信号量赋初始值 int semctl(int semid, int semnum, int cmd, ...);
前两个参数与前面一个函数中的一样; 第三个参数 command通常是下面两个值中的其中一个 SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。 IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。 如果有第四个参数,它通常是一个union semum结构,定义如下: union semun{ int val; struct semid_ds *buf; unsigned short *arry; };
代码
a.c 1 #include< stdio.h> 2 #include< stdlib.h> 3 #include< unistd.h> 4 #include"sem.h" 5 6 int main() 7 { 8 sem_init(); 9 int i=0; 10 for(;i<10;i++) 11 { 12 sem_p(); 13 printf("a"); 14 fflush(stdout); 15 int n=rand()%3; 16 // sleep(n); 17 18 printf("a"); 19 fflush(stdout); 20 sem_v(); 21 sleep(n); 22 } 23 sem_destroy(); 24 } b.c 1 #include< stdio.h> 2 #include< stdlib.h> 3 #include< unistd.h> 4 #include"sem.h" 5 6 int main() 7 { 8 sem_init(); 9 int i=0; 10 for(;i<10;i++) 11 { 12 sem_p(); 13 printf("b"); 14 fflush(stdout); 15 int n=rand()%3; 16 //sleep(n); 17 18 printf("b"); 19 fflush(stdout); 20 sem_v(); 21 sleep(n); 22 } 23 sem_destroy(); 24 } sem.c 1 #include"sem.h" 2 3 static int semid = 0; 4 void sem_init() 5 { 6 semid = semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600); 7 if(semid == -1) 8 { 9 semid = semget((key_t)1234,1,IPC_CREAT|0600); 10 if(semid == -1) 11 { 12 perror("semget error"); 13 } 14 } 15 else 16 { 17 union semun a; 18 a.val = 1; 19 if(semctl(semid,0,SETVAL,a) == -1) 20 { 21 perror("semctl error"); 22 23 } 24 } 25 } 26 27 void sem_p() 28 { 29 struct sembuf buf; 30 buf.sem_num = 0; 31 buf.sem_op = -1; 32 buf.sem_flg = SEM_UNDO; 33 34 if(semop(semid,&buf,1)==-1) 35 { 36 perror("p error"); 37 } 38 } 40 void sem_v() 41 { 42 struct sembuf buf; 43 buf.sem_num = 0; 44 buf.sem_op = 1; 45 buf.sem_flg = SEM_UNDO; 46 47 if(semop(semid,&buf,1)==-1) 48 { 49 perror("v error"); 50 } 51 } 52 void sem_destroy() 53 { 54 55 if(semctl(semid,0,IPC_RMID) == -1) 56 { 57 perror("destroy error"); 58 } 59 } sem.h 1 #include< stdio.h> 2 #include< stdlib.h> 3 #include< string.h> 4 #include< unistd.h> 5 #include< sys/sem.h> 6 7 union semun 8 { 9 int val; 10 }; 11 void sem_init(); 12 void sem_p(); 13 void sem_v(); 14 void sem_destroy();
- 共享内存
- 实现原理:共享内存区域说白了就是多个进程共享的一块物理内存地址。假设有10个进程将这块区域映射到自己的虚拟地址上,那么,这10个进程间就可以相互通信。由于是同一块区域在10个进程的虚拟地址上,当第一个进程向这块共享内存的虚拟地址中写入数据时,其他9个进程也都会看到。因此共享内存是进程间通信的一种最快的方式。
相关函数
<1 int shmget(key_t key, size_t size, int shmflg); 创建共享内存空间,或者获取已有的一块共享内存空间 第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值) 第二个参数,size以字节为单位指定需要共享的内存容量 第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。 返回值:shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1. <2 void* shmat(int shmid, const void *shmaddr, int shmflg); 把共享内存映射到当前的进程的虚拟地址空间中 第一个参数,shm_id是由shmget函数返回的共享内存标识。 第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。 第三个参数,shm_flg是一组标志位,通常为0。 调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1. <3 int shmdt(const void *shmaddr); 断开映射 参数shmaddr是shmat函数返回的地址指针 返回值:调用成功时返回0,失败时返回-1. <4 int shmctl(int shmid, int cmd, struct shmid_ds *buf); 移除共享内存空间 第一个参数,shm_id是shmget函数返回的共享内存标识符。 第二个参数,command是要采取的操作,它可以取下面的三个值 : 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; };
优点:
采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。而对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。 一般而言,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时在重新建立共享内存区域;而是保持共享区域,直到通信完毕为止。 这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件,因此,采用共享内存的通信方式效率非常高。
代码实现
Main.c 的代码: #include< stdio.h> #include< sys/shm.h> #include< string.h> #include< assert.h #include< stdlib.h> void main() { int shmid = shmget((key_t)1234, 256, IPC_CREAT|0600); assert(shmid != -1); char *s = (char *)shmat(shmid, NULL, 0); assert((int)s != -1); strcpy(s, "hello"); shmdt(s); } Test.c 代码: #include < stdio.h> #include < sys/shm.h> #include < string.h> #include < assert.h> #include < stdlib.h> void main() { int shmid = shmget((key_t)1234, 256, IPC_CREAT|0600); assert(shmid != -1); char *s = (char *)shmat(shmid, NULL, 0); assert((int)s != -1); printf(“s = %s\n”, s); shmdt(s); }
tips: 共享内存没有解决同步问题,如果写成循环输入输出,需要考虑同步问题
模型类似生产者、消费者 信号量:S1(0) S2(1) A B pS2 pS1 fgets(s) printf(buff) strcpy(buff,s) vS1 vS2
- 消息队列
定义
消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。优点
消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。主要函数
<1 int msgget(key_t, key, int msgflg); 第一个参数,与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。 第二个参数,msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。 返回值:它返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1. <2 int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg); 第一个参数,msgid是由msgget函数返回的消息队列标识符。 第二个参数,msg_ptr是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样: struct my_message{ long int message_type; /* The data you wish to transfer*/ }; 第三个参数,msg_sz是msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。 第四个参数,msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。 返回值:如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1. <3 int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg); 前三个参数同上 第四个参数,msgtype可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。 第五个参数,msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。 返回值:调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1. <4 int msgctl(int msgid, int command, struct msgid_ds *buf); 第一个参数,msgid是由msgget函数返回的消息队列标识符。 第二个参数,command是将要采取的动作,它可以取3个值, IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。 IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值 IPC_RMID:删除消息队列 第三个参数,buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员: struct msgid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; }; 返回值:成功时返回0,失败时返回-1.
代码
接收信息的程序源文件为msgreceive.c的源代码为: #include < unistd.h> #include < stdlib.h> #include < stdio.h> #include < string.h> #include < errno.h> #include < sys/msg.h> struct msg_st { long int msg_type; char text[BUFSIZ]; }; int main() { int running = 1; int msgid = -1; struct msg_st data; long int msgtype = 0; //注意1 //建立消息队列 msgid = msgget((key_t)1234, 0666 | IPC_CREAT); if(msgid == -1) { fprintf(stderr, "msgget failed with error: %d\n", errno); exit(EXIT_FAILURE); } //从队列中获取消息,直到遇到end消息为止 while(running) { if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1) { fprintf(stderr, "msgrcv failed with errno: %d\n", errno); exit(EXIT_FAILURE); } printf("You wrote: %s\n",data.text); //遇到end结束 if(strncmp(data.text, "end", 3) == 0) running = 0; } //删除消息队列 if(msgctl(msgid, IPC_RMID, 0) == -1) { fprintf(stderr, "msgctl(IPC_RMID) failed\n"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); } 发送信息的程序的源文件msgsend.c的源代码为: #include < unistd.h> #include < stdlib.h> #include < stdio.h> #include < string.h> #include < sys/msg.h> #include < errno.h> #define MAX_TEXT 512 struct msg_st { long int msg_type; char text[MAX_TEXT]; }; int main() { int running = 1; struct msg_st data; char buffer[BUFSIZ]; int msgid = -1; //建立消息队列 msgid = msgget((key_t)1234, 0666 | IPC_CREAT); if(msgid == -1) { fprintf(stderr, "msgget failed with error: %d\n", errno); exit(EXIT_FAILURE); } //向消息队列中写消息,直到写入end while(running) { //输入数据 printf("Enter some text: "); fgets(buffer, BUFSIZ, stdin); data.msg_type = 1; //注意2 strcpy(data.text, buffer); //向队列发送数据 if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1) { fprintf(stderr, "msgsnd failed\n"); exit(EXIT_FAILURE); } //输入end结束输入 if(strncmp(buffer, "end", 3) == 0) running = 0; sleep(1); } exit(EXIT_SUCCESS); }
tips:部分见博客 http://blog.csdn.net/ljianhui/article/details/10287879