一、简介
所谓管道,是指用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,又称pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道,而接收管道输出的接收进城(即读进程),可从管道中接收数据。由于发送进程和接收进程是利用管道进行通信的,故又称管道通信。这种方式能够传送大量数据,且很有效。
为了协调双方的通信,管道通信机制必须提供以下三方面的协调能力:
//// 创建管道系统调用函数。 // 在fildes 所指的数组中创建一对文件句柄(描述符)。这对文件句柄指向一管道i 节点。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。 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; // 如果没有找到两个空闲项,则返回-1。 if (j<2) return -1; /*为管道文件与进程建立联系创造条件。一旦父进程fork子进程,子进程将会拷贝这一切,同样与管道文件表项建立了联系*/ // 针对上面取得的两个文件结构项,分别分配一文件句柄,并使进程的文件结构指针分别指向这两个 // 文件结构,一个写入端,一个输出端。 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; // 如果没有找到两个空闲句柄,则释放上面获取的两个文件结构项(复位引用计数值),并返回-1。 if (j<2) { f[0]->f_count = f[1]->f_count = 0; return -1; } /*创建管道文件i 节点*/ // 申请管道i 节点,并为管道分配缓冲区(1 页内存)。如果不成功,则相应释放两个文件句柄和文 // 件结构项,并返回-1。 if (!(inode = get_pipe_inode())) {//申请一个空闲内存页面,并将该页面的地址载入i节点 current->filp[fd[0]] = current->filp[fd[1]] = NULL; f[0]->f_count = f[1]->f_count = 0; return -1; } /*将管道文件i 节点与文件管理表建立联系*/ // 初始化两个文件结构,都指向同一个i 节点,读写指针都置零。第1 个文件结构的文件模式置为读, // 第2 个文件结构的文件模式置为写。 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 */ // 将文件句柄数组复制到对应的用户数组中,并返回0,退出。 put_fs_long(fd[0], 0 + fildes); put_fs_long(fd[1], 1 + fildes); return 0; }
//// 获取管道节点。返回为i 节点指针(如果是NULL 则失败)。 // 首先扫描i 节点表,寻找一个空闲i 节点项,然后取得一页空闲内存供管道使用。 // 然后将得到的i 节点的引用计数置为2(读者和写者),初始化管道头和尾,置i 节点的管道类型表示。 struct m_inode * get_pipe_inode(void) { struct m_inode * inode; if (!(inode = get_empty_inode())) // 如果找不到空闲i 节点则返回NULL。 return NULL; if (!(inode->i_size = get_free_page())) {// 节点的i_size 字段指向缓冲区。i_size表示内存页面的起始地址 inode->i_count = 0; // 如果已没有空闲内存,则 return NULL; // 释放该i 节点,并返回NULL。 } inode->i_count = 2; /* 读/写两者总计 */ PIPE_HEAD(*inode) = PIPE_TAIL(*inode) = 0;// 复位管道头尾指针。 inode->i_pipe = 1; // 置节点为管道使用的标志。 return inode; // 返回i 节点指针。 }从上面可以看出, 管道的创建过程:1、为管道文件在文件管理表中申请空闲项;2、为管道文件与进程建立联系创造条件;3、创建管道文件i 节点;4、将管道文件i 节点与文件管理表建立联系;5、将管道文件句柄返回给用户进程。
// 管道头、管道尾、管道大小、管道空、管道满、管道头指针递增。 #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)) /* 下面用C语言实现就是 #define INC_PIPE(head) (((*head)++)&(PAGE_SIZE-1)) 用汇编实现,将数据放入寄存器中,速度更快 */ #define INC_PIPE(head) _INC_PIPE(&(head)) extern _inline void _INC_PIPE(unsigned long *head) { _asm mov ebx, head //head存入ebx中 _asm inc dword ptr[ebx] //将ebx指向的数据加1 _asm and dword ptr[ebx], 4095 //结果与4095 }
/*管道类似于一个循环队列*/ //// 管道写操作函数。 // 参数inode 是管道对应的i 节点,buf 是数据缓冲区指针,count 是将写入管道的字节数。 int write_pipe(struct m_inode * inode, char * buf, int count) { int chars, size, written = 0; // 若将写入的字节计数值count 还大于0,则循环执行以下操作。 while (count>0) { // 若当前管道中已经满了(size=0),没有空间可以写入数据了,则唤醒等待该节点的进程(读管道), //如果已没有读管道者,则向进程发送SIGPIPE 信号,并返回已写入的字节数并退出。 // 若写入0 字节,则返回-1。否则在该i 节点上睡眠,等待管道腾出空间。 while (!(size = (PAGE_SIZE - 1) - PIPE_SIZE(*inode))) {//可写入大小 wake_up(&inode->i_wait);//唤醒读管道进程 if (inode->i_count != 2) { /* 没有读进程 */ current->signal |= (1 << (SIGPIPE - 1)); return written ? written : -1; } sleep_on(&inode->i_wait);//挂起写管道进程 } // 取管道头部到缓冲区末端空间字节数chars。如果其大于还需要写入的字节数count,则令其等于 // count。如果chars 大于当前管道中空闲空间长度size,则令其等于size。 /*管道可以看做一个循环队列,清楚chars 与 size的区别*/ chars = PAGE_SIZE - PIPE_HEAD(*inode);//chars <= size if (chars > count) chars = count; if (chars > size) chars = size; // 写入字节计数减去此次可写入的字节数chars,并累加已写字节数到written。 count -= chars; written += chars; // 令size 指向管道数据头部,调整当前管道数据头部指针(前移chars 字节)。 /**/ size = PIPE_HEAD(*inode); PIPE_HEAD(*inode) += chars; PIPE_HEAD(*inode) &= (PAGE_SIZE - 1); // 从用户缓冲区复制chars 个字节到管道中。对于管道i 节点,其i_size 字段中是管道缓冲块指针。 while (chars-->0)//管道头部位置为读管道指针位置 ((char *)inode->i_size)[size++] = get_fs_byte(buf++); } // 唤醒等待该i 节点的进程(写管道),返回已写入的字节数,退出。 wake_up(&inode->i_wait); return written; }
把管道理解为一个循环队列,一个指针标明写入,一个指针标明读出。所以在判断管道满和空的情况下,是以循环队列的标准来判断,在写入的过程中,写管道指针一直指向数据的写入位置,一直向管道尾端移动,写满后,指针已经从管道的首段移动到尾端了,如下图所示。再次执行写管道操作时,系统会将写管道指针从管道尾端移动到管道首端,继续将数据写入管道。
//// 管道读操作函数。 // 参数inode 是管道对应的i 节点,buf 是数据缓冲区指针,count 是读取的字节数。 int read_pipe(struct m_inode * inode, char * buf, int count) { int chars, size, read = 0; // 若欲读取的字节计数值count 大于0,则循环执行以下操作。 while (count>0) { // 若当前管道中没有数据(size=0),则唤醒等待该节点的进程,如果已没有写管道者,则返回已读 // 字节数,退出。否则在该i 节点上睡眠,等待信息。 while (!(size = PIPE_SIZE(*inode))) {//没有数据可读 wake_up(&inode->i_wait); if (inode->i_count != 2) /* 没有写管道 */ return read; sleep_on(&inode->i_wait); } // 取管道尾到缓冲区末端的字节数chars。如果其大于还需要读取的字节数count,则令其等于count。 // 如果chars 大于当前管道中含有数据的长度size,则令其等于size。 chars = PAGE_SIZE - PIPE_TAIL(*inode);// if (chars > count) chars = count; if (chars > size) chars = size; // 读字节计数减去此次可读的字节数chars,并累加已读字节数。 count -= chars; read += chars; // 令size 指向管道尾部,调整当前管道尾指针(前移chars 字节)。 size = PIPE_TAIL(*inode); PIPE_TAIL(*inode) += chars; PIPE_TAIL(*inode) &= (PAGE_SIZE - 1); // 将管道中的数据复制到用户缓冲区中。对于管道i 节点,其i_size 字段中是管道缓冲块指针。 while (chars-->0) put_fs_byte(((char *)inode->i_size)[size++], buf++); } // 唤醒等待该管道i 节点的进程,并返回读取的字节数。 wake_up(&inode->i_wait); return read; }读管道进程开始执行后,将从上一次读的位置开始把管道中的数据读出,当操作到管道尾端后也会将读管道指针从管道尾端移动到管道首端,从首端继续读取管道中的内容,直至将数据彻底读完。管道中的数据被读完后,读管道进程将再次被挂起,并切换到写管道进程去执行。