1.2.1 open介绍
open在手册中有两个函数原型,如下所示:
*int open(const char pathname, int flags);
*int open(const char pathname, int flags, mode_t mode);
这样的函数原型有些违背了我们的直觉。C语言是不支持函数重载的,为什么open的系统调用可以有两个这样的open原型呢?内核绝对不可能为这个功能创建两个系统调用。在Linux内核中,实际上只提供了一个系统调用,对应的是上述两个函数原型中的第二个。那么open有两个函数原型又是怎么回事呢?当我们调用open函数时,实际上调用的是glibc封装的函数,然后由glibc通过自陷指令,进行真正的系统调用。也就是说,所有的系统调用都要先经过glibc才会进入操作系统。这样的话,实际上是glibc提供了一个变参函数open来满足两个函数原型,然后通过glibc的变参函数open实现真正的系统调用来调用原型二。
可以通过一个小程序来验证我们的猜想,代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd = open("test.txt", O_CREAT, 0644, "test");
close(fd);
return 0;
}
在这个程序中,调用open的时候,传入了4个参数,如果open不是变参函数,就会报错,如“too many arguments to function‘open’”。但是请看下面的编译输出:
gcc -g -Wall 2_2_1_test_open.c
没有任何的警告和错误。这就证实了我们的猜想,open是glibc的一个变参函数。fcntl.h中open函数的声明也确定了这点:
*extern int open (__const char __file, int __oflag, ...) __nonnull ((1));
下面来说明一下open的参数:
int open(const char *pathname, int flags, mode_t mode);
·pathname:表示要打开的文件路径。
·flags:用于指示打开文件的选项,常用的有O_RDONLY、O_WRONLY和O_RDWR。
这三个选项必须有且只能有一个被指定。为什么O_RDWR!=O_RDONLY|O_WRONLY呢?
Linux环境中,O_RDONLY被定义为0,O_WRONLY被定义为1,而O_RDWR却被定义为2。
之所以有这样违反常规的设计遗留至今,就是为了兼容以前的程序。除了以上三个选项,Linux平台还支持更多的选项,APUE中对此也进行了介绍。
·mode:只在创建文件时需要,用于指定所创建文件的权限位(还要受到umask环境变量的影响)。
1.2.2 更多选项
除了常用的几个打开文件的选项,APUE还介绍了一些常用的POSIX定义的选项。下面列出了Linux平台支持的大部分选项:
·O_APPEND:每次进行写操作时,内核都会先定位到文件尾,再执行写操作。
·O_ASYNC:使用异步I/O模式。
·O_CLOEXEC:在打开文件的时候,就为文件描述符设置FD_CLOEXEC标志。这是一个新的选项,用于解决在多线程下fork与用fcntl设置FD_CLOEXEC的竞争问题。某些应用使用fork来执行第三方的业务,为了避免泄露已打开文件的内容,那些文件会设置FD_CLOEXEC标志。但是fork与fcntl是两次调用,在多线程下,可能会在fcntl调用前,就已经fork出子进程了,从而导致该文件句柄暴露给子进程。关于O_CLOEXEC的用途,将会在第4章详细解。
·O_CREAT:当文件不存在时,就创建文件。
·O_DIRECT:对该文件进行直接I/O,不使用VFS Cache。
·O_DIRECTORY:要求打开的路径必须是目录。
·O_EXCL:该标志用于确保是此次调用创建的文件,需要与O_CREAT同时使用;当文件已经存在时,open函数会返回失败。
·O_LARGEFILE:表明文件为大文件。
·O_NOATIME:读取文件时,不更新文件最后的访问时间。
·O_NONBLOCK、O_NDELAY:将该文件描述符设置为非阻塞的(默认都是阻塞的)。
·O_SYNC:设置为I/O同步模式,每次进行写操作时都会将数据同步到磁盘,然后write才能返回。
·O_TRUNC:在打开文件的时候,将文件长度截断为0,需要与O_RDWR或O_WRONLY同时使用。在写文件时,如果是作为新文件重新写入,一定要使用O_TRUNC标志,否则可能会造成旧内容依然存在于文件中的错误,如生成配置文件、pid文件等——在第2章中,我会例举一个未使用截断标志而导致问题的示例代码。
1.2.3 open源码跟踪
我们经常这样描述**“打开一个文件”**,那么这个所谓的“打开”,究竟“打开”了什么?内核在这个过程中,又做了哪些事情呢?这一切将通过分析内核源码来得到答案。
*int open(const char pathname, int flags, mode_t mode);
跟踪内核open源码open->do_sys_open,代码如下:
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
struct
open_flags op;
/*
flags为用户层传递的参数,内核会对flags进行合法性检查,并根据mode生成新的flags值赋给****lookup */
int lookup = build_open_flags(flags, mode, &op);
/*
将用户空间的文件名参数复制到内核空间*/
*char tmp = getname(filename);
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
/*
未出错则申请新的文件描述符*/
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
/*
申请新的文件管理结构****file */
*struct file f = do_filp_open(dfd, tmp, &op, lookup);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
/*
产生文件打开的通知事件*/
fsnotify_open(f);
/*
将文件描述符fd与文件管理结构file对应起来,即安装*/
fd_install(fd, f);
}
}
putname(tmp);
}
return fd;
}
从do_sys_open可以看出,打开文件时,内核主要消耗了两种资源:文件描述符与内核管理文件结构file。
1.2.4 如何选择文件描述符
根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件描述符。Linux是如何实现这一标准的呢?
在Linux中,通过do_sys_open->get_unused_fd_flags->**alloc_fd(0,(flags))**来选择文件描述符,代码如下:
int alloc_fd(unsigned start, unsigned flags)
{
struct files_struct *files = current->files;
unsigned int fd;
int error;
struct fdtable *fdt;
/*
files为进程的文件表,下面需要更改文件表,所以需要先锁文件表*/
spin_lock(&files->file_lock);
repeat:
/* 得到文件描述符表*/
fdt = files_fdtable(files);
/* 从start开始,查找未用的文件描述符。在打开文件时,start为0 */
fd = start;
/* files->next_fd为上一次成功找到的
fd的下一个描述符。使用next_fd,可以快速找到未用的文件描述符;*/
if (fd < files->next_fd)
fd = files->next_fd;
/*当小于当前文件表支持的最大文件描述符个数时,利用位图找到未用的文件描述符。
如果大于max_fds怎么办呢?
如果大于当前支持的最大文件描述符,那它肯定是未用的,就不需要用位图来确认了。*/
if (fd < fdt->max_fds)
fd = find_next_zero_bit(fdt->open_fds->fds_bits,
fdt->max_fds, fd);
/* expand_files用于在必要时扩展文件表。何时是必要的时候呢?比如当前文件描述符已经超过了当前文件表支持的最大值的时候。*/
error = expand_files(files, fd);
if (error < 0)
goto out;
/*
- If we needed to expand the fs array we
- might have blocked - try again.
*/
if (error)
goto repeat;
/* 只有在start小于next_fd时,才需要更新next_fd,以尽量保证文件描述符的连续性。*/
if (start <= files->next_fd)
files->next_fd = fd + 1;
/* 将打开文件位图open_fds对应fd的位置置位*/
FD_SET(fd, fdt->open_fds);
/* 根据flags是否设置了O_CLOEXEC,设置或清除fdt->close_on_exec */
if (flags & O_CLOEXEC)
FD_SET(fd, fdt->close_on_exec);
else
FD_CLR(fd, fdt->close_on_exec);
error = fd;
#if 1
/* Sanity check */
if (rcu_dereference_raw(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
spin_unlock(&files->file_lock);
return error;
}
1.2.5 文件描述符fd与文件管理结构file
前文已经说过,内核使用fd_install将文件管理结构file与fd组合起来,具体操作请看如下代码:
void fd_install(unsigned int fd, struct file *file)
{
struct files_struct *files = current->files;
struct fdtable *fdt;
spin_lock(&files->file_lock);
/*
得到文件描述符表*/
fdt = files_fdtable(files);
BUG_ON(fdt->fd[fd] != NULL);
/*将文件描述符表中的
file类型的指针数组中对应fd的项指向file。这样文件描述符fd与file就建立了对应关系*/
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
}
当用户使用fd与内核交互时,内核可以用fd从fdt->fd[fd]中得到内部管理文件的结构struct file。