管道分为有名管道和无名管道。我们先从无名管道开始。
无名管道用于父子进程、兄弟进程之间的通信。管道的主体是pipe,pipe所建立的管道只在一个进程里,所以我们要通过fork来实现进程间通信。
我们使用管道就可以看出,对管道的读写,实际上就是read和write。那是不是说,管道实际上就是一个文件呢?
是的,在具体的实现上,管道是作为匿名文件存在的,fd[2]里的两个元素实际上就是打开文件号。
pipe会调用底层sys_pipe,sys_pipe实际上又会调用do_pipe,也就是创建管道。
do_pipe格式:
do_pipe(int *fd);
我们要注意一点,这个fd并不是我们再调用pipe时给的fd,是sys_pipe里给的一个fd[2]数组。至于为什么,我们待会说。
前面我们提到了,无名管道是存在内存上的文件,进程对每一个已打开文件的操作都是通过一个file数据结构进行的,只有在同一个进程按照相同模式重复打开同一个文件的时候才共享同一个数据结构。do_pipe里回创建两个文件指针f1,f2,并且为f1,f2各分配一个file数据结构,同时,每个进程在打开某个文件的时候会有一个打开文件号,所以得给f1f2各分配一个打开文件号。一个进程默认最多能够打开1024个文件。
每一个文件都是由一个inode数据机构代表的,所以还得为这个文件弄一个inode数据结构出来。
实际上,创建一个管道的过程就是创建一个文件的过程。
接下来,系统会先调用一些方法来分配一个内存页面作为管道的缓冲区,在分配一个缓冲区作为pipe_inode_info数据结构。这个结构比较特殊,只有在inode数据结构所代表的文件是用来实现一个管道的时候才会使用它。这个结构记录着管道的一些当前文件被使用的次数。
struct pipe_inode_info
{
wait_queue_head_t wait;
char *base;
unsigned int start;
unsigned int readers;
unsigned int writers;
unsigned int waiting_readers;
unsigned int waiting_writers;
unsigned int r_counter;
unsigned int w_counter;
}
在正常情况下,每个文件都要有一个目录项,代表这个文件的路径名,每个目录项都之描述一个文件,但是管道在内存上,按道理来说是不需要这个目录项的,实际上,管道有这么个东西。为什么呢?在file数据结构里并没有直接指向相应inode结构的指针,一定要一个目录项中转一下才行,这样一来,do_pipe就不得不分配一个目录项了,然后 使分配好的inode结构与这个目录项挂上钩,并且让两个已打开文件结构中的f_dentry指针指向这个目录项。
正是由于这个目录项是“不得不加上去的”,所以这个目录项只允许一种操作,就是把它删除。
最后是一部分关于管道读写的源码:
Int do_fork(int*fd)
{
……
/* read file */
f1->f_pos = f2->f_pos = 0;
f1->f_flags = O_RDONLY;
f1->f_op = &read_pipe_fops;
f1->f_mode = 1;
f1->f_version = 0;
/* write file */
f2->f_flags = O_WRONLY;
f2->f_op = &write_pipe_fops;
f2->f_mode = 2;
f2->f_version = 0;
fd_install(i, f1);//把f1和文件绑定
fd_install(j, f2);//把f2和文件绑定
fd[0] = i;
fd[1] = j;
Return 0;
}
这段代码很简单,就是使fd[0]作为读端的打开文件号,fd[1]作为写端的打开文件号。
函数结束返回到sys_pipe的时候系统空间的管道会被复制到当前进程的用户空间。这也就是为什么sys_fork在调用do_fork的时候不用我们再调用pipe时给的fd数组而是自己创建了一个fd数组。
fork之后,父进程关闭读端(或写端),子进程关闭写端(或读端)完成进程通信。那么,如果要实现兄弟进程通信,又该怎么做呢?
父进程fork一下,子进程1关闭读端(或写端),父进程关闭写端(或读端)。接下来父进程在fork一下,然后关闭读端,子进程2就掌握写端,实现兄弟进程通信。如下图
管道的缓冲区是一个页面4K,如果写的东西大于4K,写端进程就会进入睡眠并且唤醒读端进程读数据。读端被唤醒后会读数据,读完之后会进入睡眠并且唤醒写端进程写数据。实际上管道是生产者/消费者的一种体现。
再说说有名管道。
有名的意思就是有一个文件名。这样就使得任何进程都可以通过文件名或者路径名与这个文件挂上钩,这种文件的访问严格遵循先进先出原则,并且不允许有在文件内移动读写指针位置的lseek()操作。
有名管道与无名管道的区别仅仅在于打开的过程。我们前面提到了,无名管道是在内存里生成一个文件来进行通信的,但是有名管道是在磁盘上生成的文件。所以在打开的时候,进程调用open()会由sys_open()进入file_open(),然后在open_namei()中调用path_walk(),根据文件的路径名在文件系统里找到代表这个文件的inode。在fifo文件被第一次打开的时候,由于还没有设置缓冲区,所以会调用page_new()来创建一个缓冲区。
fifo文件可以按照不同的模式打开,只读、只写和读写。在系统调用的时候还有个参数flags。如果我们在打开fifo文件的时候使用了非阻塞模式使得其标志位O_NONBLOCK为1,那么在打开的过程中及时有些条件没满足也不会睡眠,而是直接返回。
举个例子,在写端的操作上,如果fifo文件没被打开,并且使用了阻塞模式,O_NONBLOCK为0,这种情况下生产者会进入睡眠等待消费者打开管道的读端。如果使用了非阻塞模式,O_NONBLOCK为1,就会打开失败,释放掉所有已经分配的资源并且返回-1。