进程间通信
Posted 杨静远
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程间通信相关的知识,希望对你有一定的参考价值。
进程间通信大类上可以分为两类,分别是管道和System V IPC。这两个大类可以继续细分,管道分为半双工管道和全双工管道,全双工是最近出现的技术,只是半双工的一种补充,在有些系统中是不被支持的,因此,在管道通信中最常见的就是匿名半双工通道和FIFO两种。System V IPC包括三种进程通信方式,即消息队列、信号量和共享内存,这也是一种比较古老的方式,最近的版本中已经逐渐被POSIX IPC取代,但是两者之间有着密切的联系,实现的道理是一样的。
- 管道
管道通信是最常见的通信方式,它实现数据以一种数据流的方式,在多进程间流动。主要介绍两种半双工管道的原理和操作。
管道在系统中相当于文件系统中的一个文件,来缓存所要传输的数据。在某些特性上不如文件,比如当数据读出后,管道中就没有数据了。可以理解为管道是一种特殊的文件。
(1)匿名半双工管道
匿名管道没有名字,对于管道中使用的文件描述符没有路径名,不存在任何意义上的文件,只是在内存中跟某一个索引节点相关联的两个文件描述符,主要特性如下:
*数据只能在一个方向上移动
*只能在公共祖先的进程间通信,即父子进程或者兄弟进程。
创建使用管道有两种方式,第一种使用pipe函数创建匿名半双工管道,通过read函数和write函数进行读写操作。第二种是用标准库函数进行管道创建和操作。
第一种方式:
创建管道函数 int pipe(fd[2]);:
a.参数说明:fd[2]是长度为2的文件描述数组,fd[0]端是读出端,fd[1]端是写入端。
b.返回值:0表示成功,-1表示失败。
c.函数作用:函数成功返回,自动维护了一个从fd[1]到fd[0]的数据通道
写入数据函数 write(fd[1], ”……”, n);
a.参数说明:fd[1]代表的是管道的写入端,”……”代表要写入的数据内容,n代表的是写入内容的长度,单位是字节。
b.返回值:0表示成功,-1表示失败。
c.函数作用:将长度为n的数据“……”写入管道的写入端。
读出数据函数 read(fd[0], buf, BUFSZ);
a.参数说明:fd[0]代表管道的读出端,buf是一个空的字符串,代表将管道中数据读出后存放的位置,BUFSZ代表管道默认一次性读写的数据长度。
b.返回值:0表示成功,-1表示失败。
c.函数说明:将管道中的数据读出存放到字符串数组buf中。
示例代码(父子进程之间通过管道进行通信):
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <limits.h> #include <sys/types.h> #define BUFSZ PIPE_BUF /* PIPE_BUF管道默认一次性读写的数据长度*/ int main ( void ) { int fd[2]; char buf[BUFSZ]; pid_t pid; ssize_t len; if ( (pipe(fd)) < 0 ){ /*创建管道*/ perror ( "failed to pipe" ); exit( 1 ); } if ( (pid = fork()) < 0 ){ /* 创建一个子进程 */ perror ( "failed to fork " ); exit( 1 ); } else if ( pid > 0 ){ close ( fd[0] ); /*父进程中关闭管道的读出端*/ write (fd[1], "hello my son!\\n", 14 ); /*父进程向管道写入数据*/ exit (0); } else { close ( fd[1] ); /*子进程关闭管道的写入端*/ len = read (fd[0], buf, BUFSZ ); /*子进程从管道中读出数据*/ if ( len < 0 ){ perror ( "process failed when read a pipe " ); exit( 1 ); } else write(STDOUT_FILENO, buf, len); /*输出到标准输出*/ exit(0); } }
第二种方式:
用FILE *popen( const char *command, const char *mode) 创建管道
a.参数说明:command是shell中可运行的命令字符串的指针,参数mode是一个字符指针,mode只有两种情况“r”或者“w”,分别表示popen函数的返回值是一个读打开文件指针还是写打开文件指针。
b.返回值:成功返回的是文件指针,失败返回-1。
c.函数说明:popen()函数首先创建一个管道,再调用fork函数创建子进程,然后执行exec()函数调用,调用/bin/sh –c执行command中的命令字符串。
示例:
char *cmd = “cat file1”; fd = popen(cmd, “r”);
创建子进程到父进程的数据管道,cat命令打开file1文件就相当于创建了一个管道,popen函数的返回值是文件指针,赋值给了fd,以后如果进行读写文件的时候直接对fd进行操作即可。
用 fgets(buf, BUFSZ, fp) 函数读管道中的数据:
a.参数说明:buf是读出数据的存放位置,BUFSZ是数据的长度,fp是管道的指针。
b.返回值:成功返回管道中的数据,失败返回NULL。
c.函数功能:将fp管道中的数据督导buf中。
用pclose(fd)函数关闭管道:
a.参数说明:fp是要关闭管道的指针。
b.函数功能:关闭fp管道。
示例程序(用popen和pclose函数创建管道):
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <limits.h> #include <fcntl.h> #include <limits.h> #define BUFSZ PIPE_BUF int main ( void ) { FILE *fp; char *cmd = "cat file1"; /*shell 命令*/ char buf[BUFSZ]; if ((fp = popen( cmd , "r"))==NULL ) /*创建子进程到父进程的数据管道*/ { perror ( " failed to popen " ) ; exit ( 1 ) ; } while ((fgets(buf, BUFSZ, fp))!= NULL ) /*读出管道的数据*/ printf ( "%s", buf ); pclose ( fp ) ; /*关闭管道*/ exit (0) ; }
(2)FIFO管道
FIFO管道也称为有名管道,是一种文件类型,在文件系统中可以看到。FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时管道中数据同时清除数据。
FIFO管道的应用分为三个步骤,先创建FIFO文件,再对FIFO进行读写操作。
创建FIFO文件函数:
#include <sys/stat.h> #include <sys/types.h> int mkfifo(const char * filename, mode_t mode)
a.参数说明:filename是要创建FIFO文件的名称,mode是FIFO文件的读写权限。
b.返回值:成功返回0,出错返回-1并修改erron的值。
c.函数说明:创建一个名字为filename的FIFO文件。
示例程序(创建FIFO文件):
#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> int main (int argc, char *argv[] ) { mode_t mode = 0666; /*新创建的FIFO模式*/ if ( argc != 2 ){ /*向用户提示程序使用帮助*/ printf("USEMSG: create_fifo {fifoname}\\n"); exit (1); } /* 使用mkfifo函数创建一个FIFO管道*/ if ( ( mkfifo (argv[1], mode )) < 0) { perror ( "failed to mkfifo" ); exit ( 1 ); } else printf ("you successfully create a FIFO name is : %s\\n", argv[1]); /* 输出FIFO文件的名称 */ exit (0); }
FIFO读写操作:
一般的I/O函数都可以操作FIFO文件,先打开FIFO文件然后进行读或写的操作。读写操作与匿名通道的有些类似。
open(“fifo1”,O_WRONLY);
a.参数说明:“fifo1”是要打开的FIFO文件的名称,O_WRONLY是打开方式,打开方式有很多做,“O_WRONLY”是以写打开一个FIFO文件,通俗来说就是这样打开方式打开的文件只能写入数据,“O_RDONLY”是以读打开一个FIFO文件,也就是这样打开的文件只能读数据。
b.返回值:成功返回0,失败返回-1,并置erron为ENXIO
c.函数功能:打开文件。
write(fd, buf, n);
将buf中的数据写入fd中,数据长度为n。
read(fd, buf, n);
将fd中数据读到buf中,数据长度为n。
示例程序(使用FIFO进行通信,写入):
#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <limits.h> #define BUFSZ PIPE_BUF int main(void) { int fd ; int n, i ; char buf[BUFSZ]; time_t tp; printf("I am %d\\n",getpid()); /*说明进程的ID*/ if((fd=open("fifo1",O_WRONLY))<0){ /*以写打开一个FIFO1*/ perror("open"); exit(1); } for ( i=0 ; i<10; i++){ /*循环10次向FIFO中写入数据*/ time(&tp); /*取系统当前时间*/ /*使用sprintf 函数向buf中格式化写入进程ID 和时间值*/ n=sprintf(buf,"write_fifo %d sends %s",getpid(),ctime(&tp)); printf("Send msg:%s",buf); if((write(fd, buf, n+1))<0) { /*写入到FIFO中*/ perror("write"); close(fd); /* 关闭FIFO文件 */ exit(1); } sleep(3); /*进程睡眠3秒*/ } close(fd); /* 关闭FIFO文件 */ exit(0); }
示例程序(使用FIFO进行通信,读出):
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <limits.h> #include <fcntl.h> #include <unistd.h> #define BUFSZ PIPE_BUF int main(void) { int fd; int len; char buf[BUFSZ]; mode_t mode = 0666; /* FIFO文件的权限 */ if((fd=open("fifo1",O_RDONLY))<0) /* 打开FIFO文件 */ { perror("open"); exit(1); } while((len=read(fd,buf, BUFSZ))>0) /* 开始进行通信 */ printf("read_fifo read: %s",buf); close(fd); /* 关闭FIFO文件 */ exit(0); }
2.System V IPC
管道和FIFO是基于系统文件系统的,IPC是基于系统内核的,可以使用ipcs查看系统当前的IPC对象状态。
如图所示,IPC对象是活动在内核中的级别的一种进程间的通信工具。引用IPC对象通过它的标识符来引用和访问,IPC标识符是一个非负整数,它唯一标识了IPC对象,这个IPC对象可以是消息队列、信号量或者共享内存中的一个。IPC标识符只解决了内部访问一个IPC对象的问题,让多个进程访问某一个特定的IPC对象还需要一个外部建(key)。
函数ftok()可以使用两个参数生成一个键值。
key_t fotk(const char *path, int id );
a.参数说明:path是文件名,id是一个参数,path中的stat结构中的一些数据与id结合起来生成一个键值。
b.函数功能,使用path和id生成一个键值。
系统中每一个ipc都对应一个ipc_perm结构体,该结构说明了ipc对象的权限和所有者,具体可查阅文件<sys/ipc.h>。
struct ipc_perm{ key_t key; uid_t uid; git_t gid; uid_t cuid; git_t cgid; unsigned short mode; unsigned short seq; };
在shell下可以通过命令 ipcs –a 查看IPC的状态。
(1)内存共享
内存共享是指多个进程可以把一段内存映射到自己的进程空间,以此来实现数据的共享和传输。对于每一个共享的内存段,内核会维护一个shmid_ds类型的结构体,shmid_ds结构的定义跟系统内核版本不同而不同,具体可以查系统手册。
struct shmid_ds{ struct ipc_prem shm_perm; size_t shm_segsz; pid_t shm_lpid; … … … };
函数 shmget(key_t key, size_t size, int flag ) 可以创建或者打开一块共享内存区:
a.参数说明:key用来变成一个标识符,每一个IPC与一个key对应,size是请求共享的内存的长度,flag是函数行为参数。
b.返回值:成功返回共享内存标识符,失败返回-1。
c.函数功能:新建或者打开一块共享内存区并返回共享内存的标识符。
函数示例(创建共享内存):
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <stdio.h> #define BUFSZ 4096 int main ( void ) { int shm_id; /*共享内存标识符*/ shm_id=shmget(IPC_PRIVATE, BUFSZ, 0666 ) ; if (shm_id < 0 ) { /*创建共享内存*/ perror( "shmget" ) ; exit ( 1 ); } printf ( "successfully created segment : %d \\n", shm_id ) ; system( "ipcs -m"); /*调用ipcs命令查看IPC*/ exit( 0 ); }
函数 int shmctl(int shm_id, int cmd, struct shmid_ds *buf) 实现共享内存操作
a.参数说明:shm_id是要操作的共享内存的标识符, buf是共享内存管理结构体,具体说明参见共享内存内核结构定义部分。cmd是操作指令,具体如下。
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID:删除这片共享内存
b.返回值:成功返回0,出错返回-1,错误原因存于error中。
c.函数功能:完成对共享内存的控制
示例代码(操作共享内存段):
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <stdio.h> int main ( int argc, char *argv[] ) { int shm_id ; char * shm_buf; if ( argc != 2 ){ /* 命令行参数错误 */ printf ( "USAGE: atshm <identifier>" ); /*打印帮助消息*/ exit (1 ); } shm_id = atoi(argv[1]); /*得到要引入的共享内存段*/ /*引入共享内存段,由内核选择要引入的位置*/ if ( (shm_buf = shmat( shm_id, 0, 0)) < (char *) 0 ){ perror ( "shmat" ); exit (1); } printf ( " segment attached at %p\\n", shm_buf ); /*输出导入的位置*/ system("ipcs -m"); sleep(3); /* 休眠 */ if ( (shmdt(shm_buf)) < 0 ) { /*与导入的共享内存段分离*/ perror ( "shmdt"); exit(1); } printf ( "segment detached \\n" ); system ( "ipcs -m " ); /*再次查看系统IPC状态*/ exit ( 0 ); }
(2)信号量
信号量的原理是一种数据操作锁的概念,本身不具备数据交换的功能,通过控制其他的通信资源(文件、外部设备等)来实现进程间通信的。对于信号量的操作只有两种,等待和发送信号。对于信号量的使用和理解关键是理解和使用两个函数semget()函数和semctl()函数。
semget(key_t key, int nsems, int flag) 创建新信号量或获取已存在信号量的键值。
a.参数说明:key为整型值,用户可以自己设定。有两种情况1.键值是IPC_PRIVATE,该值通常为0,意思就是创建一个仅能被进程进程给我的信号量。2.键值不是IPC_PRIVATE,我们可以指定键值,例如1234;也可以一个ftok()函数来取得一个唯一的键值。nsems表示初始化信号量的个数。比如我们要创建一个信号量,则该值为1.,创建2个就是2。flag是 信号量的创建方式或权限。
b.返回值:成功返回信号量的标识码ID。失败返回-1;
c.功能:创建一个新的信号量或获取一个已经存在的信号量的键值。
semop(int semid, struct sembuf semoparray[ ], size_t nops ) 函数进行信号量处理:
a.参数说明:semid : 信号量的标识码。也就是semget()的返回值。nsops:操作结构的数量,恒大于或等于1。semoparray[ ]是一个指向结构体数组的指针。
struct sembuf{ unsigned short sem_num; //第几个信号量,第一个信号量为0; short sem_op; //对该信号量的操作。 short sem_flg; };
sem_num: 操作信号在信号集中的编号。第一个信号的编号为0;
sem_op : 如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。
sem_flg :有两种情况IPC_NOWAIT或者IPC_UNDO 。IPC_NOWAIT指对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。IPC_UNDO指程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。
b.返回值:成功返回0,失败返回-1。
c.函数功能:操作信号量。
示例代码(用semget()函数创建一个信号量,用semop()函数进项资源释放操作):
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h> #include <stdlib.h> int main( void ) { int sem_id; int nsems = 1; int flags = 0666; struct sembuf buf; sem_id = semget(IPC_PRIVATE, nsems, flags); /*创建一个新的信号量集*/ if ( sem_id < 0 ){ perror( "semget ") ; exit (1 ); } /*输出相应的信号量集标识符*/ printf ( "successfully created a semaphore : %d\\n", sem_id ); buf.sem_num = 0; /*定义一个信号量操作*/ buf.sem_op = 1; /*执行释放资源操作*/ buf.sem_flg = IPC_NOWAIT; /*定义semop函数的行为*/ if ( (semop( sem_id, &buf, nsems) ) < 0) { /*执行操作*/ perror ( "semop"); exit (1 ); } system ( "ipcs -s " ); /*查看系统IPC状态*/ exit ( 0 ); }
以上是关于进程间通信的主要内容,如果未能解决你的问题,请参考以下文章