Unix系统IPC是进程间各种通信方式的统称,常用的进程间通信方式有:管道、FIFO(命名管道)、全双工管道、消息队列、信号量、共享存储、套接字、STREAMS(流)等。其中流和全双工管道在linux系统中默认不支持。套接字和STREAMS是仅有的两种支持在不同主机的进程间通信的方式。
1. 管道
管道是Unix系统IPC最古老和最常见的形式,它有两个局限:① 它是半双工的,即数据同一时间只能从一个方向流向另一个方向;② 它只能在具有公共祖先的进程之间使用。 在shell中使用管道线|
连接的命令表示前一个命令的输出将做为后一个命令的输入。
pipe函数创建管道:
#include <unistd.h>
int pipe(int filedes[2]);
返回值:若成功则返回0,若失败则返回-1。
经filedes
返回两个文件描述符,其中filedes[0]
表示为读打开,filedes[1]
为写打开。filedes[1]
的输出是filedes[0]
的输入。
fstat
函数对管道的每一个端都返回一个FIFO类型的文件描述符。单个进程中的管道几乎没有任何用处,通常调用pipe的进程接着调用fork,这样就创建了父子进程之间的一个管道。管道的数据流通方向取决于我们在父子进程中选择关闭管道的哪一端,例如在父进程中关闭管道的读端,在字进程中关闭管道的写端,则创建了从父进程到子进程方向的管道。
当管道的一端被关闭时,有以下两条规则起作用:
- 当读一个写端已被关闭的管道时,所有数据读完后,read将返回0;
- 如果写一个读端已被关闭的管道,则产生信号
SIGPIPE
,如果忽略该信号或捕捉该信号并从信号处理程序返回,则write返回-1,并将errno设置为EPIPE
。
PIPE_BUF
规定了管道的大小,当有多个进程同时写一个管道,若write写的字节数小于PIPE_BUF
,则write不会交叉进行;反之有可能相互穿插。
下列程序创建了一个父进程到子进程的管道,并且父进程经由管道向子进程传输数据:
#include "apue.h"
int main(void){
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if(pipe(fd)<0)
err_sys("pipe error\n");
if((pid = fork())<0)
err_sys("fork error\n");
else if(pid >0){
close(fd[0]);
write(fd[1], "write to pipe\n",15);
//sleep(5);
//n = read(fd[0], line, MAXLINE);
// write(STDOUT_FILENO, line, n);
}
else{
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
//write(fd[1], "child write\n", 13);
}
exit(0);
}
很多博客上说管道是单工的,而本书中介绍管道时一直是用半双工来描述,单工是指数据只能从一个方向流向另一个方向,半双工数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输。经过实验发现(上面代码中注释的部分),管道在创立时应该是半双工的,但是一般创立后会在父子进程中分别关闭一个描述(防止进程读自己写的数据),这样管道就变成了单工的了。
使用管道实现父子进程之间的同步操作函数:TELL_WAIT
,TELL_PARENT
,TELL_CHILD
,WAIT_PARENT
,WAIT_CHILD
#include "apue.h"
static int pfd1[2],pfd2[2];
void TELL_WAIT(void){
if(pipe(pfd1)<0 || pipe(pdf2)<0)
err_sys("pipe error");
}
void TELL_PARENT(void){
if(write(pfd1[1], 'c', 1)!=1)
err_sys("write error");
}
void WAIT_PARENT(void){
char c;
if(read(pfd2[0],&c, 1)!=1)
err_sys("read error");
if(c!='p')
err_sys("wait_parent error");
}
void TELL_CHILD(void){
if(write(pfd2[1], 'p', 1)!=1)
err_sys("write error");
}
void WAIT_CHILE(void){
char c;
if(read(pfd1[0], &c, 1)!=1)
err_sys("read error");
if(c!='c')
err_sys("waite_child error");
}
2.popen和pclose函数
标准I/O库提供了两个函数popen
和pclose
来操作管道的创建和关闭,对应于普通文件的fopen
和fclose
。popen创建一个管道,然后fork一个子进程,关闭管道的不使用端,然后执行一个shell以运行目命令,pclose关闭标准I/O流,然后等待命令终止,返回终止状态。
#include <stdio.h>
FILE* popen(const char* cmdstring, const char* type);
返回值:若成功返回文件指针,若出错返回NULL;
int pclose(FILE *fp);
返回值:cmdstring的终止状态,若出错返回-1。
popen的type参数可为w
或r
,分别代表写和读, 下图为type
为r
时数据流通方向:
popen和pclose的一种实现:
#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
static pid_t *childpid=NULL; //保存子进程id的数组,以进程打开的文件描述符为下标
static int maxfd; //最大打开描述符数,即open_max()的返回值
FILE * popen(const char* cmdstring, const char* type){
int i;
int pfd[2];
pid_t pid;
FILE *fp;
if(type[0]!='w' && type[1]!='r' || type[1]!=0){ //判断参数的合法性
errno = EINVAL;
return(NULL);
}
if(childpid==NULL){ //第一次调用
maxfd = open_max();
if((childpid = calloc(maxfd, sizeof(pid_t)))==NULL)
return (NULL);
}
if(pipe(pfd)<0)
return(NULL);
if((pid = fork())<0)
return(NULL);
else if(pid==0){
if(*type=='r'){
close(pfd[0]);
if(pfd[1] != STDOUT_FILENO){
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
}
}
else{
close(pfd[1]);
if(pfd[0] != STDIN_FILENO){
dup2(pfd[0], STDIN_FILENO);
close(pfd[0]);
}
}
for(i=0; i<maxfd; i++){ //关闭以前打开的没有关闭的所有I/O流
if(childpid[i]>0)
close(i);
}
execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
_exit(127);
}
//父进程
if(*type=='r'){
close(pfd[1]);
if((fp=fdopen(pfd[0], type))==NULL) //fdopen将一个文件描述符与一个流关联
return (NULL);
}else{
close(pfd[0]);
if((fp=fdopen(pfd[1], type))==NULL)
return (NULL);
}
childpid[fileno(fp)] = pid; //fileno获取流的文件描述符
return (fp);
}
int pclose(FILE * fp){
int fd, stat;
pid_t pid;
if(childpid == NULL){
errno = EINVAL;
return(-1);
}
fd = fileno(fp);
if((pid = childpid[fd])==0){
errno = EINVAL;
return(-1);
}
childpid[fd] =0;
if(fclose(fp)==EOF)
return(-1);
while(waitpid(pid, &stat, 0)<0){
if(errno != EINTR)
return(-1);
}
return(stat);
}
3.FIFO
FIFO有时被称为命名管道,它可以用于不相关进程间的通信,创建FIFO类似于创建文件,FIFO的路径名存在于文件系统中。
#include <sys/stat.h>
int mdfifo(const char* filename, mode_t mode);
返回值:若成功返回0,出错返回-1
mkfifo的参数mode
和open函数中的mode相同,新FIFO的用户和组的所有权规则和前文描述的相同。一旦创建了FIFO就可以用open打开它,一般的文件I/O函数都适用于FIFO。
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
- 如果没有指定
O_NONBLOCK
,只读open要阻塞到某个其他进程为写打开它,只写open要阻塞到某个其他进程为读打开它; - 如果指定了
O_NONBLOCK
,则只读open立即返回;但是如果没有一个进程已经为读而打开一个FIFO,那么只写open将出错并返回-1,其errno为ENXIO。
FIFO有两种用途:① 由shell命令使用以便将数据从一条管道传送到另一条,为此无需创建中间临时文件;② FIFO用于客户进程—服务进程应用程序中,以在客户进程和服务进程中传递数据。
4 XSI IPC
XSI IPC是由Single Unix Specification的XSI扩展定义的,有消息队列、信号量以及共享存储器。
4.1 标识符和键
每个内核中的IPC结构都用一个非负整数的标识符加以引用,当一个IPC结构被创建,然后又被删除时,与这种结构相关的标识符连续加1,直至到达一个整型数的最大正值,然后又回转到0。标识符是IPC对象的内部名,而键则是IPC对象的外部名。键的类型是基本系统数据类型key_t,通常定义在头文件<sys/types.h>
中,键由内核转换为标识符。
有三种方法创建标识符:
- 服务器进程指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存在某处(例如一个文件)以便客户进程取用,缺点是:需要将整型标识符写到文件中,此后客户进程又要读此文件。也可以用在父子进程之间。
- 在一个公用头文件中定义一个客户进程和服务器进程都认可的键,然后服务器进程用该键创建一个新的IPC结构。这种方法的问题是该键可能已与一个IPC结构结合。
- 客户进程和服务器进程认同同一个路径和项目ID,接着调用
ftok
函数将这两个值变为一个键,然后在上一个方法中使用此键。
4.2 权限结构
XSI IPC为每一个IPC结构设置了一个ipc_perm结构。该结构规定了权限和所有者,至少包括下列成员:
struct ipc_perm{
uid_t uid; //所有者(进程)的有效用户ID
gid_t gid; //所有者(进程)的有效组ID
uid_t cuid; //创建者(进程)的有效用户ID
uid_t cgid; //创建者(进程)的有效组ID
}
创建IPC结构时为权限结构赋值,以后可以改变该值,但必须满足调用进程是该结构的创建者或超级用户。
4.3 XSI IPC的优点和缺点
主要问题是:IPC结构是在系统范围内起作用的,没有访问计数,当没有数据访问它们时,还要特定的系统调用来删除它们,或者正在重启动的系统删除它们。另一个问题是:这些IPC结构在文件系统中没有名字,不能用文件系统相关函数来访问它们,必须新增十几条全新的系统调用。另外因为这些IPC结构不使用文件描述符,所以不能对它们使用多路转换I/O函数:selec和poll。
5 消息队列
消息队列是消息的链接表,存在内核中,并由消息队列标识符标识。msgget
用于创建一个新队列或者打开一个现存的队列,msgsnd
将新信息添加到队列结尾,每个消息包含一个正长整型类型字段,一个非负长度以及实际数据字节(对应于长度),msgrcv
用于从队列中取消息,我们并不一定要以先进先出次序取消息,也可以按消息的类型字段取消息。每一个队列都有一个msqid_ds结构与其关联:
struct msqid_ds{
struct ipc_perm msg_perm; // 权限结构
msgqnum_t msg_qnum; // 在队列中的消息数目
msglen_t msg_qbytes; // 队列中的最大字节数
pid_t msg_lspid; // 上一个添加消息的进程的id
pid_t msg_lrpid; // 上一个从队列中取消息的进程的id
time_t msg_stime; // 上次添加消息的时间
time_t msg_rtime; // 上次接收消息的时间
time_t msg_ctime; // 上次改变队列的时间
.
.
.
};
#include <sys/msg.h>
int msgget(key_t key, int flag);
返回值: 若成功则返回消息队列ID,若出错则返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
返回值:若成功则返回0,若出错则返回-1
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
返回值: 若成功则返回0, 若出错则返回-1
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
返回值:若成功则返回消息的数据部分的长度,若出错则返回-1
6 信号量
信号量是一个计数器,用于对共享数据对象的访问。为了获得共享资源,进程需要执行下列操作:① 测试控制该资源的信号量;② 若此信号量值为正,则进程可以使用该资源; ③ 若此信号量值为0,则进程进入休眠状态,直至信号量值大于0。为了正确的实现信号量,信号量值的测试及减1操作应当是原子操作。
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回值: 若成功则返回消息队列ID,若出错则返回-1
int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);
返回值:若成功则返回0,若出错则返回-1
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
返回值: 若成功则返回0, 若出错则返回-1
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
返回值:若成功则返回消息的数据部分的长度,若出错则返回-1