在前面章节中讲解了进程的派生和常见调用,可是进程之间通讯的惟一途径就是经过打开的文件,或者是使用进程以前信号传输,因为这些技术的局限性,Unix系统提供了一种进程间通讯模型(IPC)。
IPC是进程通讯各类方式的统称,目前只有一些经典的IPC方式能作到移植使用:管道、FIFO、消息队列、信号量、共享存储。还有基于套接字技术的网络IPC。shell
管道是很古老的进程间通讯机制了,基本全部的Unix系统或者非Unix系统都支持这种方式,管道有如下特性:数组
半双工,也就是数据只能作到单向流动安全
管道只能在具备公共祖先的两个进城之间使用。服务器
虽然具备局限性,可是因为它的可移植性,因此目前仍然是首选的进程间通讯技术,管道在shell中很是常见,咱们经常使用如下命令网络
> command1 | command2 ... commandn
shell使用管道将前一个进程的标准输出与后一条命令的标准输入相链接。
开发者调用pipe函数建立管道数据结构
int pipe(int fildes[2]);
filedes参数包含了两个文件描述符,Unix手册上这么描述The first descriptor connects to the read end of the pipe; the second connects to the write end.
,也就是说,filedes[0]是读的一端,filedes[1]是写入的一端,这就是一个半双工的管道,而后能够经过fork的方式将文件描述符分给父子进程,从而实现通讯。在fork之后,双方都持有读写的端口,而父子进程能够关闭其中两个,每一个进程只持有一个,从而作到真正的管道。管道有如下特色:架构
读一个写口关闭的管道时,当缓冲区全部数据都读取后,会返回0,表明文件结束。函数
写一个读口关闭的管道时,会产生SIGPIPE信号,若是忽略该信号或者捕获该信号而且从信号处理函数返回,则write返回-1,errno设置为EPIPE。post
咱们实际上能够将管道理解为一块内核维护的缓冲区,因此常量PIPE_BUF规定了管道的大小。测试
#include "include/apue.h" int main(int argc, char *argv[]) { int n; int fd[2]; pid_t pid; char line[MAXLINE]; if (pipe(fd) < 0) err_sys("pipe error"); if ((pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) { close(fd[0]); write(fd[1], "hello world\n", 12); } else { close(fd[1]); n = read(fd[0], line, MAXLINE); write(STDOUT_FILENO, line, n); } exit(0); }
上面就是很是简单的父子进程使用同一个管道通讯的小实例。
除了上面的pipe函数之外,更常见的作法是建立一个链接到另外一个进程的管道,而后读其输出或者向其输出端发送数据,为此标准IO库提供了popen和pclose函数
FILE *popen(const char *command, const char *mode); int pclose(FILE *stream);
咱们能够看到,这两个函数是标准C库提供的函数,而且返回的是FILE结构体的指针,而且这两个函数的实现是:建立一个管道,fork一个子进程,关闭管道端,exec一个shell运行命令,而后等待命令终止。
popen函数的type参数指示返回的文件指针链接的类型,若是type是"r",则文件指针链接到cmdstring的标准输出。若是type是"w",则文件指针链接到cmdstring的标准输入。
pclose函数关闭标准IO流,而且等待命令终止,最后返回shell的终止状态
在平常Unix使用中,一般咱们会使用管道链接多个命令,当一个程序标准输入输出都链接到管道的时候,这就是协同进程。
下面是一个父子进程实现的协同进程
#include "include/apue.h" int main(int argc, char *argv[]) { int n, int1, int2; char line[MAXLINE]; while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) { line[n] = 0; if (sscanf(line, "%d%d", &int1, &int2) == 2) { sprintf(line, "%d\n", int1 + int2); n = strlen(line); if (write(STDOUT_FILENO, line, n) != n) err_sys("write error"); } else { if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) err_sys("write error"); } } exit(0); }
上面的代码很是简单,就是标准输入读取,计算后输出到标准输出,将其编译为add2程序。
#include "include/apue.h" static void sig_pipe(int); int main(int argc, char *argv[]) { int n, fd1[2], fd2[2]; pid_t pid; char line[MAXLINE]; if (signal(SIGPIPE, sig_pipe) == SIG_ERR) err_sys("signal error"); if (pipe(fd1) < 0 || pipe(fd2) < 0) err_sys("pipe error"); if ((pid = fork()) < 0) err_sys("fork error"); else if (pid > 0) { close(fd1[0]); close(fd2[1]); while (fgets(line, MAXLINE, stdin) != NULL) { n = strlen(line); if (write(fd1[1], line, n) != n) err_sys("write error to pipe"); if ((n = read(fd2[0], line, MAXLINE)) < 0) err_sys("read error from pipe"); if (n == 0) { err_msg("child closed pipe"); break; } line[n] = 0; if (fputs(line, stdout) == EOF) err_sys("fputs error"); } if (ferror(stdin)) err_sys("fgets error on stdin"); exit(0); } else { close(fd1[1]); close(fd2[0]); if (fd1[0] != STDIN_FILENO) { if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); close(fd1[0]); } if (fd2[1] != STDOUT_FILENO) { if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) err_sys("dup2 error to stdout"); close(fd2[1]); } if (execl("./add2", "add2", (char *)0) < 0) err_sys("execl error"); } exit(0); }
程序建立了两个管道,父子进程各自关闭不须要使用的管道。而后使用dup2函数将其移到标准输入输出,最后调用execl。
FIFO就是First In First Out
先进先出队列,也被称为命名管道。未命名的管道只能在两个相关进程使用,而命名管道就能全局使用。
建立一个FIFO就等同于建立一个文件,在前面的时候就讲过,FIFO就是一种文件,而且在stat文件结构中就有st_mode成员编码能够知道是不是FIFO。
int mkfifo(const char *path, mode_t mode);
还有一个mkfifoat函数在某些系统中是不可用的,其实就是一个文件描述符版本的mkfifo函数,基本同样,其中mode参数和open函数中mode参数相同。新的FIFO用户和组的全部权规则和前面章节中讲述的同样。
当建立完FIFO文件后,须要使用open函数打开,由于这确实是一个文件,几乎全部的正常文件IO函数都能操做FIFO。
非阻塞标志(O_NONBLOCK
)对FIFO会有如下影响:
未指定标志的时候,只读的open会一直阻塞到有进程写打开,只写open会阻塞到有进程读打开。
指定了标志,则只读open马上返回。可是若是没有进程为只读打开,只写的open会出错。
和管道相似,若是write一个没有进程读打开的FIFO则会产生SIGPIPE信号。若是读完全部数据,read函数会返回文件结束。
因为这是一个文件,因此多个进程读写是很是正常的事情,因此为了保证读写安全,原子写操做是必需要考虑的。FIFO有如下用途:
shell命令使用FIFO将数据从一条管道传输到另外一条管道不须要建立中间文件
C/S架构中,FIFO用做中间点,在客户服务器之间传输数据
这里就再也不讲实例了。须要的朋友自行寻找代码。
在IPC中有三种被称为XSI IPC,他们有不少共同点。这里先讲解共同点。
XSI IPC在内核中存在着IPC结构,它们都用一个非负整数做为标识符,这点很像文件描述符,可是文件描述符永远是当前最小的开始,好比,第一个文件描述符必然是从3开始,而后这个文件描述符删除后,再次打开一个文件,文件描述符仍然是3,而IPC结构则不会减小,会变成4,而后不断增长直到整数的最大值,而后又回转到0。
标识符是IPC结构的内部名称,为了能全局使用,须要有一个键做为外部名称,不管什么时候建立IPC结构,都应当指定一个键名,这个键的数据结构是基本系统数据类型key_t
。而且有不少种方法使客户进程和服务器进程在同一个IPC结构汇聚
服务器进程指定IPC_PRIVATE键建立一个新的IPC结构。返回的标识符被存放在一个文件中,客户端进程读取这个文件来参与IPC结构。
在一个公用头文件中定义一个统一的标识符,而后服务端根据这个标识符建立新的IPC结构,可是颇有可能致使冲突
客户端和服务端进程认同同一个路径名和项目ID。接着调用函数ftok将这两个值变为一个键,而后在建立一个IPC结构
key_t ftok(const char *path, int id);
path参数必须是一个现有的文件,当产生键的时候,只会使用id参数低八位。
ftok建立键通常依据以下行为:首先根据给定的path参数得到对应文件的stat结构中st_dev和st_ino字段,而后将他们和项目ID组合。
每一个IPC结构都关联了一个ipc_perm结构,这个结构体关联了权限和全部者。
struct ipc_perm { uid_t uid; /* [XSI] Owner's user ID */ gid_t gid; /* [XSI] Owner's group ID */ uid_t cuid; /* [XSI] Creator's user ID */ gid_t cgid; /* [XSI] Creator's group ID */ mode_t mode; /* [XSI] Read/write permission */ unsigned short _seq; /* Reserved for internal use */ key_t _key; /* Reserved for internal use */ };
上面是苹果平台的结构体内容,通常来讲,都会有uid、gid、cuid、cgi、mode这些基本的内容,而其余则是各个实现自由发挥。在建立的时候,这些字段都会被赋值,然后,若是想要修改这些字段,则必须是保证具备root权限或者是建立者。
XSI IPC一个问题就是:IPC结构是在系统范围内使用的,可是却没有引用计数。若是进程使用完可是没有对其进行删除就终止了,那就会致使IPC依然在系统中存在,而管道有引用计数,等最后一个引用管道的进程终止便会自动回收。FIFO就算没有删除,可是等最后一个引用FIFO的进程终止,里面的数据已经被删除了。
XSI IPC还有一个问题就是它不是文件,咱们不能使用ls和rm等文件操做函数或者命令处理它们,它们也没有文件描述符,这就限制了它们的使用,而且若是须要使用还得携带一大堆额外的API。
消息队列,正如其名称同样,是消息的链表形式,它由内核存储维护。而且和XSI IPC结构同样,由消息队列标识符标识。
msgget函数建立一个队列或者打开一个现有队列,msgsnd将新数据添加到消息末尾,每一个消息包含一个正的长整形字段、一个非负的长度以及实际数据字节数。msgrcv从队列中得到消息。
struct __msqid_ds { struct __ipc_perm_new msg_perm; /* [XSI] msg queue permissions */ __int32_t msg_first; /* RESERVED: kernel use only */ __int32_t msg_last; /* RESERVED: kernel use only */ msglen_t msg_cbytes; /* # of bytes on the queue */ msgqnum_t msg_qnum; /* [XSI] number of msgs on the queue */ msglen_t msg_qbytes; /* [XSI] max bytes on the queue */ pid_t msg_lspid; /* [XSI] pid of last msgsnd() */ pid_t msg_lrpid; /* [XSI] pid of last msgrcv() */ time_t msg_stime; /* [XSI] time of last msgsnd() */ __int32_t msg_pad1; /* RESERVED: DO NOT USE */ time_t msg_rtime; /* [XSI] time of last msgrcv() */ __int32_t msg_pad2; /* RESERVED: DO NOT USE */ time_t msg_ctime; /* [XSI] time of last msgctl() */ __int32_t msg_pad3; /* RESERVED: DO NOT USE */ __int32_t msg_pad4[4]; /* RESERVED: DO NOT USE */ }
每一个系统实现都会在SUS标准的基础上增长本身私有的字段,因此可能和上面的有所区别。这个结构体定义了队列的当前状态。
int msgget(key_t key, int flag);
msgget根据key得到已有队列或者新队列。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgctl用于对队列进行多种操做,其中,msqid则是消息队列ID,cmd参数是命令参数,能够取如下值:
IPC_STAT 取队列msqid_ds结构体,而且存放在buf参数指定的位置
IPC_SET 将buf参数指定的结构体复制到这个队列中的结构体,须要检查root权限或者建立者权限
IPC_RMID 从系统中删除消息队列
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
这个函数很好理解,就是把ptr指针对应的消息放入消息队列中,flag的值能够指定为IPC_NOWAIT,这点相似于文件IO非阻塞标志。
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
和msgsnd同样,这个ptr参数指向一个长整形数,随后跟随的是存储实际区域的缓冲区。nbytes指定数据缓冲区的长度。参数type则能够指定想要哪一种消息。
信号量是一个计数器,针对多个进程提供对共享数据对象的访问。信号量的使用主要是用来保护共享资源的,使得资源在一个时刻只会被一个进程(线程)拥有。
信号量的使用以下:
测试控制该资源的信号量
若是信号量为正,则进程可使用该资源,这种状况下,信号量会减一,表示已经使用了一个资源。
若是信号量为0,则进程进入休眠状态,知道信号量变为正,进程将会唤醒。
内核为每一个信号量集合维护着一个semid_ds结构体,根据每一个系统实现不一样会有不一样的字段,这里就不列出了。当咱们想要使用信号量的时候,使用以下函数
int semget(key_t key, int nsems, int semflg);
咱们知道,XSI IPC实际上具备其共性,因此如同消息队列同样,这里将key变换为标识符的规则也是同样的。
int semctl(int semid, int semnum, int cmd, ...);
就如同是前面的ioctl等函数同样,这个函数也是用于控制信号量,其中第四个参数是可选的,取决于cmd参数。
IPC_STAT Fetch the semaphore set's struct semid_ds, storing it in the memory pointed to by arg.buf. IPC_SET Changes the sem_perm.uid, sem_perm.gid, and sem_perm.mode members of the semaphore set's struct semid_ds to match those of the struct pointed to by arg.buf. The calling process's effective uid must match either sem_perm.uid or sem_perm.cuid, or it must have superuser privileges. IPC_RMID Immediately removes the semaphore set from the system. The calling process's effective uid must equal the semaphore set's sem_perm.uid or sem_perm.cuid, or the process must have superuser privileges. GETVAL Return the value of semaphore number semnum. SETVAL Set the value of semaphore number semnum to arg.val. Outstanding adjust on exit values for this semaphore in any process are cleared. GETPID Return the pid of the last process to perform an operation on semaphore number semnum. GETNCNT Return the number of processes waiting for semaphore number semnum's value to become greater than its current value. GETZCNT Return the number of processes waiting for semaphore number semnum's value to become 0. GETALL Fetch the value of all of the semaphores in the set into the array pointed to by arg.array. SETALL Set the values of all of the semaphores in the set to the values in the array pointed to by arg.array. Outstanding adjust on exit values for all semaphores in this set, in any process are cleared.
上面就是cmd可选值,除了GETALL之外的全部命令,semctl都返回相应值,除此之外,还有个semop函数自动执行信号量集合上的操做数组
int semop(int semid, struct sembuf *sops, size_t nsops);
sembuf参数是一个指针,指向了sembuf结构体,实际上这是一个数组,
共享存储是一项颇有用的技术,它容许两个或者更多的进程共享同一个给定存储区,因为数据不须要复制,因此这是一种最快的IPC方式,共享存储最重要的就是资源的竞争,因此信号量通常用于共享存储访问。
在前面的章节中,咱们看到了一种共享存储的方式,就是内存映射技术,可是相比存储映射,共享存储不须要建立中间文件。
int shmget(key_t key, size_t size, int shmflg); int shmctl(int shmid, int cmd, struct shmid_ds *buf);
就如同是其余的XSI IPC同样,这里也是一个建立函数,经过这个函数得到存储标识符。而后就是shmctl函数,具体的使用直接查手册,基本上都是差很少的。
当建立完成共享存储段后,使用shmat将其连接到本身的地址空间中。
void *shmat(int shmid, const void *shmaddr, int shmflg); int shmdt(const void *shmaddr);
实际上,因为架构的不一样和平台不一样,为了保持可移植性,咱们不该当去指定共享存储段的地址,而是由系统自行分配,最终返回的就是共享存储段的地址,当操做完成后,咱们须要使用shmdt函数将其分离。
POSIX信号量是三种IPC机制之一,相比XSI标准规定的IPC方式,POSIX的方式更加简洁好用。
POSIX信号量有两种类型:命名的和未命名的,他们二者的区别就像是命名管道和未命名管道同样,有了标识符的信号量就能全局使用,而没有标识符的信号量只能在同一内存区域内使用。
sem_t *sem_open(const char *name, int oflag, ...); The parameters "mode_t mode" and "unsigned int value" are optional. The value of oflag is formed by or'ing the following values: O_CREAT create the semaphore if it does not exist O_EXCL error if create and semaphore exists
实际上这个函数技能建立也能使用现有信号量,上面的是Unix手册节选的内容,应该算是至关清楚了,当函数返回的时候,sem_open会返回一个指针,让咱们传递到其余的函数上,等一切结束,使用sem_close
关闭信号量指针。
int sem_close(sem_t *sem);
固然,也能使用sem_unlink函数销毁一个命名信号量。
int sem_unlink(const char *name);
在这里咱们能看出来,POSIX信号量的命名信号量和文件很像。不像XSI信号量,POSIX信号量的值只能经过一个函数调用来调节,也就是sem_wait函数
int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem);
后一个是sem_wait函数的尝试版本,还有一个是超时版本,可是苹果下好像不存在,建议少使用。
还能够调用sem_post函数使信号量值+1,这个解锁一个二进制信号量或者释放一个技术信号量资源的过程是很像的。
int sem_post(sem_t *sem);
对于未命名信号量,只能使用在单进程或者单线程中,是很是容易的,咱们只须要使用sem_init函数建立,而后使用sem_destroy函数销毁。这里就不在赘述。