1.2 打开文件

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值