Linux中的管道
作者:互联网
Linux中的管道
- 在lab6中学习了管道相L关的概念,为了加深理解,总结了真正的Linux系统中管道的相关内容。主要侧重于Linux的源码解读。
什么是管道
- 管道(pipe)是Linux系统中重要的进程间通信(IPC)机制,从本质上说,管道也是一种文件,也是遵循UNIX的“一切皆文件”的原则设计的。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux管道实际上就是一个操作方式为文件的内存缓冲区。
管道的分类
-
匿名管道(也就是我们所实现的管道)
匿名管道最常见的形态就是我们在shell操作中最常用的”|”。它的特点是只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,当然也降低了管道了通用性。
-
命名管道
匿名管道只能用于有亲缘关系的进程之间,而命名管道则可以用于任意进程之间。与匿名管道不同的是,命名管道会借用FIFO文件来实现。FIFO文件在文件系统中创建,有自己的路径和名称,但是它仅仅作为一个管道的标记,其数据仍然由内核管理,存放在内存中。FIFO这个名称正好也揭示了管道数据先入先出的本质。
我们可以通过系统调用
int mkfifo(const char *filename, mode_t mode)
来创建一个FIFO文件,而进程通过open()打开FIFO文件的方式决定了它是读端(O_RDONLY)还是写端(O_WRONLY)。注意不能以读写方式(O_RDWR)打开FIFO文件。特别地,若是设置了非阻塞标志位O_NONBLOCK的话,如果有一个进程试图以读/写方式打开一个没有正在被写/读的FIFO文件,open()就会立即返回成功,反之则会阻塞。而read()/write()的阻塞方式与匿名管道是完全相同的。 最后,如果FIFO文件的读端和写端都关闭,它也不会消失,而是留在文件系统中以便其他进程使用。
Linux的管道实现
-
Linux的管道实现主要在fs文件夹下的pipe.c中实现。
-
程序包括管道文件读写操作函数read_pipe()和write_pipe(), 同时实现了管道系统调用sys_pipe()。
-
在初始化管道时,管道i节点的i_size字段中被设置为指向管道缓冲区的指针,管道数据头部指针存放在i_zone[0]字段中,而管道数据尾部指针存放在i_zone[1]字段中(这一步的设定是不是非常熟悉,i_zone[0]和i_zone[1]可以分别对标fd[0]、fd[1])。
-
对于读管道操作,数据是从管道尾读出,并使管道尾指针前移读取字节数个位置;对于往管道中的写入操作,数据是向管道头部写入,并使管道头指针前移写入字节数个位置,这里的操作和MOS的实现十分类似,只不过MOS对文件的管理做了简化。
-
read_pipe()用于读管道中的数据。若管道中没有数据,就唤醒写管道的进程,而自己则进入睡眠状态。若读到了数据,就相应地调整管道头指针,并把数据传到用户缓冲区中。当把管道中所有的数据都取走后,也要唤醒等待写管道的进程,并返回已读数据字节数。当管道写进程已退出管道操作时,函数就立刻退出,并返回已读的字节数。
-
write_pipe()函数的操作与读管道函数类似。
-
系统调用sys_pipe()用于创建无名管道。它首先在系统的文件表中取得两个表项,然后在当前进程的文件描述符表中也同样寻找两个未使用的描述符表项,用来保存相应的文件结构指针。接着在系统中申请一个空闲i节点,同时获得管道使用的一个缓冲块。然后对相应的文件结构进行初始化,将一个文件结构设置为只读模式,另一个设置为只写模式。最后将两个文件描述符传给用户。这个函数和MOS中/user/pipe.c中的
int pipe(int pfd[2])
函数类似 -
下面是源代码分析
/* *linux/fs/pipe.c * *(C)1991 Linus Torvalds */ #include <signal.h> ... ... // 引用了一些头文件,不太懂都是具体干什么的 // 管道读操作函数。 // 参数inode是管道对应的i节点,buf是数据缓冲区指针,count是读取的字节数。 int read_pipe(struct m_inode* inode,char buf,int count) { int chars,size,read =0; // 若欲读取的字节计数值count大于O,则循环执行以下操作。 while (count>0){ //若当前管道中没有数据(size=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。 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; } ////管道写操作函数。 //参数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=O),则唤醒等待该节点的进程,如果己没有读管道者,则向进程发送SIGPIPE信号,并返回已写入的字节数并退出。若写入0字节,则返回-1。否则在该i节点上休眠,等待管道腾出空间。 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); } //取管道头部到缓冲区末端空间字节数chars。如果其大于还需要写入的字节数count,则令其等于count。如果chars大于当前管道中空闲空间长度size,则令其等于size。 chars = PAGE_SIZE-PIPE_HEAD(*inode); 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节点,其isize字段中是管道缓冲块指针。 while (chars-->0) ((char *)inode->i_size)[size++]=get_fs_byte(buf++); } //唤醒等待该i节点的进程,返回已写入的字节数,退出。 wake up(&inode->i wait); return written; } //创建管道系统调用函数。 //在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].count) (f[j++]=i+file_table)->f_count++; //如果只有一个空闲项,则释放该项(引用计数复位)。 if(j==1) f[0]->f_count=0: //如果没有找到两个空闲项,则返回-1。 if (j<2) return -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: //如果没有找到两个空闲句柄,则释放上面获取的两个文件结构项(复位引用计数值),并返回-1。 if(j<2){ f[0]->f_count=f[1]->f_count=0; return -1: } //申请管道i节点,并为管道分配缓冲区(1页内存)。如果不成功,则相应释放两个文件句柄和文 件结构项,并返回-1。 if ((inode=get_pipe_inode())){ current->filp[fd[0]] = current->filp[fd[1]] = NULL: f[0]->f_count f[1]->f_count = 0; return -1; //初始化两个文件结构,都指向同一个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*/ //将文件句柄数组复制到对应的用户数组中,并返回,退出。 put_fs_long(fd[0],0+fildes); put_fs_long(fd[1],1+fildes); return 0; } }
可见虽然MOS和真正的linux在pipe的实现细节上有很大的不同(尤其是在进程同步的问题上),但是大体思路基本相同,管道这种伪文件的思想值得我们借鉴与学习
标签:count,字节数,chars,管道,Linux,inode,size 来源: https://www.cnblogs.com/0xiao-hei0/p/16379903.html