介绍
管道是进程间通信的一种方式,分为无名管道和有名管道两种。使用无名管道可以进行相关进程间通信(也就是父子进程),使用有名管道可以进行不相关进程(没有父子关系的进程)之间的通信。下边主要介绍下无名管道的实现机制。
用户态创建无名管道函数有pipe()和pipe2(),通常在命令行的一个命令输出中查找一些特定数据时,也常用到管道技术,如“ps -elf | grep program”,其也是调用pipe函数来创建一个管道进行通信。系统API函数如下:
#include <unistd.h> int pipe(int pipefd[2]); #define _GNU_SOURCE /* See feature_test_macros(7) */ #include <fcntl.h> /* Obtain O_* constant definitions */ #include <unistd.h> int pipe2(int pipefd[2], int flags);
管道大致框图如下:
linux内核管道就是内存中的一个管道缓冲区pipe_buf,可以理解为存在于内存中的一个文件
源码分析
接下来具体分析下管道的内核实现代码,两个函数陷入内核调用:
/* * sys_pipe() is the normal C calling standard for creating * a pipe. It's not the way Unix traditionally does this, though. */ SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags) { struct file *files[2]; int fd[2]; int error; //__do_pipe_flags为内核管道创建两个file结构体对象,并分配两个空的文件描述符fd,并进行填充 error = __do_pipe_flags(fd, files, flags); if (!error) { /* 将上一步和管道关联的2个fd拷贝到用户空间,copy_to_user函数执行成功,则接着执行else后边语句,否则执行if语句块中的部分。 unlikely()表示括号中的表达式极小的可能执行失败,若执行失败,则执行if语句块,否则执行else语句块。 此处是将fd数组先拷贝至至用户空间,然后将fd与file对象关联, */ if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) { fput(files[0]); fput(files[1]); put_unused_fd(fd[0]); put_unused_fd(fd[1]); error = -EFAULT; } else { //把fd和file的映射关系更新到该进程的文件描述符表中fdtable fd_install(fd[0], files[0]); fd_install(fd[1], files[1]); } } return error; } SYSCALL_DEFINE1(pipe, int __user *, fildes) { return sys_pipe2(fildes, 0); }
SYSCALL_DEFINE2中主要是调用__do_pipe_flags函数创建内核管道文件及指向管道文件的两个file结构体对象和两个新的文件描述符号,以上步骤没有报错则将文件描述符数组fd[]拷贝至用户态fildes数组,成功则将__do_pipe_flags中创建的的files对象添加至当前进程的文件描述符表fdtable中,即就是将文件描述符和文件对象关联,使得通过当前进程的files_struct对象(文件系统介绍可以参照博客)可以找到内核管管道文件。
static int __do_pipe_flags(int *fd, struct file **files, int flags) { int error; int fdw, fdr; if (flags & ~(O_CLOEXEC | O_NONBLOCK | O_DIRECT)) return -EINVAL; //为管道创建两个file结构体 error = create_pipe_files(files, flags); if (error) return error; //get_unused_fd_flags分配一个文件描述符,并标记为busy error = get_unused_fd_flags(flags); if (error < 0) goto err_read_pipe; fdr = error; error = get_unused_fd_flags(flags); if (error < 0) goto err_fdr; fdw = error; audit_fd_pair(fdr, fdw); //设置审计数据信息 fd[0] = fdr; fd[1] = fdw; return 0; err_fdr: put_unused_fd(fdr); err_read_pipe: fput(files[0]); fput(files[1]); return error; }
create_pipe_files函数实现主要是创建管道的file对象,接着调用的get_unused_fd_flags函数是对__alloc_fd函数的封装,调用__alloc_fd函数主要是分配一个文件描述符,并设置为busy,其大致过程为:首先对用户打开文件表files_struct结构体对象加锁,获取当前进程的文件描述符表,依次遍历该文件描述符表,找到一个未使用的文件描述符号,最后释放files_struct的对象锁。
接下来主要看下create_pipe_files函数:
int create_pipe_files(struct file **res, int flags) { int err; struct inode *inode = get_pipe_inode(); //为管道创建一个inode并进程初始化 struct file *f; struct path path; static struct qstr name = { .name = "" }; if (!inode) return -ENFILE; err = -ENOMEM; //分配一个目录项 path.dentry = d_alloc_pseudo(pipe_mnt->mnt_sb, &name); if (!path.dentry) goto err_inode; path.mnt = mntget(pipe_mnt); //引用计数加1 d_instantiate(path.dentry, inode); err = -ENFILE; //alloc_file函数分配并初始化一个写管道的file结构体对象,并传入pipe文件操作函数结构体对象 f = alloc_file(&path, FMODE_WRITE, &pipefifo_fops); if (IS_ERR(f)) goto err_dentry; f->f_flags = O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)); f->private_data = inode->i_pipe; //创建并初始化读管道的file对象 res[0] = alloc_file(&path, FMODE_READ, &pipefifo_fops); if (IS_ERR(res[0])) goto err_file; path_get(&path); res[0]->private_data = inode->i_pipe; res[0]->f_flags = O_RDONLY | (flags & O_NONBLOCK); res[1] = f; return 0; err_file: put_filp(f); err_dentry: free_pipe_info(inode->i_pipe); path_put(&path); return err; err_inode: free_pipe_info(inode->i_pipe); iput(inode); return err; }
该函数中最重要的两个函数就是get_pipe_inode和alloc_file。其中调用alloc_file函数完成file结构体对象的分配并初始化,在该函数中首先调用get_empty_filp函数找到一个未使用的file结构体对象,然后初始化file结构体的一些属性信息,如inode、读写权限、操作函数fop等。最后返回该初始化完成的file结构体对象。
get_pipe_inode函数主要为管道创建一个inode并初始化,如下:
static struct inode * get_pipe_inode(void) { struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb); struct pipe_inode_info *pipe; if (!inode) goto fail_inode; //分配一个inode号 inode->i_ino = get_next_ino(); //分配一个linux内核级管道pipe pipe = alloc_pipe_info(); if (!pipe) goto fail_iput; inode->i_pipe = pipe; pipe->files = 2; pipe->readers = pipe->writers = 1; inode->i_fop = &pipefifo_fops; /* * Mark the inode dirty from the very beginning, * that way it will never be moved to the dirty * list because "mark_inode_dirty()" will think * that it already _is_ on the dirty list. */ inode->i_state = I_DIRTY; inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR; inode->i_uid = current_fsuid(); inode->i_gid = current_fsgid(); inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME; return inode; fail_iput: iput(inode); fail_inode: return NULL; }
该函数中调用new_inode_pseudo函数分配一个inode结构体对象并分配inode号;调用alloc_pipe_info函数分配一个linux内核级管道pipe并初始化引用该管道的file结构体对象数为2(对应管道中的读写);然后将上一步分配的inode结构体的i_pipe成员赋值为分配的管道pipe,之后再进行一些inode的属性初始化即可。
alloc_pipe_info函数分配内核管道
当索引节点指的是管道时,其i_pipe字段指向一个如下所示的内核管道结构体pipe_inode_info:
struct pipe_inode_info { struct mutex mutex; //互斥锁mutex保护整个事件 wait_queue_head_t wait; //reader/writer等待,以防止empty/full管道 unsigned int nrbufs, curbuf, buffers; //nrbufs 这个管道缓冲区中非空的大小 //curbuf 当前管道缓冲区的入口 //buffers 缓冲区总的大小(应该为2的幂次方) unsigned int readers; //管道当前readers的大小 unsigned int writers; //管道当前writers的大小 unsigned int files; //引用该管道的file结构体数(受->i_lock保护) unsigned int waiting_writers; //阻塞等待writers的数目 unsigned int r_counter; //reader计数器 unsigned int w_counter; struct page *tmp_page; struct fasync_struct *fasync_readers; //用于通过信号进行的异步IO通知 struct fasync_struct *fasync_writers; struct pipe_buffer *bufs; //管道缓冲区描述符地址 };
管道除了一个索引节点和两个文件对象外,每个管道都还有自己的管道缓冲区pipe_buffer,pipe_inode_info结构体的bufs字段指向一个具有16个pipe_buffer对象的数组。
struct pipe_buffer { struct page *page; //管道缓冲区页框的描述符地址 unsigned int offset, len; //页框内有效数据的位置及长度 const struct pipe_buf_operations *ops; //管道缓冲区方法表的地址 unsigned int flags; unsigned long private; };
接下来看下linux中是如何分配一个linux内核管道pipe的。进一步查看alloc_pipe_info函数:
struct pipe_inode_info *alloc_pipe_info(void) { struct pipe_inode_info *pipe; //用kzalloc函数给pipe对象分配内存并初始化为0,相当于kmalloc+memset pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL); if (pipe) { //给管道pipe的缓冲区buf分配内存,每个管道的bufs存放一个具有16个pipe_buffer对象的数组 pipe->bufs = kzalloc(sizeof(struct pipe_buffer) * PIPE_DEF_BUFFERS, GFP_KERNEL); if (pipe->bufs) { init_waitqueue_head(&pipe->wait); //初始化等待队列头,空/满阻塞 pipe->r_counter = pipe->w_counter = 1; pipe->buffers = PIPE_DEF_BUFFERS; mutex_init(&pipe->mutex); return pipe; } kfree(pipe); } return NULL; }
该函数主要是分配pipe_inode_info结构体对象和管道的缓冲区。并且管道添加到内核等待队列头中,此处使用等待队列实现管道的特性,管道为空时,读端阻塞;管道写满时,写端阻塞。然后设置管道读写计数器为1.
总结
以上就是linux 无名管道的大致实现过程。管道实际就是借助文件系统的file结构和inode实现的。VFS中的inode索引节点指向内核中创建一个内核缓冲区,通过将两个file结构指向该inode索引节点。之后将这两个file结构体对象和files_struct结构体中的文件描述符号表进行关联,最后将文件描述符表fd[2]拷贝至用户态即可。