文件在多大程度、多大范围共享

一、文件的重要性
文件在Unix系统中绝对是一个说之不尽的话题,也是一个非常重要的概念。对于文件,默认是子进程会继承父进程的文件描述符,而内核则负责init进程的三个文件描述符(标准输入、标准输出、标准错误,下同)。我们知道,对于同一个文件描述符,在不同的进程中可能代表不同的内容,但是如果是从init派生出来的所有的进程,一般大家是不会修改自己的三个标准文件描述符的,这就意味这个系统中所有的进程如果都是通过简单的fork,然后都不修改自己的三个文件描述符,那么它们指向的应该是相同的内容。
现在的问题是,这个“相同”到底是一个什么样的相同,是一个什么级别的相同?假设一个进程通过fcntl修改了自己的标准文件描述符,那么这个修改对其它的进程是否可见呢?如果是的话,会不会很荒唐呢?
二、内核中文件继承相关处理
do_fork--->>>copy_process-->>>copy_files--->>dup_fd
    newf = alloc_files();
……
            get_file(f);
这里的名字很有迷惑性,从名字上看会分配文件描述符,但是事实上是不会的,也就是这些操作都不会分配新的文件描述符,这个文件描述符指的就是内核中的struct file 结构,它里边有上下文f_pos数值,文件标识f_flags等信息,而其中的get_file只是增加这个文件的引用计数而不会分配一个新的结构,事实上这个分配动作只有在get_empty_filp函数来完成。
在执行execve的时候,对于文件的操作为
do_execve--->>load_elf_binary--->>flush_old_exec--->>>flush_old_files
for (;;) {
        unsigned long set, i;

        j++;
        i = j * __NFDBITS;
        fdt = files_fdtable(files);
        if (i >= fdt->max_fds)
            break;
        set = fdt->close_on_exec->fds_bits[j];
        if (!set)如果close_on_exec位图表为空,那么直接跳过相关文件描述符。
            continue;
        fdt->close_on_exec->fds_bits[j] = 0;close_on_exec属性不继承。
        spin_unlock(&files->file_lock);
        for ( ; set ; i++,set >>= 1) {
            if (set & 1) {
                sys_close(i);对于设置了该属性的,执行关闭操作。
            }
        }
        spin_lock(&files->file_lock);
    }
从上面可以看到,默认情况下,执行exec之后,子进程是不会关闭文件描述符的,而这个文件描述符指向的内核struct file是和父进程使用相同实例的,这意味着子进程对该文件的操作将会影响父进程对该文件的操作。推而广之,系统中所有的进程,只要是init直接派生的都会有这种问题。
三、何时分配一个内核态struct file实例
sys_open--->>>do_sys_open--->>do_filp_open--->>>open_namei--->>>path_lookup_open--->>>__path_lookup_intent_open--->>get_empty_filp
这意味着一个问题,虽然看起来很傻很天真,但是子进程和父进程如果操作同一个文件,只要子进程再次执行一下对该文件的open操作就可以了,这样内核会为进程分配一个新的struct file实例,从而在用户态看来就有自己的上下文,它对文件属性的设置也只影响自己。这一点在普通文件上看起来没有什么意义,但是对于串口来说就比较有意义了。
假设说从init派生出来的所有任务共享的标准输出都是一个串口设备,例如/dev/ttyS1,上面运行一个交互式shell,然后shell派生一个子进程,这个子进程通过fcntl(STDIN_FILENO,F_SETFL,O_NONBLOCK)来将这个串口设置为非阻塞,这个子进程退出之后,父进程从串口中读取数据的时候将会马上返回失败,这明显不是我们希望看到的结果。
这里补充说一下,用户态的dup和dup2都不能让内核分配一个新的struct file结构,而只是让文件多一个引用。由于init任务的第一文件描述符是open创建的,后两个都是通过dup创建的,所以这意味着默认情况下,所有进程的所有标准文件描述符都是相同的,任何一个进程对任何一个标准文件的fcntl的修改都将会影响到其它进程的任意一个文件描述符
四、验证
在bash下运行这个程序
[tsecer@Harry CoAct]$ cat CoAct.c 
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#if 0
#undef O_NONBLOCK
#define O_NONBLOCK O_APPEND
#endif
int main(int argc, char * argv[])
{
int flags ;
flags = fcntl(STDOUT_FILENO,F_GETFL);
printf("flags is %#x O_NONBLOCK is %#x ",flags,O_NONBLOCK);
flags ^= O_NONBLOCK;
if(0 == fcntl(STDOUT_FILENO,F_SETFL,flags))
printf("New flags is %#x ",flags);
flags = fcntl(STDOUT_FILENO,F_GETFL);
printf("flags is %#x O_NONBLOCK is %#x ",flags,O_NONBLOCK);

}
[tsecer@Harry CoAct]$ cat Makefile 
default:
    gcc CoAct.c -g -o CoAct.exe 

[tsecer@Harry CoAct]$ make
gcc CoAct.c -g -o CoAct.exe 
[tsecer@Harry CoAct]$ ./CoAct.exe   第一次执行结果
flags is 0x2002 O_NONBLOCK is 0x800
New flags is 0x2802
flags is 0x2802 O_NONBLOCK is 0x800
[tsecer@Harry CoAct]$ ./CoAct.exe  第二次执行结果,可以看到,两次执行结果相同,和推测情况不同,也就是子进程设置的标志非阻塞
flags is 0x2002 O_NONBLOCK is 0x800 标志位在退出之后没有生效。
New flags is 0x2802
flags is 0x2802 O_NONBLOCK is 0x800
[tsecer@Harry CoAct]$ 
这个让我有些意外,也有些怅然,但是考虑到bash可能会做手脚,所以看一下bash的代码,其中有这个一个函数
bash-4.1lib eadlineshell.c
sh_unset_nodelay_mode (fd)
  if ((flags = fcntl (fd, F_GETFL, 0)) < 0)
    return -1;

  bflags = 0;

#ifdef O_NONBLOCK
  bflags |= O_NONBLOCK;
#endif

#ifdef O_NDELAY
  bflags |= O_NDELAY;
#endif

  if (flags & bflags)
    {
      flags &= ~bflags;
      return (fcntl (fd, F_SETFL, flags));
    }
大致就是强制清空非阻塞标志,也就是readline是一定会阻塞的。而测试的时候刚好就是使用了这个O_NONBLOCK标志,所以看起来比较顽固。只好换一个“小众”一点的标志,将上面代码中的
#if 0
#undef O_NONBLOCK
#define O_NONBLOCK O_APPEND
#endif
修改为
#if 1
#undef O_NONBLOCK
#define O_NONBLOCK O_APPEND //使用一个“小众”一点的O_APPEND属性,为了减少修改,这里使用了一个猥琐的宏。
#endif
重新执行
[tsecer@Harry CoAct]$ ./CoAct.exe                        第一次执行
flags is 0x2002 O_NONBLOCK is 0x400
New flags is 0x2402
flags is 0x2402 O_NONBLOCK is 0x400
[tsecer@Harry CoAct]$ ./CoAct.exe                      第二次执行
flags is 0x2402 O_NONBLOCK is 0x400
New flags is 0x2002
flags is 0x2002 O_NONBLOCK is 0x400
[tsecer@Harry CoAct]$ ./CoAct.exe                       第三次执行
flags is 0x2002 O_NONBLOCK is 0x400
New flags is 0x2402
flags is 0x2402 O_NONBLOCK is 0x400
可以看到,这个修改可以在子进程中相互影响。
五、影响
这也就是说,在没有shell直接参与的情况下,如果是自动自动的一系列任务,它们的标准输入和输入是相互影响的,任何一个子进程对三个描述符的修改都将会对其它进程可见。这一点在通常环境中没有问题,也正是大部分情况下行为正确的一种特征。但是在嵌入式系统中,这个操作很可能会影响读写操作以及其它操作的行为,从而产生怪异影响。这就不是单单一个程序中的问题,而是一个系统级问题,一般不太容易定位。

原文地址:https://www.cnblogs.com/tsecer/p/10486174.html