pipe函数内核实现

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.c
void 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.c

SYSCALL_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;

};


pipe_write中,有个大循环,就是不断往buf中写数据的功能:

//大循环,以为每个循环最多处理一个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 0
	sleep(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 0
	sleep(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。















  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1章 预备知识 1. 1 Linux内核简介 1. 2 Intel X86 CPU系列的寻址方式 1. 3 i386的页式内存管理机制 1. 4 Linux内核源代码中的C语言代码 1.5 Linux内核源代码中的汇编语言代码 第2章 存储管理 2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理页面的使用和周转 2.7 物理页面的分配 2.8 页面的定期换出 2. 9 页面的换入 2.10 内核缓冲区的管理 2.11 外部设备存储空间的地址映射 2.12 系统调用brk() 2.13 系统调用mmap() 第3章 中断、异常和系统调用 3.1 X86 CPU对中断的硬件支持 3. 2 中断向量表IDT的初始化 3. 3 中断请求队列的初始化 3. 4 中断的响应和服务 3. 5 软中断与Bottom Half 3.6 页面异常的进入和返回 3. 7 时钟中断 3. 8 系统调用 3. 9 系统调用号与跳转表 第4章 进程与进程调度 4.1 进程四要素 4.2 进程三部曲:创建、执行与消亡 4.3 系统调用fork()、vfork()与clone() 4.4 系统调用execve() 4.5 系统调用exit()与wait4() 4.6 进程的调度与切换 4.7 强制性调度 4.8 系统调用nanosleep()和pause() 4.9 内核中的互斥操作 第5章 文件系统 5.1 概述 5. 2 从路径名到目标节点 5. 3 访问权限与文件安全性 5. 4 文件系统的安装和拆卸 5.5 文件的打开与关闭 5. 6 文件的写与读 5.7 其他文件操作 5. 8 特殊文件系统/proc 第6章 传统的Unix进程间通信 6.1 概述 6.2 管道和系统调用pipe() 6.3 命名管道 6.4 信号 6. 5 系统调用ptrace()和进程跟踪 6.6 报文传递 6.7 共享内存 6.8 信号量 第7章 基于socket的进程间通信 7.1 系统调用socket() 7.2 函数sys—socket()——创建插口 7.3 函数sys—bind()——指定插口地址 7.4 函数sys—listen()——设定server插口 7.5 函数sys—accept()——接受连接请求 7.6 函数sys—connect()——请求连接 7.7 报文的接收与发送 7.8 插口的关闭 7.9 其他 第8章 设备驱动 8.1 概述 8.2 系统调用mknod() 8.3 可安装模块 8.4 PCI总线 8.5 块设备的驱动 8.6 字符设备驱动概述 8.7 终端设备与汉字信息处理 8.8 控制台的驱动 8.9 通用串行外部总线USB 8.10 系统调用select()以及异步输入/输出 8.11 设备文件系统devfs 第9章 多处理器SMP系统结构 9.1 概述 9.2 SMP结构中的互斥问题 9.3 高速缓存与内存的一致性 9.4 SMP结构中的中断机制 9.5 SMP结构中的进程调度 9.6 SMP系统的引导 第10章 系统引导和初始化 10.1 系统引导过程概述 10.2 系统初始化(第一阶段) 10.3 系统初始化(第二阶段) 10.4 系统初始化(第三阶段) 10.5 系统的关闭和重引导

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值