一、文件描述符标志
每个进程为所有打开的文件维护对应的fd flag。当前只定义了一个文件描述符标志:FD_CLOEXEC:若设置某个文件的fd flag(即为1),则进程fork出子进程时,执行exec前要关闭这个文件描述符。
- close_on_exec标志是一个进程所有文件描述符的位图标志,每个比特位代表一个打开的文件描述符,用于确定该打开文件的FD_CLOEXEC状态(1,设置;0,清除)。
- FD_CLOEXEC默认为0,因此默认情况下子进程继承的文件描述符都处于打开状态。(“越权”安全问题)
- 设置某个文件的close on exec标志:fcntl(fd, F_SETFD, flags | FD_CLOEXEC)来进行设置(flags可先通过F_GETFD获得)。
FD_CLOSEEXEC带来的安全问题
父进程打开了某些文件并fork子进程,子进程就会默认有这些文件的读取权限,但很多时候并不想让子进程有这么多的权限。——解决办法:设置FD_CLOSEEXEC标志,exec新程序之前关闭继承的打开文件。
- 若是socket,因为socket的fd本身包含本机IP,端口号等信息,如果该socketfd被子进程继承并占用,或者未关闭,就会导致新的父进程重新启动时不能正常使用这些网络端口,严重的就是设备掉线。
- 例子:在Webserver中,首先会使用root权限启动,以此打开root权限才能打开的端口、日志等文件。然后降权到普通用户,fork出一些worker进程,这些进程中再进行解析脚本、写日志、输出结果等进一步操作。这里就隐含一个安全问题:子进程中继承了父进程的fd,那么子进程中运行的脚本只需要继续操作这些fd,就能够使用普通权限“越权”操作root用户才能操作的文件。
二、文件状态标志
指明文件的状态属性,它们由open的flags参数指明。与“文件描述符标志对应进程”不同,文件状态标志由与同一次打开文件相连的所有重复文件标志符所共享(涉及dup时,只要共享文件表项即共享fd status flag)。
文件状态标志分类
1、访问方式标志:指明允许文件标志符用于读、写或两者兼之,包括O_RDONLY、O_WRONLY和O_RDWR。
- 这些访问方式在文件被打开时选定,之后便不能再改变。
- 可以通过fcntl函数获取文件访问方式。——因为读和写访问方式不是各占一位,因此获取文件访问方式的可移植方法是用屏蔽字O_ACCMODE先与文件状态标志值作按位与操作(’&’)得到访问方式位,再将结果与这3个值逐一比较(APUE set_fl函数)。
2、打开时标志:指明打开文件时影响open行为的一些选项。
- 这些选项除了O_NONBLOCK其他的一旦文件打开就不再保留,因为O_NONBLOCK同时也是一个I/O操作方式,故此标志被保留。
- O_CREAT:若设置,当该文件不存在时创建并打开此文件。
- O_EXCL:若O_CREAT和O_EXCL同时设置,当指定的文件已经存在时open失败。保证不破坏已存在的文件。
- O_TRUNC:截断文件为零长度,这一选项只对普通文件有用,对诸如目录或FIFO之类的特殊文件无用。
- O_NONBLOCK:防止为打开文件而阻塞很长时间。这通常仅对设备、网络、管道文件才有意义。此标志同时也作为I/O操作方式标志,这意味着在open中指明O_NONBLOCK就同时设置了非阻塞I/O方式。因此要非阻塞地打开一个文件且不影响正常的阻塞I/O,必须先设置O_NONBLOCK调用open,然后调用fcntl关闭此位。
3、I/O操作方式标志:使用fd读/写的工作方式。这些标志由open()设置,之后可以用fcntl()获取和改变。
- O_APPEND:用于追加写。若此位设置,所有write()操作写数据至文件尾而不管文件位置在何处。这是附加数据至文件尾唯一可靠的方法。用附加方式可以保证无论是否有其他进程正在写同一个文件,write()操作总是将数据写在当前文件尾。相反,在未设置此位的情况下,如果通过简单地移动文件位置到文件尾,然后再写数据,则在设置文件位置之后开始写之前,可能有其他进程扩展此文件(对应于两个不同的进程打开同一个文件的情形,它们共享同一个vnode,但各自有自己的系统打开文件表,因而有自己的文件位置),从而导致所写的数据出现在实际文件尾之前的某个地方。
- O_NONBLOCK:用于非阻塞I/O。
- O_ASYNC:用于信号驱动的I/O(异步I/O)。若此位设置,当文件标志符中有输入数据时会生成SIGIO信号。
- O_SYNC:用于同步I/O。若此位设置,文件按同步I/O方式打开,并将导致任何写该文件的操作都阻塞调用进程直至内核I/O缓冲区的数据以及与此次写有关的文件属性已全部写至物理存储介质。
- O_DSYNC:用于同步数据I/O。若此位设置,文件按同步I/O方式打开,并将导致任何写该文件的操作都阻塞调用进程直至内核I/O缓冲区的数据已全部写至物理存储介质。但如果所写的数据不影响读刚写入的数据,则不等待文件属性更新。
- O_RSYNC:若此位设置,文件按同步I/O方式打开,并将导致任何读该文件的操作都将等待所有写入同一区域的写操作按O_DSYNC和O_SYNC完成后再进行。如果同时设置了O_SYNC 和 O_RSYNC标志,则读操作将阻塞直到文件的访问时间属性已写至物理存储介质。如果同时设置了O_DSYNC 和 O_RSYNC标志,则读操作将阻塞直到所有与保持文件完整性有关的数据都已写至物理存储介质。
简单地说,O_SYNC、O_DSYNC和O_RSYNC这几个标志的主要作用是使数据直接写到磁盘或直接从磁盘读入。
三、打开文件的内核数据结构图
由以上三幅图知:
- 一个进程对每个打开文件有各自的文件描述符标志和文件表项(文件状态标志、当前偏移量)。fork子进程时,默认继承打开文件描述符及文件描述符标志,同时文件表项拷贝而不是共享,保证每个进程获得各自的文件表项。
- dup时该进程内共享文件表项,即共享文件状态标志和当前偏移量。但dup会清除FD_CLOEXEC标志,因此fork出子进程仍然打开该文件,并拷贝文件表项。
- 继承文件描述符,同时继承文件描述符标志。共享文件表项,即共享文件状态标志和当前偏移量。
四、接口函数
open
int open(const char* path, int oflags, /* mode_t mode */); //成功,返回fd;出错,返回-1
dup/dup2:复制一个现有的文件描述符
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2); //成功,都返回new_fd;出错,都返回-1
dup:返回可用文件描述符的最小值。默认清除FD_CLOEXEC。
dup2:复制为指定文件描述符:若fd2已经打开,则先关闭该文件再复制并返回fd2,同时清除FD_CLOEXEC;若fd=fd2,不关闭该文件并保持它的文件描述符标志状态,同时返回fd2。
fcntl:改变已打开文件的属性(只需提供fd即可)
函数原型:
#include <fcntl.h>
int fcntl(int fd, int cmd, /* int arg */); //成功,依赖于cmd;出错,都返回-1
函数的功能:5类
- 复制一个已有的描述符(cmd=F_FUPFD 或 F_DUPFD_CLOEXEC)
- 获取/设置文件描述符标志(cmd=F_GETFD 或 F_SETFD)
- 获取/设置文件状态标志(cmd=F_GETFL 或 F_SETFL)
- 获取/设置异步I/O所有权(cmd=F_GETOWN 或 F_SETOWN)
- 获取/设置记录锁(cmd=F_GETLK、F_SETLK 或 F_SETLKW)