dup, pipe, epoll_create 这些函数都有一些变种,dup3, pipe2, epoll_create1
。为什么会这样?原因是这些接口设计时考虑不足,导致无法进行扩展,当新需求出现的时候,只能再添加新的接口。
这些函数有什么相同之处呢,它们都是用来创建新的文件描述符的 (file descriptor)。这个需求就是跟文件描述符相关。
在通过 fork/exec 创建一个新进程的时候,默认的行为是子进程继承父进程的所有已打开的文件。为了安全性,一般会把文件设成 close-on-exec,也就是说执行 exec 的时候,自动把文件关闭。否则的话,父进程的文件就会泄漏给子进程。
比如浏览器里,假设一个 tab 是银行付款页面,另一个 tab 是“某个”页面,这个页面需要用一个 plugin,而浏览器通过 fork/exec 来运行这个 plugin。如果文件描述符没有设置为 close-on-exec,那么银行页面的文件就有可能泄漏给这个 plugin。然后…
所以一般都会通过 fcntl 来设置一个 flag, FD_CLOEXEC,保证在 fork/exec 的时候自动关闭文件。但是由于 open 和 fcntl 之间总是存在一定的时间差,所以在多线程的程序里会有潜在的危险。一个办法是用锁,保证 open 和 fcntl 之间不会进行 fork,但是这并不可行 (且看引用原文的描述)。
最好的办法就是给 open 添加一个 flag,使得文件在打开时就已经设为 close-on-exec。
但是… 能打开文件 (也就是创建文件描述符) 的函数不只有 open。由于 UNIX 众所周知的设计特点,socket/accept/pipe/dup/epoll 都能创建文件描述符。而最大的问题是,不是所有的函数都像 open 那样有一个 flags 参数。比如 int pipe(int pipefd[2])。管道固然是个绝佳的概念,但是谁也没想到,后人竟然想对管道设置 flag。
但是为了保证原子性得设置 close-on-exec,必须要有 flag 参数,所以对这些函数只能创建新的接口,dup3, pipe2 等等就是这样来的。这个解决方案不算完美,但也没有更好的办法了 (比如 linus 提出过一个“间接系统调用”的方案)。
而另一些函数,比如 int socket(int domain, int type, int protocol),虽然没有
flag,但有一个 type 参数。由于 type 的取值范围很有限,而且 type 和 flag 还算有点相似,所以开发者决定复用这个参数,把 SOCK_CLOEXEC 看作新的 type。这样函数接口就不需要修改了。
这样,所有能创建文件描述符的接口就都(相当于)有 flag 参数了,不仅可以用于设置
close-on-exec,而且也能方便将来的其它扩展。比如 XXX_NONBLOCK (现在已有),和提了好久的 non-sequential file descriptor 的支持。
引用: udrepper.livejournal.com