fork-父子进程读写文件的偏移量(linux4.0.4)
一、背景
本文阐述的问题是:fork进程后父子进程操作文件的偏移量是否相同?
该问题可以用以下代码来展示,如您能知道代码执行后forkfile文件的内容是什么,那么请略过此文。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { pid_t pid = 0; int fd = -1; char buf[4] = { 'a', 'b', 'c', 'd'}; char buf2[4] = { '1', '2', '3', '4'};
unlink("./forkfile"); fd = open("./forkfile", O_CREAT|O_RDWR|O_TRUNC); if ( -1 == fd) { printf("open ./forkfile failed\n"); return -1; } write(fd, &buf[0], sizeof(buf)); pid = fork(); if (pid > 0) { printf("this is parent process, fd=%d\n", fd); } else if (0 == pid){ printf("this is child process, fd=%d\n", fd); write(fd, &buf2[0], sizeof(buf)); } else { printf("fork failed\n"); } close(fd); return 0; } |
代码执行后,forkfile文件内为:abcd1234,可以看出fork后,父子进程的文件偏移量是相同的。
二、代码分析
1)进程管理文件的数据结构struct files_struct
文件系统模块涉及到struct file、struct dentry、struct inode,三者的关系为
进程用struct file结构来记录打开文件的信息,进程每次打开一个文件,就会用一个struct file结构来记录信息,同一个文件打开多次(没有close)也会得到多个struct file结构。struct file->f_path.dentry、struct file->f_inode、struct dentry->d_inode这些字段是vfs的基本组件,构成了vfs的基本框架,不过不属于本文的描述范围,本文关注struct file->f_pos字段,该字段用于记录被打开的文件的偏移量,read、write操作就是从该偏移量处开始操作文件的。
一个进程会有很多struct file结构,所有的struct file结构构成一个数组,由struct files_struct管理。用户态是看不到这些struct file结构的,只能看到fd,fd就是该数组的索引,内核通过fd在stuct file数组中找到对应的struct file结构,就能知道文件的偏移量信息。struct files_struct定义如下:
struct files_struct { /* * read mostly part */ atomic_t count; struct fdtable __rcu *fdt; struct fdtable fdtab; /* * written part on a separate cache line in SMP */ spinlock_t file_lock ____cacheline_aligned_in_smp; int next_fd; unsigned long close_on_exec_init[1]; unsigned long open_fds_init[1]; struct file __rcu * fd_array[NR_OPEN_DEFAULT]; }; |
struct files_struct结构分为三部分,第一部分是files_struct->fdt,第二部分是files_struct->fdtab,剩下的字段是第三部分。内核以files_struct->fdt中的信息为准,可以查询到所有fd对应的struct file,files_struct->fdtab及后面的字段是一些辅助字段,用于初始化files_struct->fdt。默认情况下files_struct->fdt = &files_struct->fdtab,files_struct->fdtab中的一些字段用struct files_struct中的第三部分字段初始化。所以核心在于struct fdtable结构,定义如下:
struct fdtable { unsigned int max_fds; struct file __rcu **fd; /* current fd array */ unsigned long *close_on_exec; unsigned long *open_fds; struct rcu_head rcu; }; |
fork进程时,需要把父进程的struct files_struct中的信息复制到子进程,所以fork先分配一块内存作为子进程的struct files_struct,内存示意图如下(只列出了关键字段):
然后通过do_fork-->copy_process-->copy_files-->dup_fd对files_struct->fdtab初始化,初始化后各字段值如下:
此时,子进程申请的struct files_struct与父进程没有任何关系,struct files_struct->fd_array[]是一个struct file数组,里面的指针都是空的。fork通过dup_fd的如下代码将父进程中的struct file指针拷贝到子进程中:
struct file **old_fds, **new_fds; struct fdtable *old_fdt, *new_fdt;
old_fdt = files_fdtable(oldf);
old_fds = old_fdt->fd; new_fds = new_fdt->fd;
for (i = open_files; i != 0; i--) { struct file *f = *old_fds++; if (f) { get_file(f); } else { /* * The fd may be claimed in the fd bitmap but not yet * instantiated in the files array if a sibling thread * is partway through open(). So make sure that this * fd is available to the new process. */ __clear_open_fd(open_files - i, new_fdt); } rcu_assign_pointer(*new_fds++, f); } |
代码检查父进程struct file **old_fds中的元素是否为空,如果不空,则把元素值(struct file*类型)赋付给子进程struct file **new_fds中对应的位置。从这个赋值过程可以看出,父子进程的struct file都是一样的(父子进程都指向同一个struct file),所以文件偏移量struct file->f_pos也一样。
2)父子进程修改struct file结构的字段时,对方能看到吗?
根据常识父子进程是独立的,修改struct file结构的字段值对方是看不到的,但是根据前面分析(父子进程的struct file都是一样的(父子进程都指向同一个struct file),所以文件偏移量struct file->f_pos也一样),父子进程操作的是同一个struct file,一个人修改,另一个人应该能看到啊-----这是错误的。
这里涉及到写时复制机制,父子进程都指向同一个struct file,这只是指父子进程struct file数据结构的虚拟地址是相同的,但是物理地址是不同的。fork进程时,父进程的页表项被标记为只读,当父子进程中任意一个写值时,会产生缺页异常,建立虚拟地址与物理地址的映射关系,然后在新的物理地址上进行写,从而与另一个进程独立,不会相互影响。
3)init进程的struct files_struct
fork进程会将父进程struct files_struct中的信息复制给子进程,那么最原始的struct files_struct是哪里来的?这是在file.c中静态定义的:
struct files_struct init_files = { .count = ATOMIC_INIT(1), .fdt = &init_files.fdtab, .fdtab = { .max_fds = NR_OPEN_DEFAULT, .fd = &init_files.fd_array[0], .close_on_exec = init_files.close_on_exec_init, .open_fds = init_files.open_fds_init, }, .file_lock = __SPIN_LOCK_UNLOCKED(init_files.file_lock), }; |