前言
现在Linux进程间通信方式包括:匿名管道(pipe)及有名管道(fifo)、信号(signal)、消息队列(message queue)、共享内存(shared memory)、信号量(semaphore)、套接字(socket)。
管道:把一个程序的输出直接连接到另一个程序的输入。
匿名管道
- 只能用于具有亲缘关系的进程之间的通信(父进程和子进程之间)
- 单工通信模式,固定读端和写端
- 特殊的文件,可以使用普通的read()、write()等函数,不属于文件系统,只存在于系统中
一、说明
1、应用接口说明
创建与关闭——pipe()、close()
#include <unistd.h> /** * fd:包含两个元素的整型数组,存放管道对于文件描述符 * 返回值:成功:0,失败:-1 */ int pipe(int pipefd[2]); int close(int fd);
2、实现说明
管道操作是进程间通信的最基本方式。本程序包括管道文件读写操作函数 read_pipe() 和 write_pipe(),同时实现了管道系统调用 sys_pipe()。这两个函数也是系统调用 read() 和 write() 的低层实现函数,也仅在 read_write.c 中使用。
在创建并初始化管道时,程序会专门申请一个管道 i 节点 (参考:1、m_inode 结构体 ),并为管道分配一页缓冲区(4KB)。管道 i 节点的 i_size 字段中被设置为指向管道缓冲区的指针,管道数据头部指针存放在 i_zone[0] 字段中,而管道数据尾部指针存放在 i_zone[1] 字段中。对于读管道操作,数据是从管道尾读出,并使管道尾指针前移读取字节数个位置;对于往管道中的写入操作,数据是向管道头部写入,并使管道头指针前移写入字节数个位置。参见下面的管道示意图 12-28 所示。
read_pipe() 用于读管道中的数据。若管道中没有数据,就唤醒写管道的进程,而自己则进入睡眠状态。若读到了数据,就相应地调整管道头指针,并把数据传到用户缓冲区中。当把管道中所有的数据都取走后,也要唤醒等待写管道的进程,并返回已读数据字节数。当管道写进程已退出管道操作时,函数就立刻退出,并返回已读的字节数。
write_pipe()函数的操作与读管道函数类似。
系统调用 sys_pipe() 用于创建无名管道。它首先在系统的文件表中取得两个表项,然后在当前进程的文件描述符表中也同样寻找两个未使用的描述符表项,用来保存相应的文件结构指针。接着在系统中申请一个空闲 i 节点,同时获得管道使用的一个缓冲块。然后对相应的文件结构进行初始化,将一个文件结构设置为只读模式,另一个设置为只写模式。最后将两个文件描述符传给用户。
另外,以上函数中使用的几个与管道操作有关的宏例如 PIPE_HEAD()、PIPE_TAIL() 等)定义在 include/linux/fs.h 文件第 57-64 行上。
二、应用代码
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int fd[2]; pid_t pid; char str1[] = "abcdeabcdeabcdeabcde"; char str2[512]; int i, j; if(pipe(fd) <0) { perror("fail to pipe"); exit(-1); } if ((pid = fork()) < 0) { return -1; } else if (pid > 0) { // 父进程向管道中写入数据 close(fd[0]); for (i = 0; i < 10000; i++) write(fd[1], str1, strlen(str1)); } else { // 子进程从管道中读取数据 close(fd[1]); for (j = 0; j < 20000; j++) read(fd[0], str2, strlen(str2)); } return 0; }
三、系统背后行为
1、file 结构体
// include/linux/fs.h // 文件结构(用于在文件句柄与 i 节点之间建立关系) struct file { unsigned short f_mode; // 文件操作模式 (RW 位) unsigned short f_flags; // 文件打开和控制的标志 unsigned short f_count; // 对应文件引用计数值 struct m_inode * f_inode; // 指向对应 i 节点 off_t f_pos; // 文件位置(读写偏移值) };
2、sys_pipe 函数
pipe 函数会调用系统实现的 sys_pipe 函数 。
从技术上看,管道就是一页内存,但进程要以操作文件的方式对其进行操作,这就要求这页内存具备一些文件属性并减少页属性。
具备一些文件属性表现为,创建管道相当于创建一个文件,如进程 task_struct 中 *filp[20] 和 file_table[64] 挂接、新建 i 节点、file_table[64] 和文件 i 节点挂接等工作要在创建管道过程中完成,最终使进程只要知道自己在操作管道类型的文件就可以了,其他的都不用关心。
减少页属性表现为,这页内存毕竟要当做一个文件使用,比如进程不能像访问自己用户。空间的数据一样访问它,不能映射到进程的线性地址空间内。再如,两个进程操作这个页面,一个读一个写,也不能产生页写保护异常把页面另复制一份,否则无法共享管道。下面我们来看管道的具体创建过程。
// fs/pipe.c /// 创建管道系统调用。 // 在 fildes 所指的数组中创建一对文件句柄(描述符)。这对文件。柄指向一管道 i 节点。 // 参数:filedes -文件句柄数组。fildes[0]用于读管道数据,fildes[1]向管道写入数据。 // 成功时返回 0,出错时返回-1。 int sys_pipe(unsigned long * fildes) { struct m_inode * inode; struct file * f[2]; // 文件结构数组。 int fd[2]; // 文件句柄数组。 int i,j; // 首先从系统文件表中取两个空闲项(引用计数字段为 0 的项),并分别设置引用计数为 1。 // 若只有 1 个空闲项,则释放该项(引用计数复位)。若没有找到两个空闲项,则返回 -1。 j=0; for(i=0;j<2 && i<NR_FILE;i++) if (!file_table[i].f_count) (f[j++]=i+file_table)->f_count++; if (j==1) f[0]->f_count=0; if (j<2) return -1; // 针对上面取得的两个文件表结构项,分别分配一文件句柄号,并使进程文件结构指针数组的。 // 两项分别指向这两个文件结构。而文件句柄即是该数组的索引号。类似地,如果只有一个空 // 闲文件句柄,则释放该句柄(置空相应数组项)。如果没有找到两个空闲句柄,则释放上面。 // 获取的两个文件结构项(复位引用计数值),并返回 -1。 j=0; for(i=0;j<2 && i<NR_OPEN;i++) if (!current->filp[i]) { current->filp[ fd[j]=i ] = f[j]; j++; } if (j==1) current->filp[fd[0]]=NULL; if (j<2) { f[0]->f_count=f[1]->f_count=0; return -1; } // 然后利用函数 get_pipe_inode() 申请一个管道使用的 i 节点,并为管道分配一页内存作为缓 // 冲区。如果不成功,则相应释放两个文件句柄和文件结构项,并返回-1。 if (!(inode=get_pipe_inode())) { // fs/inode.c,第 228 行开始处。 current->filp[fd[0]] = current->filp[fd[1]] = NULL; f[0]->f_count = f[1]->f_count = 0; return -1; } // 如果管道 i 节点申请成功,则对两个文件结构进行初始化操作,让它们都指向同一个管道 i 节 // 点,并把读写指针都置零。第 1 个文件结构的文件模式置为读,第 2 个文件结构的文件模式置 // 为写。最后将文件句柄数组复制到对应的用户空间数组中,成功返回 0, 退出。 f[0]->f_inode = f[1]->f_inode = inode; f[0]->f_pos = f[1]->f_pos = 0; f[0]->f_mode = 1; /* read */ f[1]->f_mode = 2; /* write */ put_fs_long(fd[0],0+fildes); put_fs_long(fd[1],1+fildes); return 0; }
get_pipe_inode 函数
// include/linux/fs.h #define PIPE_HEAD(inode) ((inode).i_zone[0]) // 写管道 #define PIPE_TAIL(inode) ((inode).i_zone[1]) // 读管道 #define PIPE_SIZE(inode) ((PIPE_HEAD(inode)-PIPE_TAIL(inode))&(PAGE_SIZE-1)) #define PIPE_EMPTY(inode) (PIPE_HEAD(inode)==PIPE_TAIL(inode)) #define PIPE_FULL(inode) (PIPE_SIZE(inode)==(PAGE_SIZE-1)) #define INC_PIPE(head) \ __asm__("incl %0\n\tandl $4095,%0"::"m" (head)) // fs/inode.c 获取管道节点。 // 首先扫描 i 节点表,寻找一个空闲 i 节点项,然后取得一页空闲内存供管道使用。然后将得 // 到的 i 节点的引用计数置为 2(读者和写者),初始化管道头和尾,置 i 节点的管道类型表示。 // 返回为 i 节点指针,如果失败则返回 NULL。 struct m_inode * get_pipe_inode(void) { struct m_inode * inode; // 首先从内存 i 节点表中取得一个空闲 i 节点。如果找不到空闲 i 节点则返回 NULL。然后为该 // i 节点申请一页内存,并让节点的 i_size 字段指向该页面。如果已没有空闲内存,则释放该 // i 节点,并返回 NULL。 if (!(inode = get_empty_inode())) return NULL; if (!(inode->i_size=get_free_page())) { inode->i_count = 0; return NULL; } // 然后设置该 i 节点的引用计数为 2,并复位复位管道头尾指针。i 节点逻辑块号数组 i_zone[] // 的 i_zone[0] 和 i_zone[1]中分别用来存放管道头和管道尾指针。最后设置 i 节点是管道 i 节。 // 点标志并返回该 i 节点号。 inode->i_count = 2; /* sum of readers/writers */ /* 读/写两者总计 */ PIPE_HEAD(*inode) = PIPE_TAIL(*inode) = 0; // 复位管道头尾指针。 inode->i_pipe = 1; // 置节点为管道使用的标志。 return inode; }
3、管道操作
源码分析
// include/linux/fs.h #define PIPE_HEAD(inode) ((inode).i_zone[0]) // 写管道 #define PIPE_TAIL(inode) ((inode).i_zone[1]) // 读管道 #define PIPE_SIZE(inode) ((PIPE_HEAD(inode)-PIPE_TAIL(inode))&(PAGE_SIZE-1)) // fs/pipe.c #include <signal.h> // 信号头文件。定义信号符号常量,信号结构及操作函数原型。 #include <linux/sched.h> // 调度程序头文件,定义了任务结构 task_struct、任务 0 数据等。 #include <linux/mm.h> /* for get_free_page */ /* 使用其中的 get_free_page */ // 内存管理头文件。含有页面长度定义和一些页面释放函数原型。 #include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。 管道读操作函数。 // 参数 inode 是管道对应的 i 节点,buf 是用户数据缓冲区指针,count 是读取的字节数。 int read_pipe(struct m_inode * inode, char * buf, int count) { int chars, size, read = 0; // 如果需要读取的字节计数 count 大于 0,我们就循环执行以下操作。在循环读操作过程中, // 若当前管道中没有数据(size=0),则唤醒等待该节点的进程,这通常是写管道进程。如果 // 已没有写管道者,即 i 节点引用计数值小于 2,则返回已读字节数退出。否则在该 i 节点上 // 睡眠,等待信息。宏 PIPE_SIZE 定义在 include/linux/fs.h 中。 while (count>0) { while (!(size=PIPE_SIZE(*inode))) { // 取管道中数据长度值。 wake_up(&inode->i_wait); if (inode->i_count != 2) /* are there any writers? */ return read; sleep_on(&inode->i_wait); } // 此时说明管道(缓冲区)中有数据。于是我们取管道尾指针到缓冲区末端的字节数 chars。 // 如果其大于还需要读取的字节数 count,则令其等于 count。如果 chars 大于当前管道中含 // 有数据的长度 size,则令其等于 size。 然后把需读字节数 count 减去此次可读的字节数 // chars,并累加已读字节数 read。 // 即使管道(缓冲区)中的数据大于需要读取的数(count),但写指针到置缓冲区末端的字节数少于 count, // 那本次也只写到缓冲区未端,剩下的数据待下一次写入。 chars = PAGE_SIZE-PIPE_TAIL(*inode); if (chars > count) chars = count; if (chars > size) chars = size; count -= chars; read += chars; // 再令 size 指向管道尾指针处,并调整当前管道尾指针(前移 chars 字节)。若尾指针超过 // 管道末端则绕回。然后将管道中的数据复制到用户缓冲区中。对于管道 i 节点,其 i_size。 // 字段中是管道缓冲块指针。 size = PIPE_TAIL(*inode); PIPE_TAIL(*inode) += chars; PIPE_TAIL(*inode) &= (PAGE_SIZE-1); while (chars-->0) put_fs_byte(((char *)inode->i_size)[size++],buf++); } // 当此次读管道操作结束,则唤醒等待该管道的进程,并返回读取的字节数。 wake_up(&inode->i_wait); return read; } 管道写操作函数。 // 参数 inode 是管道对应的 i 节点,buf 是数据缓中区指针,count 是将写入管道的字节数。 int write_pipe(struct m_inode * inode, char * buf, int count) { int chars, size, written = 0; // 如果要写入的字节数 count 还大于 0,那么我们就循环执行以下操作。在循环操作过程中, // 若当前管道中没有已经满了(空闲空间 size = 0),则唤醒等待该节点的进程,通常唤醒。 // 的是读管道进程。 如果已没有读管道者,即 i 节点引用计数值小于 2,则向当前进程发送 // SIGPIPE 信号,并返回已写入的字节数退出;若写入 0 字节,返回 -1。否则让当前进程。 // 在该 i 节点上睡眠,以等待读管道进程读取数据,从而让管道腾出空间。宏 PIPE_SIZE()、 // PIPE_HEAD()等定义在文件 include/linux/fs.h 中。 while (count>0) { while (!(size=(PAGE_SIZE-1)-PIPE_SIZE(*inode))) { wake_up(&inode->i_wait); if (inode->i_count != 2) { /* no readers */ current->signal |= (1<<(SIGPIPE-1)); return written?written:-1; } sleep_on(&inode->i_wait); } // 程序执行到这里表示管道缓冲区中有可写空间 size。于是我们取管道头指针到缓冲区末端空 // 间字节数 chars。写管道操作是从管道头指针处开始写的。如果 chars 大于还需要写入的字节 // 数 count,则令其等于 count。 如果 chars 大于当前管道中空闲空间长度size,则令其等于 // size。然后把需要写入字节数 count 减去此次可写入的字节数 chars,并把写入字节数累加到 // written 中。 // 即使管道(缓冲区)空闲缓存大小大于所需要写入的数(count),但置缓冲区末端的字节数少于 count, // 那本次也读到缓冲区未端,剩下的数据待下一次读取。 chars = PAGE_SIZE-PIPE_HEAD(*inode); if (chars > count) chars = count; if (chars > size) chars = size; count -= chars; written += chars; // 再令 size 指向管道数据头指针处,并调整当前管道数据头部指针(前移 chars 字节)。若头 // 指针超过管道末端则绕回。然后从用户缓冲区复制 chars 个字节到管道头指针开始处。 对于 // 管道 i 节点,其 i_size 字段中是管道缓冲块指针。 size = PIPE_HEAD(*inode); PIPE_HEAD(*inode) += chars; PIPE_HEAD(*inode) &= (PAGE_SIZE-1); while (chars-->0) ((char *)inode->i_size)[size++]=get_fs_byte(buf++); } // 当此次写管道操作结束,则唤醒等待管道的进程,返回已写入的字节数,退出。 wake_up(&inode->i_wait); return written; }
图解