pipe函数内核实现
来源:互联网 发布:ping ip加端口怎么写 编辑:程序博客网 时间:2024/05/18 16:17
pipe源码分析
本文基于linux kernel 4.13 分析,与通用的2.6差距较大。请读者自行甄别本文的特性,是否符合自己当前环境。
本文要解决的问题
1:pipe源码分析
2:pipe大小限制
3:如果没有读(写)端了,那么我写(读)操作会发生什么。
父子进程之间通信,首先想到的是pipe函数,pipe函数返回2个fd。通常,fork前先调用pipe,fd[0]负责读,fd[1]负责写。
示例程序往往是这样
#include <stdio.h>#include <stdlib.h>#include <string.h>main(){ int pipefd[2]; int pid; int i, line; char s[100]={0}; if (pipe(pipefd) < 0) { perror("pipe"); exit(1); } pid = fork(); if (pid > 0)//父进程 { printf("fater writing....\n"); memcpy(s,"helloworld\n",strlen("helloworld\n")); write(pipefd[1], s, strlen(s)); close(pipefd[1]); } else//子进程 { printf("child reading....\n"); read(pipefd[0],s,1000); printf("read result: %s\n",s); close(pipefd[0]); }}
通常,还有人告诉你,上面程序中,父进程还需要关闭pipefd[0],子进程还需要关闭pipefd[1]。因为fork后,“子进程继承父进程文件描述符”,这里就不纠结细节了。
第一节:fd的创建以及关联
pipe() 系统调用对应的内核函数是sys_pipe,它由SYSCALL_DEFINE1宏来生成,由下面函数可以看到,sys_pipe实际调用了sys_pipe2函数。
fs/pipe.c(kernel 4.13)SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags){struct file *files[2];int fd[2];int error;/*__do_pipe_flags函数生成fd和files*/error = __do_pipe_flags(fd, files, flags);if (!error) {/*我们不关心错误处理*/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 {/*这里是核心,将一个int 型的 fd 和 一个struct file型的files关联起来*/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);}我们先看fd_install的实现,在看看__do_pipe_flags如何创建fd和files的。fs/file.cvoid fd_install(unsigned int fd, struct file *file){__fd_install(current->files, fd, file);}void __fd_install(struct files_struct *files, unsigned int fd,struct file *file){struct fdtable *fdt;spin_lock(&files->file_lock);fdt = files_fdtable(files);BUG_ON(fdt->fd[fd] != NULL);rcu_assign_pointer(fdt->fd[fd], file);spin_unlock(&files->file_lock);}
current用来描述当前进程,是一个struct task_struct类型的结构体。
每个current进程都有一个 文件描述符表,即 current->files->fdt
所以 上面的代码中,__fd_install 函数核心功能就是执行 current->files->fdt[fd] = files。这样,一个int 的fd和一个struct file的files对应起来了,当前进程的任何一个fd就能找到唯一个files。
fd我们都知道,但是 struct file 是上面东西?那 现在我们来看看__do_pipe_flags函数,上面说了,它创建了一对fd和一对files。根据pipe的用法,我们知道能猜到,这两个fd肯定是有关系的,否则不可能一个fd写,另一个fd能读,而且只有亲属关系的进程间才能这样。
又因为一个fd能关联到一个files,那么也就是说,我们需要实现两个files有关联,这样,两个fd就自然有关联了。
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;error = create_pipe_files(files, flags);if (error)return error;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;}
__do_pipe_flags函数关键是 create_pipe_files,而其他的函数例如get_unused_fd_flags,顾名思义,获取一个可用的fd号而已,通过这个函数获取一个fdr和fdw。
着重讲create_pipe_files函数,他创建了2个有关联的struct file 的 files。
int create_pipe_files(struct file **res, int flags){int err;struct inode *inode = get_pipe_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);d_instantiate(path.dentry, inode);err = -ENFILE;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;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;}
上面涉及到了文件系统的各个方面,所以比较复杂,我们这里不详细说各个数据结构的作用,
inode很重要,通过get_pipe_inode函数获取。
inode->i_fop = &pipefifo_fops;//指定了一对操作函数,告诉vfs如何读写等操作。
inode->i_pipe = pipe;
后续我们会关注,pipe中的数据结构。
pipe->bufs
pipe->tmp_page
files创建:
files[0] = alloc_file(&path, FMODE_READ, &pipefifo_fops);
files[1] = alloc_file(&path, FMODE_WRITE, &pipefifo_fops);
从第二个参数就知道,这两个file限制了各自的功能,也就意味着,对应的两个fd也限制了各自的功能。
d_instantiate(path.dentry, inode); 相当于执行了 path.dentry->d_inode = inode;
这样,alloc_file函数中,file->f_path = *path,就相当于file[x]->f_path.dentry->d_inode = inode,也即两个file指向了一个inode。
这个就是所谓的两个file关联。
下面这句话,简单的把pipe放在file中,方便取值,否则给定一个files,如果需要获取pipe,就需要从file->f_path.dentry->d_inode->i_pipe去的取得,这显然不合适。
files[0]->private_data = inode->i_pipe;
files[1]->private_data = inode->i_pipe;
至此sys_pipe函数执行完毕,它创建了2个互相关联的file,然后创建了2个fd,fd与file一一对应。
其次file都指向了同一个inode,我们可以想象,如果自己接下去实现读写,那么写的数据,肯定放在inode中了,事实也是如此。
第二节 通过fd[0]读和通过fd[1]写
读写操作,在用户态,都是read和write,其对于的内核函数分别是sys_read和sys_write。我们先看写操作,
fs/read_write.cSYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,size_t, count){struct fd f = fdget_pos(fd);ssize_t ret = -EBADF;if (f.file) {loff_t pos = file_pos_read(f.file);ret = vfs_write(f.file, buf, count, &pos);if (ret >= 0)file_pos_write(f.file, pos);fdput_pos(f);}return ret;}
入参很简单,一个fd,一个存放数据的buf,一个想要存放数据的长度值len。
fdget_pos 函数返回值是一个struct fd,不要被表面疑惑,他和fd没有任何关系。
这个函数通过fd,然后返回一个file结构,以及一些flag。通过第一节我们知道,一个fd如何和一个file关联的,我们也能想到,取得方法也很简单,current->files->fdt[fd],这样我们就能取到fd对应的file了。找到file后,执行了vfs_write,这里不像多谈虚拟文件系统,说白了就是一个中间层。
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos){ssize_t ret;if (!(file->f_mode & FMODE_WRITE))return -EBADF;if (!(file->f_mode & FMODE_CAN_WRITE))return -EINVAL;if (unlikely(!access_ok(VERIFY_READ, buf, count)))return -EFAULT;ret = rw_verify_area(WRITE, file, pos, count);if (ret >= 0) {count = ret;file_start_write(file);ret = __vfs_write(file, buf, count, pos);if (ret > 0) {fsnotify_modify(file);add_wchar(current, ret);}inc_syscw(current);file_end_write(file);}return ret;}
首先判断,这个fd对于的file是否可写,这与第一节中:
files[1] = alloc_file(&path, FMODE_WRITE, &pipefifo_fops);
这句话相辅相成,所以pipe创建的两个fd一个只能读,一个只能写,就是这个道理。
接着调用__vfs_write,也即调用 file->f_op->write,这个f_op,就是第一节中,alloc_file函数设置的pipefifo_fops。f_op->write 也就是 pipe_write 函数。
pipe_write函数比较长,就不一一列举了,我们需要知道的是,write之后的数据,被放进了哪里:
struct pipe_inode_info *pipe = filp->private_data;//取出pipe,这个第一节中说过。
pipe中有一个字段,用来管理写入的数据:
/**
*struct pipe_inode_info - a linux kernel pipe
*@mutex: mutex protecting the whole thing
*@wait: reader/writer wait point in case of empty/full pipe
*@nrbufs: the number of non-empty pipe buffers in this pipe
*@buffers: total number of buffers (should be a power of 2)
*@curbuf: the current pipe buffer entry
*@tmp_page: cached released page
*@readers: number of current readers of this pipe
*@writers: number of current writers of this pipe
*@files: number of struct file referring this pipe (protected by ->i_lock)
*@waiting_writers: number of writers blocked waiting for room
*@r_counter: reader counter
*@w_counter: writer counter
*@fasync_readers: reader side fasync
*@fasync_writers: writer side fasync
*@bufs: the circular array of pipe buffers
*@user: the user who created this pipe
**/
struct pipe_inode_info {
struct mutex mutex;
wait_queue_head_t wait;
unsigned int nrbufs, curbuf, buffers;
unsigned int readers;
unsigned int writers;
unsigned int files;
unsigned int waiting_writers;
unsigned int r_counter;
unsigned int w_counter;
struct page *tmp_page;
struct fasync_struct *fasync_readers;
struct fasync_struct *fasync_writers;
struct pipe_buffer *bufs;//用来存放write的数据,他是一个链表。
struct user_struct *user;
};
//大循环,以为每个循环最多处理一个page_size大小的数据for (;;) {int bufs;//没有读端了直接发送sigpipe信号给进程。if (!pipe->readers) {send_sig(SIGPIPE, current, 0);if (!ret)ret = -EPIPE;break;}bufs = pipe->nrbufs;//如果当前有可用的buf,则进入判断if (bufs < pipe->buffers) {int newbuf = (pipe->curbuf + bufs) & (pipe->buffers-1);struct pipe_buffer *buf = pipe->bufs + newbuf;struct page *page = pipe->tmp_page;int copied;//page为空,说明,之前拷贝的数据满了一个page_size,page不能接着用了。需要重新申请一个。if (!page) {page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);if (unlikely(!page)) {ret = ret ? : -ENOMEM;break;}pipe->tmp_page = page;}/* Always wake up, even if the copy fails. Otherwise * we lock up (O_NONBLOCK-)readers that sleep due to * syscall merging. * FIXME! Is this really true? */do_wakeup = 1;copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);if (unlikely(copied < PAGE_SIZE && iov_iter_count(from))) {if (!ret)ret = -EFAULT;//page未写满,意味着送入的数据已经全部拷贝完成,就break退出。break;}ret += copied;/* Insert it into the buffer array *///到这里,全部记录在buf中。buf->page = page;buf->ops = &anon_pipe_buf_ops;buf->offset = 0;buf->len = copied;buf->flags = 0;if (is_packetized(filp)) {buf->ops = &packet_pipe_buf_ops;buf->flags = PIPE_BUF_FLAG_PACKET;}pipe->nrbufs = ++bufs;pipe->tmp_page = NULL;//没有要写的数据了,回去了。if (!iov_iter_count(from))break;}if (bufs < pipe->buffers)continue;//走到这,说明pipe 中可用的buf全部满了,如果设置了非阻塞,则回到用户态if (filp->f_flags & O_NONBLOCK) {if (!ret)ret = -EAGAIN;break;}if (signal_pending(current)) {if (!ret)ret = -ERESTARTSYS;break;}if (do_wakeup) {wake_up_interruptible_sync_poll(&pipe->wait, POLLIN | POLLRDNORM);kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);do_wakeup = 0;}pipe->waiting_writers++;pipe_wait(pipe);pipe->waiting_writers--;}
任何读写操作的逻辑近乎类似,无论socket的读写还是pipe的读写,上面的逻辑其实几句话就可以概括:
1:每次write调用,就在一个pipe->buffs队列中申请一个可用的buf,以及申请一个page。
2:拷贝数据至page,然后page放入buf中。
3:如果write的数据全部处理完毕,则return。
4:如果write的数据没有被处理完毕,判断当前buf是否够用,够用则执行1,不够用,判断当前fd是否是阻塞的,阻塞的就睡眠进程,非阻塞的,就范围EAGAIN。
所以,问题来了
问题1:一个pipe最大能写多大的数据?
答案是pipe->buffers*PAGE_SIZE超过该数之后,write将被阻塞。
pipe->buffers*PAGE_SIZE = 16 * 4k = 65536字节
不过pipe->buffers值往往不都是16个,详情请看get_pipe_inode中,对该队列申请大小的判断条件,这里不再展开。
对于网上说的PIPE_BUF限制,当前内核版本已经不存在了。
问题2:如果每次write 1字节,那么write16次,buf队列就不够用了
非也,pipe_write在你实际写操作前,有个merge的操作
/* We try to merge small writes */chars = total_len & (PAGE_SIZE-1); /* size of the last buffer */if (pipe->nrbufs && chars != 0) {int lastbuf = (pipe->curbuf + pipe->nrbufs - 1) &(pipe->buffers - 1);struct pipe_buffer *buf = pipe->bufs + lastbuf;int offset = buf->offset + buf->len;if (buf->ops->can_merge && offset + chars <= PAGE_SIZE) {ret = pipe_buf_confirm(pipe, buf);if (ret)goto out;ret = copy_page_from_iter(buf->page, offset, chars, from);if (unlikely(ret < chars)) {ret = -EFAULT;goto out;}do_wakeup = 1;buf->len += ret;if (!iov_iter_count(from))goto out;}
pipe_readd这里就不进行分析了,就是循环buf队列,得到想要的数据,如果只获取了一个buf中的部分数据,则记录下offset即可,下次从offset处接着获取。
第三节 pipe的另一端关闭了,会发生什么
首先,pipe如何判断有没有对端?
struct pipe_inode_info 有2个字段readers以及writers,创建pipe时,即在get_pipe_inode中,各自赋值为1。
我们把文章中的程序改成这样
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>void main(){ int pipefd[2]; int pid,ret; int i, line; char s[4*1024*16+1]={0}; if (pipe(pipefd) < 0) { perror("pipe"); exit(1); } pid = fork(); if (pid > 0)//父进程 { close(pipefd[0]); sleep(1); //printf("fater writing....\n"); //memcpy(s,"helloworld\n",strlen("helloworld\n")); ret = write(pipefd[1], s, sizeof(s)); printf("ret:%d\n",ret); perror(""); close(pipefd[1]); } else//子进程 {close(pipefd[0]);close(pipefd[1]);return;#if 0sleep(10); printf("child reading....\n"); //read(pipefd[0],s,1000); printf("read result: %s\n",s); close(pipefd[0]);#endif }}
父进程关闭自己的读端,然后让出cpu,子进程关闭自己的读写两端。父进程再往写端数据。
我的系统什么都没有打出,显然write后面没有运行。
我们捕获一下SIGPIPE信号
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <signal.h>void handler(){printf("sigpipe\n");perror("");}void main(){ int pipefd[2]; int pid,ret; int i, line; char s[4*1024*16+1]={0}; struct sigaction sig; sig.sa_handler = handler; sig.sa_flags = 0; sigaction(SIGPIPE, &sig, NULL); if (pipe(pipefd) < 0) { perror("pipe"); exit(1); } pid = fork(); if (pid > 0)//父进程 { close(pipefd[0]); sleep(1); //printf("fater writing....\n"); //memcpy(s,"helloworld\n",strlen("helloworld\n")); ret = write(pipefd[1], s, sizeof(s)); printf("ret:%d\n",ret); perror(""); close(pipefd[1]); } else//子进程 {close(pipefd[0]);close(pipefd[1]);return;#if 0sleep(10); printf("child reading....\n"); //read(pipefd[0],s,1000); printf("read result: %s\n",s); close(pipefd[0]);#endif }}
果然,有结果了,提示 Broken pipe。
我回过头看看pipe_write发现有这么一段代码:
if (!pipe->readers) {send_sig(SIGPIPE, current, 0);ret = -EPIPE;goto out;}
但是在pipe_read中,当发现write端不存在时,只是返回0,不会产生SIGPIPE。
看这个判断,核心就是pipe->readers字段,我们的示例程序父子进程全都执行了close(fd[0]),这样,pipe的read端就彻底关闭了。至于为什么,我会在后续的父子进程描述符继承中讲,这里就简单讲一下。
首先,fork前,fd[0] 对应的file,file中引用计数为1,fork后,子进程继承了父进程的描述符,copy_process函数中把file引用计数加1,这样,file的引用计数为2,所以父子进程需要各自执行close(fd[0])才能把读端file的引用计数减成0,file才能彻底释放对应的pipe->reader才能为0。
- pipe函数内核实现
- linux内核管道pipe实现详解
- 从内核代码聊聊pipe的实现
- pipe 函数
- 【底层开发】内核Pipe
- unp pipe popen函数
- PIPE函数的例子
- unp pipe popen函数
- Linux pipe函数
- pipe函数使用
- pipe函数详解
- Linux pipe函数
- Linux pipe函数
- Linux pipe 函数
- Linux pipe函数
- Linux pipe函数
- pipe()函数的使用
- Linux pipe函数
- 创建线程的两种传统方式
- 最小生成树MST(Prim/Kruskal模版)
- Makefile工程管理语法与使用技巧
- java正则表达式
- LightOJ-1314 Aladdin and the Flying Carpet
- pipe函数内核实现
- 表格冻结首列及左起指定列数(CongelarFilaColumna插件的使用)
- 一. 流式处理简介
- b reset 和 ldr pc ,reset 的区别?
- spring-boot-mybatis-cache-thymeleaf学习
- Week03_day05 集合(下)
- Ubuntu系统环境变量详解
- mysql5.7官网直译数据结构优化--数据大小优化
- uva1583