Linux环境编程与内核之文件与IO

韩乔落

内核文件表

文件

Linux内核将一切视为文件,既可以是事实上的真正的物理文件,也可以是设备、管道,甚至还可以是一块内存。狭义的文件是指文件系统中的物理文件,而广义的文件则可以是Linux管理的所有对象。这些广义的文件利用VFS机制,以文件系统的形式挂载在Linux内核中,对外提供一致的文件操作接口。

文件描述符

文件描述符是一个非负整数,其本质就是一个句柄,所以也可以认为文件描述符就是一个文件句柄。一切对于用户透明的返回值,即可视为句柄。用户空间利用文件描述符与内核进行交互;而内核拿到文件描述符后,可以通过它得到用于管理文件的真正的数据结构。文件描述符采用最小返回,0, 1, 2分别为 stdin, stdout, stderr,如果我们 close(0),那麼再打开文件描述符将返回0

文件表

Linux的每个进程都会维护一个文件表,以便维护该进程打开文件的信息,包括打开的文件个数、每个打开文件的偏移量等信息。

文件表的实现

内核中进程对应的结构是task_struct,进程的文件表保存在task_struct->files中。其结构代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};

struct files_struct {
/* count为文件表files_struct的引用计数*/
atomic_t count;
/* 文件描述符表*/
/*
* 为什么有两个fdtable呢?这是内核的一种优化策略。fdt为指针,而fdtab为普通变量。
* 一般情况下,fdt是指向fdtab的,当需要它的时候,才会真正动态申请内存。
* 因为默认大小的文件表足以应付大多数情况,因此这样就可以避免频繁的内存申请。
* 这也是内核的常用技巧之一。在创建时,使用普通的变量或者数组,然后让指针指向它,作为默认情况使用。
* 只有当进程使用量超过默认值时,才会动态申请内存。
*/
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
/* 使用 ____cacheline_aligned_in_smp 可以保证file_lock是以cacheline对齐的,避免了false sharing */
spinlock_t file_lock ____cacheline_aligned_in_smp;
/* 用于查找下一个空闲的 fd */
int next_fd;
/* 保存执行exec需要关闭的文件描述符的位图*/
struct embedded_fd_set close_on_exec_init;
/* 保存打开的文件描述符的位图 */
struct embedded_fd_set open_fds_init;
/* fd_array为一个固定大小的file结构数组。
* struct file是内核用于文件管理的结构。这里使用默认大小的数组,就是为了可以涵盖大多数情况,避免动态分配
*/
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

下面看看files_struct是如何使用默认的fdtabfd_array的,initLinux的第一个进程,它的文件表是一个全局变量,代码如下:

1
2
3
4
5
6
7
8
9
10
11
struct files_struct init_files = {
.count = ATOMIC_INIT(1),
.fdt = &init_files.fdtab,
.fdtab = {
.max_fds = NR_OPEN_DEFAULT,
.fd = &init_files.fd_array[0],
.close_on_exec = (fd_set *)&init_files.close_on_exec_init,
.open_fds = (fd_set *)&init_files.open_fds_init,
},
.file_lock = __SPIN_LOCK_UNLOCKED(init_task.file_lock),
};

init_files.fdtinit_files.fdtab.fd都分别指向了自己已有的成员变量,并以此作为一个默认值。后面的进程都是从init进程fork出来的。fork的时候会调用dup_fd,而在dup_fd中其代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
newf = kmem_cache_alloc(files_cachep, GFP_KERNEL);
if (!newf)
goto out;
atomic_set(&newf->count, 1);
spin_lock_init(&newf->file_lock);
newf->next_fd = 0;
new_fdt = &newf->fdtab;
new_fdt->max_fds = NR_OPEN_DEFAULT;
new_fdt->close_on_exec = (fd_set *)&newf->close_on_exec_init;
new_fdt->open_fds = (fd_set *)&newf->open_fds_init;
new_fdt->fd = &newf->fd_array[0];
new_fdt->next = NULL;

初始化new_fdt,同样是为了让new_fdt和new_fdt->fd指向其本身的成员变量fdtab和fd_array。


/proc/pid/status为对应pid的进程的当前运行状态,其中FDSize值即为当前进程max_fds的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
# cat /proc/3576/status 
Name: su
Umask: 0022
State: S (sleeping)
Tgid: 3576
Ngid: 0
Pid: 3576
PPid: 3447
TracerPid: 0
Uid: 1000 0 0 0
Gid: 1000 1000 1000 1000
FDSize: 64
[...]

因此,初始状态下,files_structfdtablefiles的关系如下图所示。

内核文件表

除了文件名以外的所有文件信息,都存在inode之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct inode {
struct hlist_node i_hash; /* 哈希表 */
struct list_head i_list; /* 索引节点链表 */
struct list_head i_dentry; /* 目录项链表 */
unsigned long i_ino; /* 节点号 */
atomic_t i_count; /* 引用记数 */
umode_t i_mode; /* 访问权限控制 */
unsigned int i_nlink; /* 硬链接数 */
uid_t i_uid; /* 使用者id */
gid_t i_gid; /* 使用者id组 */
kdev_t i_rdev; /* 实设备标识符 */
loff_t i_size; /* 以字节为单位的文件大小 */
struct timespec i_atime; /* 最后访问时间 */
struct timespec i_mtime; /* 最后修改(modify)时间 */
struct timespec i_ctime; /* 最后改变(change)时间 */
unsigned int i_blkbits; /* 以位为单位的块大小 */
unsigned long i_blksize; /* 以字节为单位的块大小 */
unsigned long i_version; /* 版本号 */
unsigned long i_blocks; /* 文件的块数 */
unsigned short i_bytes; /* 使用的字节数 */
spinlock_t i_lock; /* 自旋锁 */
struct rw_semaphore i_alloc_sem; /* 索引节点信号量 */
struct inode_operations *i_op; /* 索引节点操作表 */
struct file_operations *i_fop; /* 默认的索引节点操作 */
struct super_block *i_sb; /* 相关的超级块 */
struct file_lock *i_flock; /* 文件锁链表 */
struct address_space *i_mapping; /* 相关的地址映射 */
struct address_space i_data; /* 设备地址映射 */
struct dquot *i_dquot[MAXQUOTAS]; /* 节点的磁盘限额 */
struct list_head i_devices; /* 块设备链表 */
struct pipe_inode_info *i_pipe; /* 管道信息 */
struct block_device *i_bdev; /* 块设备驱动 */
unsigned long i_dnotify_mask; /* 目录通知掩码 */
struct dnotify_struct *i_dnotify; /* 目录通知 */
unsigned long i_state; /* 状态标志 */
unsigned long dirtied_when; /* 首次修改时间 */
unsigned int i_flags; /* 文件系统标志 */
unsigned char i_sock; /* 可能是个套接字吧 */
atomic_t i_writecount; /* 写者记数 */
void *i_security; /* 安全模块 */
__u32 i_generation; /* 索引节点版本号 */
union {
void *generic_ip; /* 文件特殊信息 */
} u;
};

打开文件

open 函数简介

open在手册中有两个函数原型,如下所示:

1
2
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

在Linux内核中,实际上只提供了一个系统调用,对应的是上述两个函数原型中的第二个。当我们调用open函数时,实际上调用的是glibc封装的函数,然后由glibc通过自陷指令,进行真正的系统调用。也就是说,所有的系统调用都要先经过glibc才会进入操作系统。这样的话,实际上是glibc提供了一个变参函数open来满足两个函数原型,然后通过glibc的变参函数open实现真正的系统调用来调用原型二。

函数参数pathname:表示要打开的文件路径。flags:用于指示打开文件的选项,常用的有O_RDONLY、O_WRONLY和O_RDWR。这三个选项必须有且只能有一个被指定。O_RDWR!=O_RDONLY|O_WRONLY,Linux环境中,O_RDONLY被定义为0,O_WRONLY被定义为1,而O_RDWR却被定义为2。之所以有这样违反常规的设计遗留至今,就是为了兼容以前的程序。当然还有很多其他选项这里不一一列举。

open 内核源码追踪

下面是open的内核源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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。

特别的其在linux kernel 6.0以上版本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_how how = build_open_how(flags, mode);
return do_sys_openat2(dfd, filename, &how);
}
inline struct open_how build_open_how(int flags, umode_t mode)
{
struct open_how how = {
.flags = flags & VALID_OPEN_FLAGS,
.mode = mode & S_IALLUGO,
};

/* O_PATH beats everything else. */
if (how.flags & O_PATH)
how.flags &= O_PATH_FLAGS;
/* Modes should only be set for create-like flags. */
if (!WILL_CREATE(how.flags))
how.mode = 0;
return how;
}
static long do_sys_openat2(int dfd, const char __user *filename,
struct open_how *how)
{
struct open_flags op;
int fd = build_open_flags(how, &op);
struct filename *tmp;

if (fd)
return fd;

tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);

fd = get_unused_fd_flags(how->flags);
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}

多了一道检查,借此说明,我们分析的是内核的核心逻辑,后面有机会会做新内核的安全性分析,写此系列文章也只是做一下学习笔记


根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件描述符。在Linux中,通过do_sys_open->get_unused_fd_flags->alloc_fd(0,(flags))来选择文件描述符,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;
}

前文已经说过,内核使用fd_install将文件管理结构file与fd组合起来,具体操作请看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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。

open() 用法解析

open 函数是 Linux 和其他类 Unix 操作系统中用于打开文件的系统调用。它在用户空间的应用程序与内核之间提供了一个接口,用于文件的创建、打开和访问。以下是 open 函数的详细解释:

函数原型

在 C 语言中,open 函数的原型通常定义在 <fcntl.h> 头文件中:

1
2
3
4
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

参数说明

  1. **pathname**:

    • 这是指向文件路径的指针,表示要打开的文件的路径。可以是绝对路径或相对路径。
  2. **flags**:

    • 这是一个整数,用于指定打开文件时的选项。可以是以下标志的组合:
      • **O_RDONLY**:以只读方式打开文件。
      • **O_WRONLY**:以只写方式打开文件。
      • **O_RDWR**:以读写方式打开文件。
      • **O_CREAT**:如果文件不存在则创建文件。需要指定 mode 参数。
      • **O_EXCL**:与 O_CREAT 一起使用,如果文件存在则返回错误。
      • **O_TRUNC**:如果文件存在并且以写入方式打开,则将其截断为零长度。
      • **O_APPEND**:以追加模式打开文件。写入操作将添加到文件的末尾。
      • **O_NONBLOCK**:以非阻塞模式打开文件。
      • **O_SYNC**:以同步模式打开文件,保证写入操作立即写入底层存储设备。
  3. **mode**:

    • 这是一个文件权限的掩码,用于指定新创建文件的权限(如 O_CREAT 标志被指定时)。常用的权限包括:
      • **S_IRUSR**:用户读权限。
      • **S_IWUSR**:用户写权限。
      • **S_IXUSR**:用户执行权限。
      • **S_IRGRP**:组读权限。
      • **S_IWGRP**:组写权限。
      • **S_IXGRP**:组执行权限。
      • **S_IROTH**:其他用户读权限。
      • **S_IWOTH**:其他用户写权限。
      • **S_IXOTH**:其他用户执行权限。

返回值

  • 成功时,open 返回一个非负整数,称为文件描述符(file descriptor),用于后续对文件的操作。
  • 失败时,返回 -1,并设置 errno 来指示错误类型。

错误处理

常见的错误包括:

  • **EACCES**:权限不足,无法打开文件。
  • **EEXIST**:文件已存在(与 O_CREAT | O_EXCL 一起使用时)。
  • **ENOENT**:文件不存在,且未指定 O_CREAT
  • **ENOTDIR**:路径中的某个组件不是目录。
  • **EISDIR**:试图以写模式打开一个目录。
  • **EMFILE**:进程已打开的文件描述符达到上限。

示例

以下是一个使用 open 函数的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 使用文件描述符进行读写操作
// ...

close(fd); // 关闭文件
return 0;
}

在这个示例中,example.txt 被以只写模式打开。如果文件不存在,则创建它,并将其权限设置为用户可读写。若文件已存在,则截断为零长度。

创建文件

creat 函数简介

creat函数用于创建一个新文件,其等价于open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode)。由于历史原因,早期的Unix版本中,open的第二个参数只能是0、1或者2。这样就没有办法打开一个不存在的文件。因此,一个独立系统调用creat被引入,用于创建新文件。现在的open函数,通过使用O_CREAT和O_TRUNC选项,可以实现creat的功能,因此creat已经不是必要的了。内核creat的实现代码如下所示:

1
2
3
4
SYSCALL_DEFINE2(creat, const char __user *, pathname, int, mode)
{
return sys_open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);
}

关闭文件

close 内核源码追踪

close用于关闭文件描述符。而文件描述符可以是普通文件,也可以是设备,还可以是socket。在关闭时,VFS会根据不同的文件类型,执行不同的操作。下面将通过跟踪close的内核源码来了解内核如何针对不同的文件类型执行不同的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
SYSCALL_DEFINE1(close, unsigned int, fd)
{
struct file * filp;
/* 得到当前进程的文件表 */
struct files_struct *files = current->files;
struct fdtable *fdt;
int retval;
spin_lock(&files->file_lock);
/* 通过文件表,取得文件描述符表 */
fdt = files_fdtable(files);
/* 参数fd大于文件描述符表记录的最大描述符,那么它一定是非法的描述符 */
if (fd >= fdt->max_fds)
goto out_unlock;
/* 利用fd作为索引,得到file结构指针 */
filp = fdt->fd[fd];
/* 检查filp是否为NULL。正常情况下,filp一定不为NULL。*/
if (!filp)
goto out_unlock;
/* 将对应的filp置为0 */
rcu_assign_pointer(fdt->fd[fd], NULL);
/* 清除fd在close_on_exec位图中的位*/
FD_CLR(fd, fdt->close_on_exec);
/* 释放该fd,或者说将其置为unused。*/
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
/* 关闭file结构 */
retval = filp_close(filp, files);
/* can't restart close syscall because file table entry was cleared */
if (unlikely(retval == -ERESTARTSYS ||
retval == -ERESTARTNOINTR ||
retval == -ERESTARTNOHAND ||
retval == -ERESTART_RESTARTBLOCK))
retval = -EINTR;
return retval;
out_unlock:
spin_unlock(&files->file_lock);
return -EBADF;
}
EXPORT_SYMBOL(sys_close);

__put_unused_fd源码如下所示:

1
2
3
4
5
6
7
8
9
10
static void __put_unused_fd(struct files_struct *files, unsigned int fd)
{
/* 取得文件描述符表*/
struct fdtable *fdt = files_fdtable(files);
/* 清除fd在open_fds位图的位*/
__FD_CLR(fd, fdt->open_fds);
/* 如果fd小于next_fd,重置next_fd为释放的fd */
if (fd < files->next_fd)
files->next_fd = fd;
}

结合之前的alloc_fd()可知,Linux文件描述符策略永远选择最小的可用的文件描述符。从__put_unused_fd退出后,close会接着调用filp_close,其调用路径为filp_close->fput。在fput中,会对当前文件struct file的引用计数减一并检查其值是否为0。当引用计数为0时,表示该struct file没有被其他人使用,则可以调用__fput执行真正的文件释放操作,然后调用要关闭文件所属文件系统的release函数,从而实现针对不同的文件类型来执行不同的关闭操作。


这里选择socket文件系统作为示例,来说明Linux如何挂载文件系统指定的文件操作函数files_operations。socket.c中定义了其文件操作函数file_operations,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.aio_read = sock_aio_read,
.aio_write = sock_aio_write,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.open = sock_no_open, /* special open code to disallow open via /proc */
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};

函数sock_alloc_file用于申请socket文件描述符及文件管理结构file结构。它调用alloc_file来申请管理结构file,并将socket_file_ops作为参数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file = alloc_file(&path, FMODE_READ | FMODE_WRITE, &socket_file_ops);

struct file *alloc_file(struct path *path, fmode_t mode,
const struct file_operations *fop)
{
struct file *file;
/* 申请一个file */
file = get_empty_filp();
if (!file)
return NULL;
file->f_path = *path;
file->f_mapping = path->dentry->d_inode->i_mapping;
file->f_mode = mode;
/* 将自定义的文件操作函数赋给file->f_op */
file->f_op = fop;
[...]
}

在初始化file结构的时候,socket文件系统将其自定义的文件操作赋给了file->f_op,从而实现了在VFS中可以调用socket文件系统自定义的操作。

close() 用法解析

close 函数是用于关闭文件描述符的系统调用。在 Linux 和其他类 Unix 操作系统中,文件描述符是用于访问文件、管道、套接字等的抽象句柄。close 函数可以释放文件描述符,使其可供其他文件或资源使用。

函数原型

在 C 语言中,close 函数的原型定义在 <unistd.h> 头文件中:

1
2
3
#include <unistd.h>

int close(int fd);

参数说明

  • **fd**:
    • 这是一个整数,表示要关闭的文件描述符。该描述符应该是先前通过 opensocketdup 等系统调用获得的。

返回值

  • 成功时,close 返回 0
  • 失败时,返回 -1,并设置 errno 来指示错误类型。

错误处理

常见的错误包括:

  • **EBADF**:提供的文件描述符 fd 无效,可能是因为它从未被打开或已经被关闭。
  • **EINTR**:close 调用被信号中断。此时通常需要重新尝试调用 close
  • **EIO**:发生了输入/输出错误。

使用注意事项

  1. 资源释放close 函数的主要作用是释放与文件描述符关联的资源。未关闭的文件描述符可能导致资源泄漏,最终导致系统无法打开更多的文件。

  2. 多次关闭:不要多次关闭同一个文件描述符。多次关闭可能会导致未定义行为,特别是在该描述符已经被其他资源重新使用的情况下。

  3. 文件缓冲区:在关闭文件描述符之前,确保所有数据都已写入文件。如果使用标准 I/O 函数(如 fwrite),请在关闭文件描述符之前调用 fflush

示例代码

以下是一个简单的示例,说明如何使用 close 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
// 打开文件
int fd = open("example.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 写入数据
const char *content = "Hello, World!";
if (write(fd, content, 13) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}

// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}

return 0;
}

在这个代码示例中,文件 example.txt 被打开用于写入数据,写入完成后通过 close 函数关闭文件描述符。关闭操作确保文件资源被正确释放。

文件偏移

文件偏移是基于某个打开文件来说的,一般情况下,读写操作都会从当前的偏移位置开始读写(所以read和write都没有显式地传入偏移量),并且在读写结束后更新偏移量。

lseek函数简介

lseek 原型如下:

1
off_t lseek(int fd, off_t offset, int whence);

参数

该函数用于将fd的文件偏移量设置为以whence为起点,偏移为offset的位置。其中whence可以为三个值:SEEK_SET、SEEK_CUR和SEEK_END,分别表示为“文件的起始位置”、“文件的当前位置”和“文件的末尾”,而offset的取值正负均可。lseek执行成功后,会返回新的文件偏移量。

返回值

当lseek执行成功时,它会返回最终以文件起始位置为起点的偏移位置。如果出错,则返回-1,同时errno被设置为对应的错误值。也就是说,一般情况下,对于普通文件来说,lseek都是返回非负的整数,但是对于某些设备文件来说,是允许返回负的偏移量。因此要想判断lseek是否真正出错,必须在调用lseek前将errno重置为0,然后再调用lseek,同时检查返回值是否为-1及errno的值。只有当两个同时成立时,才表明lseek真正出错了。因为这里的文件偏移都是内核的概念,所以lseek并不会引起任何真正的I/O操作

lseek 内核源码追踪

lseek的源码位于read_write.c中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
SYSCALL_DEFINE3(lseek, unsigned int, fd, off_t, offset, unsigned int, origin)
{
off_t retval;
struct file * file;
int fput_needed;
retval = -EBADF;
/* 根据fd得到file指针 */
file = fget_light(fd, &fput_needed);
if (!file)
goto bad;
retval = -EINVAL;
/* 对初始位置进行检查 */
if (origin <= SEEK_MAX) {
loff_t res = vfs_llseek(file, offset, origin);
/*
* 下面这段代码,先使用res来给retval赋值,然后再次判断res是否与retval相等。
* 为什么会有这样的逻辑呢?什么时候两者会不相等呢?只有在retval与res的位数不相等的情况下。
* retval的类型是off_t->__kernel_off_t->long;
* 而res的类型是loff_t->__kernel_off_t->long long;
* 在32位机上,前者是32位,而后者是64位。当res的值超过了retval的范围时,两者将会不等。
* 即实际偏移量超过了long类型的表示范围。
*/
retval = res;
if (res != (loff_t)retval)
retval = -EOVERFLOW; /* LFS: should only happen on 32 bit platforms */
}
fput_light(file, fput_needed);
bad:
return retval;
}

然后进入vfs_llseek,代码如下:

1
2
3
4
5
6
7
8
9
10
11
loff_t vfs_llseek(struct file *file, loff_t offset, int origin)
{
loff_t (*fn)(struct file *, loff_t, int);
/* 默认的lseek操作是no_llseek,当file没有对应的llseek实现时,就会调用no_llseek,并返回-ESPIPE错误 */
fn = no_llseek;
if (file->f_mode & FMODE_LSEEK) {
if (file->f_op && file->f_op->llseek)
fn = file->f_op->llseek;
}
return fn(file, offset, origin);
}

当file支持llseek操作时,就会调用具体的llseek函数。在此,选择default_llseek作为实例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
loff_t default_llseek(struct file *file, loff_t offset, int origin)
{
struct inode *inode = file->f_path.dentry->d_inode;
loff_t retval;
mutex_lock(&inode->i_mutex);
switch (origin) {
case SEEK_END:
/* 最终偏移等于文件的大小加上指定的偏移量*/
offset += i_size_read(inode);
break;
case SEEK_CUR:
/* offset为0时,并不改变当前的偏移量,而是直接返回当前偏移量*/
if (offset == 0) {
retval = file->f_pos;
goto out;
}
/* 若offset不为0,则最终偏移等于指定偏移加上当前偏移 */
offset += file->f_pos;
break;
case SEEK_DATA:
/*
* In the generic case the entire file is data, so as
* long as offset isn't at the end of the file then the
* offset is data.
*/
/* 如注释所言,对于一般文件,只要指定偏移不超过文件大小,那么指定偏移的位置就是数据位置 */
if (offset >= inode->i_size) {
retval = -ENXIO;
goto out;
}
break;
case SEEK_HOLE:
/*
* There is a virtual hole at the end of the file, so
* as long as offset isn't i_size or larger, return
* i_size.
*/
/* 只要指定偏移不超过文件大小,那么下一个空洞位置就是文件的末尾*/
if (offset >= inode->i_size) {
retval = -ENXIO;
goto out;
}
offset = inode->i_size;
break;
}
retval = -EINVAL;
/* 对于一般文件来说,最终的offset必须大于或等于0,或者该文件的模式要求只能产生无符号的偏移量。否则就会报错 */
if (offset >= 0 || unsigned_offsets(file)) {
/* 当最终偏移不等于当前位置时,则更新文件的当前位置*/
if (offset != file->f_pos) {
file->f_pos = offset;
file->f_version = 0;
}
retval = offset;
}
out:
mutex_unlock(&inode->i_mutex);
return retval;
}

lseek() 用法解析

lseek 函数是一个用于在文件中移动读写位置的系统调用。在 Linux 和其他类 Unix 操作系统中,lseek 允许你在文件中定位到特定的字节位置,从而可以从该位置开始进行读写操作。

函数原型

在 C 语言中,lseek 函数的原型定义在 <unistd.h> 头文件中:

1
2
3
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

参数说明

  • **fd**:

    • 文件描述符,表示要操作的文件。该描述符应该是通过 open 函数获得的。
  • **offset**:

    • 相对于 whence 参数指定位置的偏移量。可以是正数、负数或零。
  • **whence**:

    • 指定偏移量的基准位置。可以是以下三个值之一:
      • SEEK_SET:将文件偏移量设置为距离文件开头 offset 字节的位置。
      • SEEK_CUR:将文件偏移量设置为当前位置加上 offset 字节的位置。
      • SEEK_END:将文件偏移量设置为文件尾加上 offset 字节的位置。

返回值

  • 成功时,lseek 返回新的文件偏移量。
  • 失败时,返回 -1,并设置 errno 来指示错误类型。

错误处理

常见的错误包括:

  • **EBADF**:提供的文件描述符 fd 无效,或者文件描述符不支持定位(例如,套接字)。
  • **EINVAL**:whence 参数无效,或者 offset 导致的偏移量无效。
  • **EOVERFLOW**:计算出的文件偏移量超出了可表示的范围。

使用注意事项

  1. 文件类型lseek 仅适用于支持随机访问的文件类型。对于管道、FIFO 或套接字,lseek 会失败。

  2. 扩展文件:如果通过 lseek 将偏移量设置到文件末尾之后的位置,并随后进行写入操作,文件将被扩展,中间的空隙通常被填充为零。

  3. 同步问题lseek 只影响文件描述符的偏移量,不会同步文件内容。因此,如果在多线程环境中使用,可能需要额外的同步机制。

示例代码

以下是一个简单的示例,说明如何使用 lseek 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
// 打开文件
int fd = open("example.txt", O_RDWR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 移动偏移量到文件开头的第 10 个字节
off_t offset = lseek(fd, 10, SEEK_SET);
if (offset == -1) {
perror("lseek");
close(fd);
exit(EXIT_FAILURE);
}

// 从新的偏移量开始写入数据
const char *data = "Data";
if (write(fd, data, 4) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}

// 关闭文件
close(fd);

return 0;
}

在这个代码示例中,lseek 被用来将文件偏移量移动到文件开头的第 10 个字节,然后从该位置开始写入数据。

读取文件

read 函数简介

read函数原型如下:

1
ssize_t read(int fd, void *buf, size_t count);

read尝试从fd中读取count个字节到buf中,并返回成功读取的字节数,同时将文件偏移向前移动相同的字节数。返回0的时候则表示已经到了“文件尾”。read还有可能读取比count小的字节数。使用read进行数据读取时,要注意正确地处理错误,也是说read返回-1时,如果errno为EAGAIN、EWOULDBLOCK或EINTR,一般情况下都不能将其视为错误。因为前两者是由于当前fd为非阻塞且没有可读数据时返回的,后者是由于read被信号中断所造成的。这两种情况基本上都可以视为正常情况。

read 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
/* 通过文件描述符fd得到管理结构file */
file = fget_light(fd, &fput_needed);
if (file) {
/* 得到文件的当前偏移量*/
loff_t pos = file_pos_read(file);
/* 利用vfs进行真正的read */
ret = vfs_read(file, buf, count, &pos);
/* 更新文件偏移量*/
file_pos_write(file, pos);
/* 归还管理结构file,如有必要,就进行引用计数操作*/
fput_light(file, fput_needed);
}
return ret;
}

再进入vfs_read,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
/* 检查文件是否为读取打开 */
if (!(file->f_mode & FMODE_READ))
return -EBADF;
/* 检查文件是否支持读取操作 */
if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
return -EINVAL;
/* 检查用户传递的参数buf的地址是否可写 */
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT;
/* 检查要读取的文件范围实际可读取的字节数 */
ret = rw_verify_area(READ, file, pos, count);
if (ret >= 0) {
/* 根据上面的结构,调整要读取的字节数 */
count = ret;
/*
* 如果定义read操作,则执行定义的read操作。
* 如果没有定义read操作,则调用do_sync_read—其利用异步aio_read来完成同步的read操作。
*/
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else
ret = do_sync_read(file, buf, count, pos);
if (ret > 0) {
/* 读取了一定的字节数,进行通知操作 */
fsnotify_access(file);
/* 增加进程读取字节的统计计数 */
add_rchar(current, ret);
}
/* 增加进程系统调用的统计计数 */
inc_syscr(current);
}
return ret;
}

上面的代码为read公共部分的源码分析,具体的读取动作是由实际的文件系统决定的。


pread 简介

Linux还提供pread从指定偏移位置读取数据。其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE4(pread64, unsigned int, fd, char __user *, buf,
size_t, count, loff_t, pos)
{
struct fd f;
ssize_t ret = -EBADF;

if (pos < 0)
return -EINVAL;

f = fdget(fd);
if (f.file) {
ret = -ESPIPE;
if (f.file->f_mode & FMODE_PREAD)
ret = vfs_read(f.file, buf, count, &pos);
fdput(f);
}

return ret;
}

pread不会从文件表中获取当前偏移,而是直接使用用户传递的偏移量,并且在读取完毕后,不会更改当前文件的偏移量。

用法解析

在 Linux 和其他类 Unix 操作系统中,readpread 函数用于从文件中读取数据。它们的主要区别在于 pread 是一个原子操作,允许从指定偏移量开始读取数据,而不改变文件描述符的偏移量。

read 函数

read 函数用于从文件描述符中读取数据,并将其存储到缓冲区中。

函数原型

1
2
3
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

参数说明

  • **fd**:

    • 文件描述符,表示要读取的文件。
  • **buf**:

    • 指向一个缓冲区的指针,用于存储读取的数据。
  • **count**:

    • 要读取的字节数。

返回值

  • 返回读取的字节数。
  • 返回 0 表示已到达文件末尾。
  • 返回 -1 表示发生错误,并设置 errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
char buffer[100];
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

ssize_t bytesRead = read(fd, buffer, sizeof(buffer) - 1);
if (bytesRead == -1) {
perror("read");
close(fd);
exit(EXIT_FAILURE);
}

buffer[bytesRead] = '\0'; // Null-terminate the buffer
printf("Read %zd bytes: %s\n", bytesRead, buffer);

close(fd);
return 0;
}

pread 函数

pread 函数类似于 read,但它从文件的指定偏移量开始读取数据,而不影响文件描述符的偏移量。

函数原型

1
2
3
#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);

参数说明

  • **fd**:

    • 文件描述符,表示要读取的文件。
  • **buf**:

    • 指向一个缓冲区的指针,用于存储读取的数据。
  • **count**:

    • 要读取的字节数。
  • **offset**:

    • 从文件开始读取数据的偏移量。

返回值

  • 返回读取的字节数。
  • 返回 0 表示已到达文件末尾。
  • 返回 -1 表示发生错误,并设置 errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
char buffer[100];
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// Read 50 bytes from offset 10
ssize_t bytesRead = pread(fd, buffer, 50, 10);
if (bytesRead == -1) {
perror("pread");
close(fd);
exit(EXIT_FAILURE);
}

buffer[bytesRead] = '\0'; // Null-terminate the buffer
printf("Read %zd bytes from offset 10: %s\n", bytesRead, buffer);

close(fd);
return 0;
}

总结

  • read 从当前文件偏移量读取数据,并更新偏移量。
  • pread 从指定偏移量读取数据,不修改文件描述符的偏移量。适用于需要在多线程环境中进行并发读操作的场景。

写入文件

write 函数简介

write 函数原型如下:

1
ssize_t write(int fd, const void *buf, size_t count);

write尝试从buf指向的地址,写入count个字节到文件描述符fd中,并返回成功写入的字节数,同时将文件偏移向前移动相同的字节数。write有可能写入比指定count少的字节数。

write 内核源码追踪

write的源码与read的很相似,位于read_write.c中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
/* 得到file管理结构指针 */
file = fget_light(fd, &fput_needed);
if (file) {
/* 得到当前的文件偏移 */
loff_t pos = file_pos_read(file);
/* 利用VFS写入 */
ret = vfs_write(file, buf, count, &pos);
/* 更新文件偏移量 */
file_pos_write(file, pos);
/* 释放文件管理指针 file */
fput_light(file, fput_needed);
}
return ret;
}

进入vfs_write,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
/* 检查文件是否为写入打开 */
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
/* 检查文件是否支持打开操作 */
if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
return -EINVAL;
/* 检查用户给定的地址范围是否可读取 */
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
/* 验证文件从pos起始是否可以写入count个字节数,并返回可以写入的字节数 */
ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
/* 更新写入字节数 */
count = ret;
/*
* 如果定义write操作,则执行定义的write操作。
* 如果没有定义write操作,则调用do_sync_write—其利用异步aio_write来完成同步的write操作。
*/
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
else
ret = do_sync_write(file, buf, count, pos);
if (ret > 0) {
/* 写入了一定的字节数,进行通知操作 */
fsnotify_modify(file);
/* 增加进程读取字节的统计计数 */
add_wchar(current, ret);
}
/* 增加进程系统调用的统计计数 */
inc_syscw(current);
}
return ret;
}

pwrite 简介

pwrite 与 pread 类似,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE4(pwrite64, unsigned int, fd, const char __user *, buf,
size_t, count, loff_t, pos)
{
struct fd f;
ssize_t ret = -EBADF;

if (pos < 0)
return -EINVAL;

f = fdget(fd);
if (f.file) {
ret = -ESPIPE;
if (f.file->f_mode & FMODE_PWRITE)
ret = vfs_write(f.file, buf, count, &pos);
fdput(f);
}

return ret;
}

用法解析

在 Linux 和其他类 Unix 操作系统中,writepwrite 函数用于向文件中写入数据。它们的主要区别在于 pwrite 是一个原子操作,允许从指定偏移量开始写入数据,而不改变文件描述符的偏移量。

write 函数

write 函数用于将数据从缓冲区写入到文件描述符所指向的文件中。

函数原型

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

参数说明

  • **fd**:

    • 文件描述符,表示要写入的文件。
  • **buf**:

    • 指向一个缓冲区的指针,包含要写入的数据。
  • **count**:

    • 要写入的字节数。

返回值

  • 返回写入的字节数。
  • 返回 -1 表示发生错误,并设置 errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
const char *data = "Hello, world!";
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

ssize_t bytesWritten = write(fd, data, 13);
if (bytesWritten == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}

printf("Wrote %zd bytes\n", bytesWritten);

close(fd);
return 0;
}

pwrite 函数

pwrite 函数类似于 write,但它从文件的指定偏移量开始写入数据,而不影响文件描述符的偏移量。

函数原型

1
2
3
#include <unistd.h>

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

参数说明

  • **fd**:

    • 文件描述符,表示要写入的文件。
  • **buf**:

    • 指向一个缓冲区的指针,包含要写入的数据。
  • **count**:

    • 要写入的字节数。
  • **offset**:

    • 从文件开始写入数据的偏移量。

返回值

  • 返回写入的字节数。
  • 返回 -1 表示发生错误,并设置 errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
const char *data = "Hello, world!";
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// Write data starting at offset 5
ssize_t bytesWritten = pwrite(fd, data, 13, 5);
if (bytesWritten == -1) {
perror("pwrite");
close(fd);
exit(EXIT_FAILURE);
}

printf("Wrote %zd bytes at offset 5\n", bytesWritten);

close(fd);
return 0;
}

总结

  • write 从当前文件偏移量写入数据,并更新偏移量。
  • pwrite 从指定偏移量写入数据,不修改文件描述符的偏移量。适用于需要在多线程环境中进行并发写操作的场景。

文件描述符的复制

Linux提供了三个复制文件描述符的系统调用,分别为:

1
2
3
int dup(int oldfd);
int dup2(int oldfd, int newfd);
int dup3(int oldfd, int newfd, int flags);
  • dup 会使用一个最小的未用文件描述符作为复制后的文件描述符。

  • dup2 是使用用户指定的文件描述符newfd来复制oldfd的。如果newfd已经是打开的文件描述符,Linux会先关闭newfd,然后再复制oldfd。

  • dup3 只有定义了feature宏“_GNU_SOURCE”才可以使用,它比dup2多了一个参数,可以指定标志,不过目前仅仅支持O_CLOEXEC标志,可在newfd上设置O_CLOEXEC标志。定义dup3的原因与open类似,可以在进行dup操作的同时原子地将fd设置为O_CLOEXEC,从而避免将文件内容暴露给子进程。

dup 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SYSCALL_DEFINE1(dup, unsigned int, fildes)
{
int ret = -EBADF;
/* 必须先得到文件管理结构file,同时也是对描述符fildes的检查 */
struct file *file = fget_raw(fildes);
if (file) {
/* 得到一个未使用的文件描述符*/
ret = get_unused_fd();
if (ret >= 0) {
/* 将文件描述符与file指针关联起来*/
fd_install(ret, file);
}
else
fput(file);
}
return ret;
}

fd_install 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
/* 让文件表中fd对应的指针等于该文件关联结构 file */
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
}

dup2 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd)
{
/* 如果oldfd与newfd相等,这是一种特殊的情况 */
if (unlikely(newfd == oldfd)) { /* corner case */
struct files_struct *files = current->files;
int retval = oldfd;
/* 检查oldfd的合法性,如果是合法的fd,则直接返回oldfd的值;如果是不合法的,则返回EBADF */
rcu_read_lock();
if (!fcheck_files(files, oldfd))
retval = -EBADF;
rcu_read_unlock();
return retval;
}
/* 如果oldfd与newfd不同,则利用sys_dup3来实现dup2 */
return sys_dup3(oldfd, newfd, 0);
}

dup3 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
SYSCALL_DEFINE3(dup3, unsigned int, oldfd, unsigned int, newfd, int, flags)
{
int err = -EBADF;
struct file * file, *tofree;
struct files_struct * files = current->files;
struct fdtable *fdt;
/* 对标志flags进行检查,支持O_CLOEXEC */
if ((flags & ~O_CLOEXEC) != 0)
return -EINVAL;
/* 与dup2不同,当oldfd与newfd相同的时候,dup3返回错误 */
if (unlikely(oldfd == newfd))
return -EINVAL;
spin_lock(&files->file_lock);
/* 根据newfd决定是否需要扩展文件表的大小 */
err = expand_files(files, newfd);
/*检查oldfd,如果是非法的,就直接返回;不过我更倾向于先检查oldfd后扩展文件表,如果是非法的,就不需要扩展文件表了*/
file = fcheck(oldfd);
if (unlikely(!file))
goto Ebadf;
if (unlikely(err < 0)) {
if (err == -EMFILE)
goto Ebadf;
goto out_unlock;
}
err = -EBUSY;
/* 得到文件表 */
fdt = files_fdtable(files);
/* 通过newfd得到对应的file结构 */
tofree = fdt->fd[newfd];
/* tofree是NULL,但是newfd已经分配的情况 */
if (!tofree && FD_ISSET(newfd, fdt->open_fds))
goto out_unlock;
/* 增加file的引用计数 */
get_file(file);
/* 将文件表newfd对应的指针指向file */
rcu_assign_pointer(fdt->fd[newfd], file);
/*
* 将newfd加到打开文件的位图中,如果newfd已经是一个合法的fd,重复设置位图则没有影响;
* 如果newfd没有打开,则必须将其加入位图中。
*/
FD_SET(newfd, fdt->open_fds);
/*
* 如果flags设置了O_CLOEXEC,则将newfd加到close_on_exec位图;
* 如果没有设置,则清除lose_on_exec位图中对应的位
*/
if (flags & O_CLOEXEC)
FD_SET(newfd, fdt->close_on_exec);
else
FD_CLR(newfd, fdt->close_on_exec);
spin_unlock(&files->file_lock);
/* 如果tofree不为空,则需要关闭newfd之前的文件 */
if (tofree)
filp_close(tofree, files);
return newfd;
Ebadf:
err = -EBADF;
out_unlock:
spin_unlock(&files->file_lock);
return err;
}

用法解析

在 Linux 和其他类 Unix 操作系统中,dup, dup2, 和 dup3 函数用于复制文件描述符。它们提供了不同的功能和灵活性,以满足各种需求。

dup 函数

dup 函数用于复制一个文件描述符,并返回一个新的文件描述符,它指向与原始文件描述符相同的文件表项。

函数原型

1
2
3
#include <unistd.h>

int dup(int oldfd);

参数说明

  • **oldfd**:
    • 需要复制的文件描述符。

返回值

  • 返回新的文件描述符。
  • 返回 -1 表示发生错误,并设置 errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

int newfd = dup(fd);
if (newfd == -1) {
perror("dup");
close(fd);
exit(EXIT_FAILURE);
}

write(newfd, "Hello, dup!\n", 12);

close(fd);
close(newfd);
return 0;
}

dup2 函数

dup2 函数将一个文件描述符复制到指定的文件描述符。如果目标文件描述符已经打开,它将首先被关闭。

函数原型

1
2
3
#include <unistd.h>

int dup2(int oldfd, int newfd);

参数说明

  • **oldfd**:

    • 需要复制的文件描述符。
  • **newfd**:

    • 指定的新文件描述符。

返回值

  • 返回 newfd
  • 返回 -1 表示发生错误,并设置 errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// Duplicate fd to standard output (1)
if (dup2(fd, 1) == -1) {
perror("dup2");
close(fd);
exit(EXIT_FAILURE);
}

printf("This will be written to the file.\n");

close(fd);
return 0;
}

dup3 函数

dup3dup2 的扩展版本,允许在复制文件描述符时指定额外的标志。

函数原型

1
2
3
#include <fcntl.h>

int dup3(int oldfd, int newfd, int flags);

参数说明

  • **oldfd**:

    • 需要复制的文件描述符。
  • **newfd**:

    • 指定的新文件描述符。
  • **flags**:

    • 额外的标志,目前支持 O_CLOEXEC,表示在执行 exec 系列函数时关闭文件描述符。

返回值

  • 返回 newfd
  • 返回 -1 表示发生错误,并设置 errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// Duplicate fd to newfd with O_CLOEXEC flag
if (dup3(fd, 1, O_CLOEXEC) == -1) {
perror("dup3");
close(fd);
exit(EXIT_FAILURE);
}

printf("This will be written to the file with O_CLOEXEC flag.\n");

close(fd);
return 0;
}

总结

  • dup 复制文件描述符到最小可用的文件描述符。
  • dup2 复制文件描述符到指定的文件描述符,若该描述符已打开,则先关闭。
  • dup3 提供类似 dup2 的功能,并可以指定额外的标志,如 O_CLOEXEC

文件数据的同步

为了提高性能,操作系统会对文件的I/O操作进行缓存处理。对于读操作,如果要读取的内容已经存在于文件缓存中,就直接读取文件缓存。对于写操作,会先将修改提交到文件缓存中,在合适的时机或者过一段时间后,操作系统才会将改动提交到磁盘上。Linux提供了三个同步接口:

1
2
3
void sync(void);
int fsync(int fd);
int fdatasync(int fd);

sync 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE0(sync)
{
/* 唤醒后台内核线程,将“脏”缓存冲刷到磁盘上 */
wakeup_flusher_threads(0, WB_REASON_SYNC);
/*
* 为什么要调用两次sync_filesystems呢?这是一种编程技巧,第一次sync_filesystems(0),参数0表示不等待,
* 可以迅速地将没有上锁的inode同步。第二次sync_filesystems(1),参数1表示等待。
* 对于上锁的inode会等待到解锁,再执行同步,这样可以提高性能。因为第一次操作中,
* 上锁的inode很可能在第一次操作结束后,就已经解锁,这样就避免了等待
*/
sync_filesystems(0);
sync_filesystems(1);
/*
* 如果是laptop模式,那么因为此处刚刚做完同步,因此可以停掉后台同步定时器
*/
if (unlikely(laptop_mode))
laptop_sync_completion();
return 0;
}

再看一下sync_filesystems->iterate_supers->sync_one_sb->__sync_filesystem,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int __sync_filesystem(struct super_block *sb, int wait)
{
/*
* This should be safe, as we require bdi backing to actually
* write out data in the first place
*/
if (sb->s_bdi == &noop_backing_dev_info)
return 0;
/* 磁盘配额同步 */
if (sb->s_qcop && sb->s_qcop->quota_sync)
sb->s_qcop->quota_sync(sb, -1, wait);
/*
* 如果wait为true,则一直等待直到所有的脏inode写入磁盘如果wait为false,则启动脏inode回写工作,但不必等待到结束
*/
if (wait)
sync_inodes_sb(sb);
else
writeback_inodes_sb(sb, WB_REASON_SYNC);
/* 如果该文件系统定义了自己的同步操作,则执行该操作 */
if (sb->s_op->sync_fs)
sb->s_op->sync_fs(sb, wait);
/* 调用block设备的flush操作,真正地将数据写到设备上 */
return __sync_blockdev(sb->s_bdev, wait);
}

从sync的代码实现上看,Linux的sync是阻塞调用。

fsync 内核源码追踪

sync只同步fd指定的文件,并且直到同步完成才返回。sync不仅同步数据,还会同步所有被修改过的文件元数据,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SYSCALL_DEFINE1(fsync, unsigned int, fd)
{
return do_fsync(fd, 0);
}

static int do_fsync(unsigned int fd, int datasync)
{
struct file *file;
int ret = -EBADF;
/* 得到file管理结构 */
file = fget(fd);
if (file) {
/* 利用vfs执行sync操作 */
ret = vfs_fsync(file, datasync);
fput(file);
}
return ret;
}

进入vfs_fsync->vfs_fsync_range,代码如下:

1
2
3
4
5
6
7
int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
{
/* 调用具体操作系统的同步操作 */
if (!file->f_op || !file->f_op->fsync)
return -EINVAL;
return file->f_op->fsync(file, start, end, datasync);
}

真正执行同步操作的fsync是由具体的文件系统的操作函数file_operations决定的。下面选择一个常用的文件系统同步函数generic_file_fsync,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int generic_file_fsync(struct file *file, loff_t start, loff_t end,
int datasync)
{
struct inode *inode = file->f_mapping->host;
int err;
int ret;
/* 同步该文件缓存中处于start到end范围内的脏页 */
err = filemap_write_and_wait_range(inode->i_mapping, start, end);
if (err)
return err;
mutex_lock(&inode->i_mutex);
/* 同步该inode对应的缓存 */
ret = sync_mapping_buffers(inode->i_mapping);
/* inode状态没有变化,无需同步,可以直接返回 */
if (!(inode->i_state & I_DIRTY))
goto out;
/* 如果是fdatasync则仅做数据同步,并且若该inode没有影响任何数据方面操作的变化(比如文件长度),则可以直接返回 */
if (datasync && !(inode->i_state & I_DIRTY_DATASYNC))
goto out;
/* 同步inode的元数据 */
err = sync_inode_metadata(inode, 1);
if (ret == 0)
ret = err;
out:
mutex_unlock(&inode->i_mutex);
return ret;
}

fdatasync的性能会优于fsync。在不需要同步所有元数据的情况下,选择fdatasync会得到更好的性能。只有在inode被设置了I_DIRTY_DATASYNC标志时,fdatasync才需要同步inode的元数据。

用法解析

在 Linux 和其他类 Unix 操作系统中,syncfsyncfdatasync 函数用于将数据从内存写入到存储设备,以确保数据的持久性。它们在数据完整性和文件系统一致性方面发挥着重要作用。以下是对这三个函数的详细解析:

sync 函数

sync 函数将所有已修改的文件系统元数据和数据块刷新到存储设备。它是一个全局操作,影响系统中的所有文件系统。

函数原型

1
2
3
#include <unistd.h>

void sync(void);

使用说明

  • sync 是一个无参数、无返回值的函数。
  • 它会异步地将所有挂载文件系统的修改写入到磁盘。
  • 由于 sync 是异步操作,调用后数据可能不会立即写入磁盘。

使用示例

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>

int main() {
// Perform some file operations...

// Synchronize all file systems
sync();

return 0;
}

fsync 函数

fsync 函数用于将指定文件描述符关联的文件的所有已修改数据和元数据写入到存储设备。这是一个针对单个文件的操作。

函数原型

1
2
3
#include <unistd.h>

int fsync(int fd);

参数说明

  • **fd**:文件描述符,表示要同步到磁盘的文件。

返回值

  • 返回 0 表示成功。
  • 返回 -1 表示发生错误,并设置 errno

使用说明

  • fsync 是同步操作,调用后会阻塞进程,直到数据被写入磁盘。
  • 通常用于需要确保特定文件数据持久化的场景,如数据库文件。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// Write some data to the file
if (write(fd, "Hello, fsync!\n", 14) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}

// Synchronize the file to disk
if (fsync(fd) == -1) {
perror("fsync");
close(fd);
exit(EXIT_FAILURE);
}

close(fd);
return 0;
}

fdatasync 函数

fdatasyncfsync 的一个变种,功能类似,但只同步文件数据和必要的元数据(如文件大小),而不包括其他元数据(如文件的最后访问时间)。

函数原型

1
2
3
#include <unistd.h>

int fdatasync(int fd);

使用说明

  • fdatasync 可能比 fsync 更高效,因为它只同步必要的数据。
  • 它适用于需要确保数据一致性但不关心其他元数据的场景。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// Write some data to the file
if (write(fd, "Hello, fdatasync!\n", 18) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}

// Synchronize the file data to disk
if (fdatasync(fd) == -1) {
perror("fdatasync");
close(fd);
exit(EXIT_FAILURE);
}

close(fd);
return 0;
}

总结

  • **sync**:将所有挂载文件系统的修改异步刷新到磁盘,影响整个系统。
  • **fsync**:同步指定文件的所有修改到磁盘,是一个同步操作,确保数据的持久性。
  • **fdatasync**:类似 fsync,但只同步文件数据和必要的元数据,通常更高效。适用于对数据一致性要求高,但对元数据一致性要求较低的场景。

文件的元数据

文件的元数据包括文件的访问权限、上次访问的时间戳、所有者、所有组、文件大小等信息。Linux环境提供了三个获取文件信息的API:

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);

这三个函数都可用于得到文件的基本信息,区别在于stat得到路径path所指定的文件基本信息,fstat得到文件描述符fd指定文件的基本信息,而lstat与stat则基本相同,只有当path是一个链接文件时,lstat得到的是链接文件自己本身的基本信息而不是其指向文件的信息。所得到的文件基本信息的结果struct stat的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
/* st_mode,其不仅仅是权限管理,同时也用于表示文件类型,比如是普通文件还是目录。 */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};

stat 内核源码追踪

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
SYSCALL_DEFINE2(stat, const char __user *, filename,
struct __old_kernel_stat __user *, statbuf)
{
struct kstat stat;
int error;
/* vfs_stat用于读取文件元数据至stat */
error = vfs_stat(filename, &stat);
if (error)
return error;
/* 这里仅是从内核的元数据结构stat复制到用户层的数据结构statbuf中 */
return cp_old_stat(&stat, statbuf);
}

进入vfs_stat->vfs_fstatat->vfs_getattr,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int vfs_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat)
{
struct inode *inode = dentry->d_inode;
int retval;
/* 对获取inode属性操作进行安全性检查 */
retval = security_inode_getattr(mnt, dentry);
if (retval)
return retval;
/* 如果该文件系统定义了这个inode的自定义操作函数,就执行它 */
if (inode->i_op->getattr)
return inode->i_op->getattr(mnt, dentry, stat);
/* 如果文件系统没有定义inode的操作函数,则执行通用的函数 */
generic_fillattr(inode, stat);
return 0;
}

void generic_fillattr(struct inode *inode, struct kstat *stat)
{
stat->dev = inode->i_sb->s_dev;
stat->ino = inode->i_ino;
stat->mode = inode->i_mode;
stat->nlink = inode->i_nlink;
stat->uid = inode->i_uid;
stat->gid = inode->i_gid;
stat->rdev = inode->i_rdev;
stat->size = i_size_read(inode);
stat->atime = inode->i_atime;
stat->mtime = inode->i_mtime;
stat->ctime = inode->i_ctime;
stat->blksize = (1 << inode->i_blkbits);
stat->blocks = inode->i_blocks;
}

所有的文件元数据均保存在inode中,而inode是Linux也是所有类Unix文件系统中的一个概念。这样的文件系统一般将存储区域分为两类,一类是保存文件对象的元信息数据,即inode表;另一类是真正保存文件数据内容的块,所有inode完全由文件系统来维护。但是Linux也可以挂载非类Unix的文件系统,这些文件系统本身没有inode的概念,Linux为了让VFS有统一的处理流程和方法,就必须要求那些没有inode概念的文件系统,根据自己系统的特点——如何维护文件元数据,生成“虚拟的”inode以供Linux内核使用。

权限位解析

文件常见的权限位有r、w和x,分别表示可读、可写和可执行。下面重点解析三个不常用的标志位。

SUID 权限位

当文件设置SUID权限位时,就意味着无论是谁执行这个文件,都会拥有该文件所有者的权限。passwd命令正是利用这个特性,来允许普通用户修改自己的密码,因为只有root用户才有修改密码文件的权限。当普通用户执行passwd命令时,就具有了root权限,从而可以修改自己的密码。以修改文件属性的权限检查代码为例,inode_change_ok用于检查该进程是否有权限修改inode节点的属性即文件属性,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
int inode_change_ok(const struct inode *inode, struct iattr *attr)
{
unsigned int ia_valid = attr->ia_valid;
[...]
/* Make sure a caller can chown. */
/* 只有在uid和suid都不符合条件的情况下,才会返回权限不足的错误 */
if ((ia_valid & ATTR_UID) &&
(current_fsuid() != inode->i_uid ||
attr->ia_uid != inode->i_uid) && !capable(CAP_CHOWN))
return -EPERM;
[...]
}

SGID 权限位

SGID与SUID权限位类似,当设置该权限位时,就意味着无论是谁执行该文件,都会拥有该文件所有者所在组的权限。

Stricky 权限位

Stricky位只有配置在目录上才有意义。当目录配置上sticky位时,其效果是即使所有的用户都拥有写权限和执行权限,该目录下的文件也只能被root或文件所有者删除。

内核实现

1
2
3
4
5
6
7
8
9
10
static int may_delete(struct inode *dir,struct dentry *victim,int isdir)
{
[...]
if (check_sticky(dir, victim->d_inode)||
IS_APPEND(victim->d_inode)||
IS_IMMUTABLE(victim->d_inode) ||
IS_SWAPFILE(victim->d_inode))
return -EPERM;
[...]
}

在删除文件前,内核要调用may_delete来判断该文件是否可以被删除。在这个函数中,内核通过调用check_sticky来检查文件的sticky标志位,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline int check_sticky(struct inode *dir, struct inode *inode)
{
/* 得到当前文件访问权限的uid */
uid_t fsuid = current_fsuid();
/* 判断上级目录是否设置了sticky标志位 */
if (!(dir->i_mode & S_ISVTX))
return 0;
/* 检查名称空间 */
if (current_user_ns() != inode_userns(inode))
goto other_userns;
/* 检查当前文件的uid是否与当前用户的uid相同 */
if (inode->i_uid == fsuid)
return 0;
/* 检查文件所处目录的uid是否与当前用户的uid相同 */
if (dir->i_uid == fsuid)
return 0;
/* 该文件不属于当前用户 */
other_userns:
return !ns_capable(inode_userns(inode), CAP_FOWNER);
}

当文件所处的目录设置了sticky位,即使用户(root用户除外)拥有了对应的权限,只要不是目录或文件的拥有者,就无法删除该文件。

stat、fstat、lstat、fstatat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

struct stat {
dev_t st_dev; /* 包含文件的设备的ID */
ino_t st_ino; /* inode值 */
mode_t st_mode; /* 类型及权限 */
nlink_t st_nlink; /* 硬链接数量 */
uid_t st_uid; /* 所有者的用户ID */
gid_t st_gid; /* 所有者的组ID */
dev_t st_rdev; /* 设备ID(如果是特殊文件) */
off_t st_size; /* 总大小,以byte为单位 */
blksize_t st_blksize; /* 文件系统I/O的块大小 */
blkcnt_t st_blocks; /* 分配的512B块数 */
time_t st_atime; /* 最后访问时间 */
time_t st_mtime; /* 最后修改时间 */
time_t st_ctime; /* 上次状态更改的时间 */
};

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
int fstatat(int dirfd, const char *pathname, struct stat *buf,int flags);

// 可以使用以下宏传递结构体成员st_mode查看文件类型:
S_ISREG(m) // 常规文件
S_ISDIR(m) // 目录
S_ISCHR(m) // 字符设备
S_ISBLK(m) // 块设备
S_ISFIFO(m) // FIFO文件
S_ISLNK(m) // 符号链接
S_ISSOCK(m) // 套接字文件

// 事实上以下就是st_mode字段定义的标志,也可以相“&”来读取文件的属性
S_IFMT 0170000 //文件类型的位遮罩
S_IFSOCK 0140000 //套接字
S_IFLNK 0120000 //符号连接
S_IFREG 0100000 //普通文件
S_IFBLK 0060000 //块设备
S_IFDIR 0040000 //目录
S_IFCHR 0020000 //字符设备
S_IFIFO 0010000 //FIFO
S_ISUID 0004000 //文件的(set user-id on execution)位
S_ISGID 0002000 //文件的(set group-id on execution)位
S_ISVTX 0001000 //文件的sticky位
S_IRWXU 00700 //文件所有者权限的掩码
S_IRUSR 00400 //拥有者读权限
S_IWUSR 00200 //拥有者写权限
S_IXUSR 00100 //拥有者执行权限
S_IRWXG 00070 //文件所在组权限的掩码
S_IRGRP 00040 //用户组读权限
S_IWGRP 00020 //用户组写权限
S_IXGRP 00010 //用户组执行权限
S_IRWXO 00007 //其他用户权限的掩码
S_IROTH 00004 //其他用户读权限
S_IWOTH 00002 //其他用户写权限
S_IXOTH 00001 //其他用户执行权限

stat 系列函数是 POSIX 标准库中的文件状态查询函数,用于获取文件的各种元数据,例如文件大小、文件类型、权限、最后访问时间等。它们有微妙的差别,适用于不同的使用场景。以下是对它们的详细说明:

stat

  • 功能: 获取文件的状态信息。
  • 原型: int stat(const char *path, struct stat *buf);
  • 参数:
    • path: 文件路径。
    • buf: 用于存储文件状态信息的 struct stat 结构体。
  • 返回值: 成功返回 0,失败返回 -1。
  • 特点: 如果路径是一个符号链接,stat 会对其进行解引用,即返回目标文件的信息。
1
2
3
4
5
6
7
struct stat buf;
int result = stat("/path/to/file", &buf);
if (result == 0) {
// 成功
} else {
// 处理错误
}

fstat

  • 功能: 获取文件描述符关联的文件的状态信息。
  • 原型: int fstat(int fd, struct stat *buf);
  • 参数:
    • fd: 文件描述符。
    • buf: 用于存储文件状态信息的 struct stat 结构体。
  • 返回值: 成功返回 0,失败返回 -1。
  • 特点: 适用于已经打开的文件。
1
2
3
4
5
6
7
8
9
int fd = open("/path/to/file", O_RDONLY);
struct stat buf;
int result = fstat(fd, &buf);
if (result == 0) {
// 成功
} else {
// 处理错误
}
close(fd);

lstat

  • 功能: 获取符号链接本身的状态信息。
  • 原型: int lstat(const char *path, struct stat *buf);
  • 参数:
    • path: 文件路径。
    • buf: 用于存储文件状态信息的 struct stat 结构体。
  • 返回值: 成功返回 0,失败返回 -1。
  • 特点: 与 stat 不同的是,如果路径是一个符号链接,lstat 返回的是符号链接本身的信息,而不是目标文件的信息。
1
2
3
4
5
6
7
struct stat buf;
int result = lstat("/path/to/symlink", &buf);
if (result == 0) {
// 成功
} else {
// 处理错误
}

fstatat

  • 功能: 获取文件的状态信息,可以选择性地不追踪符号链接并且具有获取相对路径的功能。
  • 原型: int fstatat(int dirfd, const char *pathname, struct stat *buf, int flags);
  • 参数:
    • dirfd: 目录文件描述符,表示 pathname 相对于哪个目录进行解析。可以使用 AT_FDCWD 表示当前工作目录。
    • pathname: 文件路径。
    • buf: 用于存储文件状态信息的 struct stat 结构体。
    • flags: 用于控制行为的标志,可以是 0(默认行为)或者 AT_SYMLINK_NOFOLLOW(不跟踪符号链接)。
  • 返回值: 成功返回 0,失败返回 -1。
  • 特点: 更加灵活,可以处理相对路径,支持不跟踪符号链接。
1
2
3
4
5
6
7
struct stat buf;
int result = fstatat(AT_FDCWD, "/path/to/file", &buf, 0);
if (result == 0) {
// 成功
} else {
// 处理错误
}

1
2
3
4
5
6
7
struct stat buf;
int result = fstatat(AT_FDCWD, "/path/to/symlink", &buf, AT_SYMLINK_NOFOLLOW);
if (result == 0) {
// 成功
} else {
// 处理错误
}

  • stat: 获取文件状态信息,并解引用符号链接,支持相对路径。
  • fstat: 获取已打开文件描述符的文件状态信息,支持相对路径。
  • lstat: 获取符号链接本身的状态信息,不解引用符号链接,支持相对路径。
  • fstatat: 更加灵活的文件状态获取函数,支持相对路径和可选的符号链接解引用。

这些函数在 UNIX 系统编程中非常常用,为文件管理提供了基础的支持。

文件截断

Linux提供了两个截断文件的API:

1
2
3
4
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

两者之间的唯一区别在于,truncate截断的是路径path指定的文件,ftruncate截断的是fd引用的文件。length可以大于文件本身的大小,这时文件长度将变为length的大小,扩充的内容均被填充为0。需要注意的是,尽管ftruncate使用的是文件描述符,但是其并不会更新当前文件的偏移。

truncate 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
SYSCALL_DEFINE2(truncate, const char __user *, path, long, length)
{
return do_sys_truncate(path, length);
}

static long do_sys_truncate(const char __user *pathname, loff_t length)
{
struct path path;
struct inode *inode;
int error;
error = -EINVAL;
/* 长度不能为负数 */
if (length < 0)
goto out;
/* 得到路径结构 */
error = user_path(pathname, &path);
if (error)
goto out;
inode = path.dentry->d_inode;
error = -EISDIR;
/* 目录不能被截断 */
if (S_ISDIR(inode->i_mode))
goto dput_and_out;
error = -EINVAL;
/* 不是普通文件不能被截断 */
if (!S_ISREG(inode->i_mode))
goto dput_and_out;
/* 尝试获得文件系统的写权限 */
error = mnt_want_write(path.mnt);
if (error)
goto dput_and_out;
/* 检查是否有文件写权限 */
error = inode_permission(inode, MAY_WRITE);
if (error)
goto mnt_drop_write_and_out;
error = -EPERM;
/* 文件设置了追加属性,则不能被截断 */
if (IS_APPEND(inode))
goto mnt_drop_write_and_out;
/* 得到inode的写权限 */
error = get_write_access(inode);
if (error)
goto mnt_drop_write_and_out;
/* 查看是否与文件lease锁相冲突 */
error = break_lease(inode, O_WRONLY);
/* 检查是否与文件锁相冲突 */
if (error)
goto put_write_and_out;
error = locks_verify_truncate(inode, NULL, length);
if (!error)
error = security_path_truncate(&path);
/* 如果没有错误,则进行真正的截断 */
if (!error)
error = do_truncate(path.dentry, length, 0, NULL);
put_write_and_out:
put_write_access(inode);
mnt_drop_write_and_out:
mnt_drop_write(path.mnt);
dput_and_out:
path_put(&path);
out:
return error;
}

int do_truncate(struct dentry *dentry, loff_t length, unsigned int time_attrs,
struct file *filp)
{
int ret;
struct iattr newattrs;
if (length < 0)
return -EINVAL;
/* 设置要改变的属性,对于截断来说,最重要的是文件长度 */
newattrs.ia_size = length;
newattrs.ia_valid = ATTR_SIZE | time_attrs;
if (filp) {
newattrs.ia_file = filp;
newattrs.ia_valid |= ATTR_FILE;
}
/*
* suid权限一定会被去掉同时设置sgid和xgrp时,sgid权限也会被去掉
*/
ret = should_remove_suid(dentry);
if (ret)
newattrs.ia_valid |= ret | ATTR_FORCE;
/* 修改inode属性 */
mutex_lock(&dentry->d_inode->i_mutex);
ret = notify_change(dentry, &newattrs);
mutex_unlock(&dentry->d_inode->i_mutex);
return ret;
}

truncate 函数用于调整文件的大小。它可以将文件缩短或延长到指定的大小。如果文件被缩短,超出新长度的部分将被丢弃;如果文件被延长,则新增加的部分通常会填充零字节。

函数原型

1
2
3
#include <unistd.h>

int truncate(const char *path, off_t length);

参数

  • path:要调整大小的文件路径。
  • length:调整后的文件大小,以字节为单位。

返回值

  • 返回 0:表示成功。
  • 返回 -1:表示失败,并设置 errno 以指示错误原因。

使用示例

以下是一个使用 truncate 函数的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>

int main() {
const char *filename = "example.txt";
off_t new_length = 1024; // 将文件调整为 1024 字节

if (truncate(filename, new_length) == 0) {
printf("Successfully truncated the file to %ld bytes.\n", new_length);
} else {
perror("truncate");
}

return 0;
}

注意事项

  1. 权限要求:调用进程必须对文件具有写权限才能使用 truncate 函数。

  2. 文件类型truncate 只能用于常规文件,不能用于其他文件类型(例如目录或设备文件)。

  3. 文件延长:如果文件被延长,新增的部分通常会填充零字节,但这依赖于文件系统的实现。

  4. 错误处理:如果 truncate 失败,可以通过检查 errno 来获取错误的具体原因。例如,可能的错误包括没有足够的权限 (EACCES)、文件不存在 (ENOENT)、或路径名无效 (EINVAL)。

  5. 兼容性truncate 是 POSIX 标准的一部分,因此在大多数 Unix-like 系统上都可用。

常见错误代码

  • EACCES:没有写权限。
  • ENOENT:文件不存在。
  • EINVAL:无效的长度。
  • EISDIR:路径指向一个目录,而不是一个常规文件。

通过 truncate 函数,你可以方便地调整文件的大小,尤其是在需要清理文件内容或准备文件空间时。

ftruncate 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
SYSCALL_DEFINE2(ftruncate, unsigned int, fd, unsigned long, length)
{
/* 真正的工作函数do_sys_ftruncate */
long ret = do_sys_ftruncate(fd, length, 1);
/* avoid REGPARM breakage on x86: */
asmlinkage_protect(2, ret, fd, length);
return ret;
}
static long do_sys_ftruncate(unsigned int fd, loff_t length, int small)
{
struct inode * inode;
struct dentry *dentry;
struct file * file;
int error;
error = -EINVAL;
/* 长度检查 */
if (length < 0)
goto out;
error = -EBADF;
/* 从文件描述符得到file指针 */
file = fget(fd);
if (!file)
goto out;
/* 如果文件是以O_LARGEFILE选项打开的,则将标志small置为0即假 */
if (file->f_flags & O_LARGEFILE)
small = 0;
dentry = file->f_path.dentry;
inode = dentry->d_inode;
error = -EINVAL;
/* 如果文件不是普通文件或文件不是写打开,则报错 */
if (!S_ISREG(inode->i_mode) || !(file->f_mode & FMODE_WRITE))
goto out_putf;
error = -EINVAL; /* Cannot ftruncate over 2^31 bytes without large file support */
/* 如果文件不是以O_LARGEFILE打开的话,长度就不能超过MAX_NON_LFS */
if (small && length > MAX_NON_LFS)
goto out_putf;
error = -EPERM;
/* 如果是追加模式打开的,也不能进行截断 */
if (IS_APPEND(inode))
goto out_putf;
/* 检查是否有锁冲突 */
error = locks_verify_truncate(inode, file, length);
if (!error)
error = security_path_truncate(&file->f_path);
if (!error) {
/* 执行截断操作—前文已经分析过 */
error = do_truncate(dentry, length, ATTR_MTIME|ATTR_CTIME, file);}
out_putf:
fput(file);
out:
return error;
}

ftruncate 函数用于调整已打开文件的大小,与 truncate 类似,但它是通过文件描述符来操作文件的。这种方法通常用于已经打开的文件,尤其是在需要对文件进行写操作时。

函数原型

1
2
3
#include <unistd.h>

int ftruncate(int fd, off_t length);

参数

  • fd:文件描述符,表示已经打开的文件。
  • length:调整后的文件大小,以字节为单位。

返回值

  • 返回 0:表示成功。
  • 返回 -1:表示失败,并设置 errno 以指示错误原因。

使用示例

以下是一个使用 ftruncate 函数的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
const char *filename = "example.txt";
off_t new_length = 1024; // 将文件调整为 1024 字节

// 打开文件以进行读写操作
int fd = open(filename, O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}

// 调整文件大小
if (ftruncate(fd, new_length) == 0) {
printf("Successfully truncated the file to %ld bytes.\n", new_length);
} else {
perror("ftruncate");
}

// 关闭文件
close(fd);

return 0;
}

注意事项

  1. 文件描述符ftruncate 使用文件描述符来指定文件,这意味着文件必须已经被打开,并且通常需要以写模式打开(例如 O_RDWRO_WRONLY)。

  2. 权限要求:调用进程需要对文件具有写权限。

  3. 文件类型ftruncate 只能用于常规文件,不能用于其他文件类型(例如目录或设备文件)。

  4. 文件延长:如果文件被延长,新增的部分通常会填充零字节,但这取决于文件系统的实现。

  5. 错误处理:如果 ftruncate 失败,可以通过检查 errno 来获取错误的具体原因。例如,可能的错误包括没有足够的权限 (EACCES)、无效的文件描述符 (EBADF)、或无效的长度 (EINVAL)。

  6. 兼容性ftruncate 是 POSIX 标准的一部分,因此在大多数 Unix-like 系统上都可用。

常见错误代码

  • EBADF:文件描述符无效,或者文件未以写模式打开。
  • EINVAL:无效的长度。
  • EFBIG:文件系统不支持这么大的文件。
  • EIO:I/O 错误。

ftruncate 函数提供了一种灵活的方法来调整文件的大小,尤其适用于需要对打开文件进行动态调整的场景。

标准库

I/O 操作

stdin,stdout和stderr

当Linux新建一个进程时,会自动创建3个文件描述符0、1和2,分别对应标准输入、标准输出和错误输出。C库中与文件描述符对应的是文件指针,与文件描述符0、1和2类似,我们可以直接使用文件指针stdin、stdout和stderr。文件描述符采用最小返回,0, 1, 2分别为 stdin, stdout和stderr,如果我们close(0),那麼再打开文件描述符将返回0。

查看C库头文件stdio.h中的源码:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IO_FILE FILE;
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
#ifdef __STDC__
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#endif

从上面的源码可以看出,stdin、stdout和stderr确实是文件指针。而C标准要求stdin、stdout和stderr是宏定义,所以在C库的代码中又定义了同名宏。stdin、stdout和stderr 定义代码如下:

1
2
3
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

继续查看_IO_2_1_stdin_等的定义,代码如下:

1
2
3
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

DEF_STDFILE是一个宏定义,用于初始化C库中的FILE结构。这里_IO_2_1_stdin_IO_2_1_stdout_IO_2_1_stderr这三个FILE结构分别用于文件描述符0、1和2的初始化,这样C库的文件指针就与系统的文件描述符互相关联起来了。大家注意最后的标志位,stdin是不可写的,stdout是不可读的,而stderr不仅不可读,且没有缓存。

fopen

fopen 是一个用于打开文件的函数。它的定义在 stdio.h 头文件中。fopen 函数的基本语法如下:

1
FILE *fopen(const char *filename, const char *mode);

参数解析

  1. filename
    • 类型:const char *
    • 说明:要打开的文件的路径,可以是相对路径或绝对路径。如果文件在当前工作目录下,可以只用文件名;如果在其他目录下,需要提供完整的路径。
  2. mode
    • 类型:const char *
    • 说明:指定打开文件的模式。它决定了文件的操作方式(只读、只写、追加等)。常用的模式有:
      • "r":只读模式,文件必须存在。
      • "w":只写模式,若文件存在则清空文件内容,若不存在则创建新文件。
      • "a":追加模式,若文件存在则将在文件末尾写入,若不存在则创建新文件。
      • "r+":读写模式,文件必须存在。
      • "w+":读写模式,若文件存在则清空文件内容,若不存在则创建新文件。
      • "a+":读写模式,能够在文件末尾写入,若不存在则创建新文件。
      • "b":二进制模式,通常与其他模式组合使用,例如 "rb""wb",用于处理二进制文件。

注意事项

  • 确保在使用 fopen 后检查返回值是否为 NULL,以确定文件是否成功打开。
  • 使用 fclose 函数关闭文件,释放相关资源。
  • 处理文件时,如果采用二进制模式(例如 "rb""wb"),需注意在处理文本与二进制数据时的区别。
  • 当以追加模式打开文件时,可以从文件末尾开始读取,但写入时会追加到文件末尾。

fdopen与fileno

Linux提供了文件描述符,而C库又提供了文件流。在平时的工作中,有时候需要在两者之间进行切换,因此C库提供了两个API:

1
2
3
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
int fileno(FILE *stream);

fdopen用于从文件描述符fd生成一个文件流FILE,而fileno则用于从文件流FILE得到对应的文件描述符。

fdopen 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
FILE *
_IO_new_fdopen (int fd, const char *mode)
{
int read_write;
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f;
int i;
int use_mmap = 0;

/* Decide whether we modify the offset of the file we attach to and seek to
the end of file. We only do this if the mode is 'a' and if the file
descriptor did not have O_APPEND in its flags already. */
bool do_seek = false;

switch (*mode)
{
case 'r':
read_write = _IO_NO_WRITES;
break;
case 'w':
read_write = _IO_NO_READS;
break;
case 'a':
read_write = _IO_NO_READS|_IO_IS_APPENDING;
break;
default:
__set_errno (EINVAL);
return NULL;
}
for (i = 1; i < 5; ++i)
{
switch (*++mode)
{
case '\0':
break;
case '+':
read_write &= _IO_IS_APPENDING;
break;
case 'm':
use_mmap = 1;
continue;
case 'x':
case 'b':
default:
/* Ignore */
continue;
}
break;
}
int fd_flags = __fcntl (fd, F_GETFL);
if (fd_flags == -1)
return NULL;

if (((fd_flags & O_ACCMODE) == O_RDONLY && !(read_write & _IO_NO_WRITES))
|| ((fd_flags & O_ACCMODE) == O_WRONLY && !(read_write & _IO_NO_READS)))
{
__set_errno (EINVAL);
return NULL;
}

/* The May 93 draft of P1003.4/D14.1 (redesignated as 1003.1b)
[System Application Program Interface (API) Amendment 1:
Realtime Extensions], Rationale B.8.3.3
Open a Stream on a File Descriptor says:

Although not explicitly required by POSIX.1, a good
implementation of append ("a") mode would cause the
O_APPEND flag to be set.

(Historical implementations [such as Solaris2] do a one-time
seek in fdopen.)

However, we do not turn O_APPEND off if the mode is "w" (even
though that would seem consistent) because that would be more
likely to break historical programs.
*/
if ((read_write & _IO_IS_APPENDING) && !(fd_flags & O_APPEND))
{
do_seek = true;
if (__fcntl (fd, F_SETFL, fd_flags | O_APPEND) == -1)
return NULL;
}

new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd,
#if _G_HAVE_MMAP
(use_mmap && (read_write & _IO_NO_WRITES))
? &_IO_wfile_jumps_maybe_mmap :
#endif
&_IO_wfile_jumps);
_IO_JUMPS (&new_f->fp) =
#if _G_HAVE_MMAP
(use_mmap && (read_write & _IO_NO_WRITES)) ? &_IO_file_jumps_maybe_mmap :
#endif
&_IO_file_jumps;
_IO_new_file_init_internal (&new_f->fp);
/* We only need to record the fd because _IO_file_init_internal will
have unset the offset. It is important to unset the cached
offset because the real offset in the file could change between
now and when the handle is activated and we would then mislead
ftell into believing that we have a valid offset. */
new_f->fp.file._fileno = fd;
new_f->fp.file._flags &= ~_IO_DELETE_DONT_CLOSE;

_IO_mask_flags (&new_f->fp.file, read_write,
_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);

/* For append mode, set the file offset to the end of the file if we added
O_APPEND to the file descriptor flags. Don't update the offset cache
though, since the file handle is not active. */
if (do_seek && ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
== (_IO_IS_APPENDING | _IO_NO_READS)))
{
off64_t new_pos = _IO_SYSSEEK (&new_f->fp.file, 0, _IO_seek_end);
if (new_pos == _IO_pos_BAD && errno != ESPIPE)
return NULL;
}
return &new_f->fp.file;
}

其基本工作是创建一个新的文件流FILE,并建立文件流FILE与描述符的对应关系。


fileno 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int
__fileno (FILE *fp)
{
CHECK_FILE (fp, EOF);

if (!(fp->_flags & _IO_IS_FILEBUF) || _IO_fileno (fp) < 0)
{
__set_errno (EBADF);
return -1;
}

return _IO_fileno (fp);
}

从fileno的实现基本上就可以得知文件流与文件描述符的对应关系。文件流FILE保存了文件描述符的值。当从文件流转换到文件描述符时,可以直接通过当前FILE保存的值_fileno得到fd。而从文件描述符转换到文件流时,C库返回的都是一个重新申请的文件流FILE,且这个FILE的_fileno保存了文件描述符。因此无论是fdopen还是fileno,关闭文件时,都要使用fclose来关闭文件,而不是用close。因为只有采用此方式,fclose作为C库函数,才会释放文件流FILE占用的内存。

ferror

ferror用于告诉用户C库的文件流FILE是否有错误发生。当有错误发生时,ferror返回非零值,反之则返回0。那么ferror是否会返回不同的错误呢?让我们来看看ferror的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
weak_alias (_IO_ferror, ferror)
int _IO_ferror (fp)
_IO_FILE* fp;
{
int result;
/* 检查文件流的有效性,失败则返回EOF */
CHECK_FILE (fp, EOF);
_IO_flockfile (fp);
result = _IO_ferror_unlocked (fp);
_IO_funlockfile (fp);
return result;
}

#define _IO_ferror_unlocked(__fp) (((__fp)->_flags & _IO_ERR_SEEN) != 0)
#define _IO_ERR_SEEN 0x20

从源码上可以看出ferror有两个返回值:

  • 当文件流FILE *fp非法时,返回EOF(-1)

  • 当文件流FILE *fp前面的操作发生错误时,返回1。

并且由于文件流的错误只是使用一个标志位_IO_ERR_SEEN来表示的,因此ferror的返回值就不可能针对不同的错误返回不同的值了。

clearerr

clearerr用于清除文件流的文件结束位和错误位。查看clearerr的实现,代码如下:

1
2
3
4
5
6
7
8
9
#define clearerr_unlocked(x) clearerr (x)
void
clearerr_unlocked (fp)
FILE *fp;
{
CHECK_FILE (fp, /*nothing*/);
_IO_clearerr (fp);
}
#define _IO_clearerr(FP) ((FP)->_flags &= ~(_IO_ERR_SEEN|_IO_EOF_SEEN))

可见,clearerr可以清除文件流中的文件结尾标志和错误标志。

getc/fgetc

fgetc和getc是两个定义得很不友好的函数,其函数名中的getc很容易让使用者误以为其返回值是char字符。实际上两个函数的接口定义如下:

1
2
3
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);

两者的返回值都是int类型。为什么要用int类型作为返回值呢?因为当文件流读到文件尾时,需要返回EOF值。C99标准中规定了EOF为一个int类型的负数常量,并没有规定具体的值。在glibc中,EOF被定义为-1且char为有符号数。但是不能排除某些实现将EOF定义为其他负值,甚至可能因为不遵守C99标准,EOF的值有可能超过char的表示范围。因此,为了代码的健壮性和可移植性,在使用fgetc和getc时,应使用int类型的变量保存其返回值。

fwrite/fread

fread和fwrite的声明代码如下:

1
2
3
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

这两个函数原型很容易让人产生误解。当看到返回值类型为size_t时,人们很有可能理解为fread和fwrite会返回成功读取或写入的字节数,然而实际上其返回的是成功读取或写入的个数,即有多少个size大小的对象被成功读取或写入了。而参数nmemb则用于指示fread或fwrite要执行的对象个数。

tmpnam/tmpfile/mkstemp

在项目中经常会需要生成临时文件,用于保存临时数据,创建管道文件、Unix域socket等。为了不与已有的文件同名,或者避免与其他临时文件相冲突,有些朋友可能会选择利用进程id、时间戳等来生成临时文件名。其实,C库已经提供了生成临时文件的接口。下面对生成临时文件的各种方法进行分析对比。先来看看tmpnam方式,代码如下:

1
2
#include <stdio.h>
char *tmpnam(char *s);

tmpnam 会返回一个目前系统不存在的临时文件名。当s为NULL时,返回的文件名保存在一个静态的缓存中,因此再次调用tmpnam时,新生成的文件名会覆盖上一次的结果。当s不为NULL时,生成的临时文件名会保存在s中,因此要求s至少要有C库规定的L_tmpnam大小。C库同时还规定tmpnam产生的临时文件的路径以P_tmpdir开头——glibc中P_tmpdir定义为/tmp。从上面的描述中可以清楚地发现tmpnam的缺点:

  • 当s为NULL时,tmpnam不是线程安全的。
  • tmpnam生成的临时文件名,必须位于固定的路径下(/tmp)。
  • 使用tmpnam创建临时文件不是一个原子行为,需要先生成临时文件名,然后调用其他I/O函数创建文件。这有可能会导致在创建文件时,该文件已经存在。

1
2
#include <stdio.h>
FILE *tmpfile(void);

tmpfile返回一个以读写模式打开的、唯一的临时文件流指针。当文件指针关闭或程序正常结束时,该临时文件会被自动删除。tmpfile直接返回临时的文件流指针——这个自然避免了tmpnam中潜在的线程安全问题,同时还避免了将生成文件名和创建文件分为两个步骤来执行的行为。那么tmpfile是否真的实现了原子地创建临时文件?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FILE *
tmpfile (void)
{
char buf[FILENAME_MAX];
int fd;
FILE *f;
if (__path_search (buf, FILENAME_MAX, NULL, "tmpf", 0))
return NULL;
int flags = 0;
#ifdef FLAGS
flags = FLAGS;
#endif
fd = __gen_tempname (buf, 0, flags, __GT_FILE);
if (fd < 0)
return NULL;
/* Note that this relies on the UNIX semantics that
a file is not really removed until it is closed. */
(void) __unlink (buf);
if ((f = __fdopen (fd, "w+b")) == NULL)
__close (fd);
return f;
}

乍一看,tmpfile是通过__path_search先产生临时文件名,然后再创建该文件,最后通过文件句柄生成文件流指针。这样的过程看上去好像并不是原子的。下面,让我们深入到__gen_tempname中一探究竟。

1
2
3
4
5
case __GT_FILE:
fd = __open (tmpl,
(flags & ~O_ACCMODE)
| O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
break;

在创建临时文件时,C库使用了open函数的O_CREAT和O_EXCL标志组合,这点保证了文件的原子性创建,从而使tmpfile创建临时文件的行为是原子的。但tmpfile也有一个缺点,与tmpnam相同,这个临时文件只能生成在固定的路径下(/tmp),并且其有可能因为文件名称冲突而失败返回NULL。


1
2
#include <stdlib.h>
int mkstemp(char *template);

mkstemp会根据template创建并打开一个独一无二的临时文件。template的最后6个字符必须是“XXXXXX”。glibc库会生成一个独一无二的后缀来替换“XXXXXX”,因此要求template必须是可以修改的。mkstemp执行成功后会返回创建的临时文件的文件描述符,失败时则返回-1。下面看一下mkstemp的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
int mkstemp (char *template)
{
return __gen_tempname (template, 0, 0, __GT_FILE);
}
int __gen_tempname (char *tmpl, int suffixlen, int flags, int kind)
{
int len;
char *XXXXXX;
static uint64_t value;
uint64_t random_time_bits;
unsigned int count;
int fd = -1;
int save_errno = errno;
struct_stat64 st;
#define ATTEMPTS_MIN (62 * 62 * 62)
/* The number of times to attempt to generate a temporary file. To
conform to POSIX, this must be no smaller than TMP_MAX. */
#if ATTEMPTS_MIN < TMP_MAX
unsigned int attempts = TMP_MAX;
#else
unsigned int attempts = ATTEMPTS_MIN;
#endif
/* 检查template的合法性,检查长度及结尾的XXXXXX字符 */
len = strlen (tmpl);
if (len < 6 + suffixlen || memcmp (&tmpl[len - 6 - suffixlen], "XXXXXX", 6))
{
__set_errno (EINVAL);
return -1;
}
/* 得到结尾XXXXXX起始位置 */
XXXXXX = &tmpl[len - 6 - suffixlen];
/* 得到“随机”数据 */
#ifdef RANDOM_BITS
RANDOM_BITS (random_time_bits);
#else
#if HAVE_GETTIMEOFDAY || _LIBC
{
struct timeval tv;
__gettimeofday (&tv, NULL);
random_time_bits = ((uint64_t) tv.tv_usec << 16) ^
tv.tv_sec;
}
#else
random_time_bits = time (NULL);
#endif
#endif
/* 根据上面的伪随机数和进程pid生成value */
value += random_time_bits ^ __getpid ();
/*
根据value得到唯一的临时文件名,如有重复则加上7777继续。最多重复attempts次。
*/
for (count = 0; count < attempts; value += 7777, ++count)
{
uint64_t v = value;
/*
letters是26个英文大小写加上10个阿拉伯数字,为62个大小的字符数组。因此使用62作为除数,以得到随机字符。
*/
XXXXXX[0] = letters[v % 62];
v /= 62;
XXXXXX[1] = letters[v % 62];
v /= 62;
XXXXXX[2] = letters[v % 62];
v /= 62;
XXXXXX[3] = letters[v % 62];
v /= 62;
XXXXXX[4] = letters[v % 62];
v /= 62;
XXXXXX[5] = letters[v % 62];
switch (kind)
{ case __GT_FILE:
/* 这是mkstemp的情况,利用O_CREAT|O_EXCL创建唯一文件 */
fd = __open (tmpl,
(flags & ~O_ACCMODE)
| O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
break;
}
if (fd >= 0)
{
/* 成功创建了文件,恢复原来的errno,并返回创建的文件描述符fd */
__set_errno (save_errno);
return fd;
}
else if (errno != EEXIST) {
/* 如失败的原因不是因为文件已经存在的时候,则直接返回。*/
return -1;
}
/* 如果是其他原因,则会重新生成新的文件名,并再次尝试重建 */
}
/* 将errno设置为EEXIST,即文件已经存在*/
__set_errno (EEXIST);
return -1;
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//1.mkstemp函数在系统中以唯一的文件名创建一个文件并打开,而且只有当前用户才能访问这个临时文件,
//2.mkstemp函数只有一个参数,这个参数是个以“XXXXXX”结尾的非空字符串。mkstemp函数会用随机产生的字符串替换“XXXXXX”,保证 了文件名的唯一性
//3.由于mkstemp函数创建的临时文件不能自动删除,所以执行完 mkstemp函数后要调用unlink函数,unlink函数删除文件的目录入口

char temp_template[] = "/tmp/htp.XXXXXX";

tfd = mkstemp(temp_template);
if(!(tfp = fdopen(tfd,"w"))) {
fprintf(stderr,"Could not open temp file.\n");
exit(1);
}

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
int fd;
char temp_file[]="tmp_XXXXXX";
/*Creat a temp file.*/
if((fd=mkstemp(temp_file))==-1)
{
printf("Creat temp file faile./n");
exit(1);
}
/*Unlink the temp file.*/
unlink(temp_file);
/*Then you can read or write the temp file.*/
//ADD YOUR CODE;
/*Close temp file, when exit this program, the temp file will be removed.*/
close(fd);
}

在需要使用临时文件时,不推荐使用tmpnam,而要用tmpfile和mkstemp。前者的局限在于不能指定路径,并且在文件名称冲突时会返回失败。后者可以由调用者来指定路径,并且在文件名称冲突时,会自动重新生成并重试。

除了上面介绍的几种方法,Linux环境还提供了这些接口的一些变种:tempnam、mkostemp、mkstemps等,分别对其原始形态进行了扩展,详细区别可以直接查看Linux手册。

目录/文件操作

在C语言中,操作目录和文件的过程中,会接触到一些相关的结构体,主要有以下几个关键的结构体:

  1. DIR 结构体
  2. struct dirent 结构体
  3. struct stat 结构体
  4. struct statvfs 结构体

下面是这些结构体的一些详细说明:

DIR 结构体

DIR 结构体是用来表示目录流的,它的具体定义在 POSIX 标准中未公开,通常是在使用 opendir 打开一个目录后,得到一个 DIR * 类型的指针,通过该指针可进行目录流的操作。

1
2
3
4
5
6
#include <dirent.h>

// DIR 是一个不透明的结构体,对它的操作应该通过相应的函数接口进行。
typedef struct {
// 内部成员(具体实现依系统而定)
} DIR;

struct dirent 结构体

struct dirent 结构体用于表示目录项的相关信息。它常与 readdir 函数一起使用。

1
2
3
4
5
6
7
8
9
#include <dirent.h>

struct dirent {
ino_t d_ino; // 索引节点号
off_t d_off; // 到下一个目录项的偏移量
unsigned short d_reclen; // d_name 的长度
unsigned char d_type; // 文件类型
char d_name[256]; // 文件名(以 null 结尾)
};

struct stat 结构体

struct stat 结构体用于存储文件的状态信息,包括文件类型、权限、大小等。常与 statfstatlstat 等函数一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/stat.h>

// 这是 sys/stat.h 中的一个示例定义,具体字段数量因系统不同而异
struct stat {
dev_t st_dev; // 文件所在的设备
ino_t st_ino; // 索引节点号
mode_t st_mode; // 文件的类型和权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 所有者的用户 ID
gid_t st_gid; // 所有者的组 ID
dev_t st_rdev; // 设备 ID(如果是特殊文件)
off_t st_size; // 文件大小(以字节为单位)
blksize_t st_blksize; // 文件系统 I/O 块大小
blkcnt_t st_blocks; // 分配的块数

// 文件时间
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态更改时间
};

struct statvfs 结构体

struct statvfs 结构体用于表示文件系统的信息,常与 statvfs 函数一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/statvfs.h>

struct statvfs {
unsigned long f_bsize; // 文件系统块大小
unsigned long f_frsize; // 基本块大小
fsblkcnt_t f_blocks; // 总块数
fsblkcnt_t f_bfree; // 空闲块数
fsblkcnt_t f_bavail; // 非超级用户可获取的块数
fsfilcnt_t f_files; // 总文件数
fsfilcnt_t f_ffree; // 空闲文件数
fsfilcnt_t f_favail; // 非超级用户可获取的文件数
unsigned long f_fsid; // 文件系统标识符
unsigned long f_flag; // 挂载标志
unsigned long f_namemax; // 最大文件名长度
};

示例代码

下面是一个综合示例,展示如何使用这些结构体和相关的函数操作目录和文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main() {
const char *path = "."; // 当前目录
DIR *dir;
struct dirent *entry;
struct stat statbuf;

// 打开目录
if ((dir = opendir(path)) == NULL) {
perror("opendir failed");
return 1;
}

// 读取目录项
while ((entry = readdir(dir)) != NULL) {
if (stat(entry->d_name, &statbuf) == -1) {
perror("stat failed");
continue;
}

// 打印目录项信息
printf("Name: %s\n", entry->d_name);
printf("Type: ");
switch (entry->d_type) {
case DT_REG: printf("regular file\n"); break;
case DT_DIR: printf("directory\n"); break;
case DT_LNK: printf("symbolic link\n"); break;
default: printf("other\n"); break;
}
printf("Size: %lld bytes\n", (long long)statbuf.st_size);
printf("Permissions: %o\n", statbuf.st_mode & 0777);
printf("Last modification: %s\n", ctime(&statbuf.st_mtime));
}

// 关闭目录
closedir(dir);

return 0;
}

这个示例演示了如何使用 opendirreaddirstat 函数来读取当前目录中的文件和目录信息,并打印出每个条目的名称、类型、大小、权限和最后修改时间。

access() 函数

access 函数是一个用于检查文件的访问权限的系统调用。在 Linux 和其他类 Unix 系统中,它可以用来验证调用进程是否有权限读取、写入或执行指定的文件。access 是 POSIX 标准的一部分,通常用于在实际尝试打开或操作文件之前进行权限检查。

函数原型

1
2
3
#include <unistd.h>

int access(const char *pathname, int mode);

参数

  • pathname:要检查的文件路径。
  • mode:要检查的权限类型,可以是以下值的组合:
    • R_OK:检查是否有读权限。
    • W_OK:检查是否有写权限。
    • X_OK:检查是否有执行权限。
    • F_OK:检查文件是否存在。

这些标志可以组合使用,例如,R_OK | W_OK 用于检查是否同时具有读和写权限。

返回值

  • 返回 0:表示调用进程具有指定的访问权限。
  • 返回 -1:表示调用进程不具有指定的访问权限,或者发生了其他错误(例如,文件不存在)。在这种情况下,errno 会被设置为具体的错误代码。

常见用法

access 函数通常用于在执行文件操作之前进行权限检查。例如:

1
2
3
4
5
if (access("/path/to/file", R_OK) == 0) {
printf("File is readable\n");
} else {
perror("access");
}

注意事项

  1. 权限变化access 检查的是当前进程的真实用户 ID 和组 ID 的权限,而不是有效用户 ID 和组 ID。这在使用 setuid 程序时尤其重要。

  2. 安全性:使用 access 进行权限检查并不能保证后续操作的安全性,因为在检查和实际操作之间,文件的权限可能会发生变化。这种情况被称为 TOCTOU(Time Of Check to Time Of Use)漏洞。因此,access 不应该用于安全性检查,而只是用于提高用户体验。

  3. 符号链接:如果 pathname 是一个符号链接,access 会检查符号链接指向的目标文件的权限,而不是符号链接本身。

示例

以下是一个简单的例子,检查某个文件是否存在并且可读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <unistd.h>

int main() {
const char *filename = "/path/to/file";

if (access(filename, F_OK) == 0) {
printf("File exists.\n");
if (access(filename, R_OK) == 0) {
printf("File is readable.\n");
} else {
printf("File is not readable.\n");
}
} else {
printf("File does not exist.\n");
}

return 0;
}

在这个例子中,我们首先检查文件是否存在,然后再检查文件是否可读。根据检查结果,程序会输出相应的信息。

mkdir 函数

mkdir 用于创建一个新的目录。

定义

1
2
3
4
#include <sys/stat.h>
#include <sys/types.h>

int mkdir(const char *pathname, mode_t mode);

参数

  • pathname:要创建的目录的路径。
  • mode:指定新目录的访问权限。它是基于文件模式的掩码,例如 0755

返回值

  • 成功返回 0。
  • 失败返回 -1,并设置 errno 指示具体错误原因。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <errno.h>

int main() {
const char *path = "new_directory";
mode_t mode = 0755;

if (mkdir(path, mode) == 0) {
printf("Directory created successfully.\n");
} else {
perror("mkdir failed");
}

return 0;
}

opendir 函数

opendir 用于打开一个目录流,以便后续读取目录中的条目。

定义

1
2
3
#include <dirent.h>

DIR *opendir(const char *name);

参数

  • name:要打开的目录的路径。

返回值

  • 成功返回目录流的指针(DIR *)。
  • 失败返回 NULL,并设置 errno 指示具体错误原因。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <dirent.h>
#include <stdio.h>
#include <errno.h>

int main() {
const char *path = "."; // 当前目录

DIR *dir = opendir(path);
if (dir) {
printf("Directory opened successfully.\n");
closedir(dir); // 记得关闭目录
} else {
perror("opendir failed");
}

return 0;
}

readdir 函数

readdir 用于读取目录中的下一个条目。

定义

1
2
3
#include <dirent.h>

struct dirent *readdir(DIR *dirp);

参数

  • dirp:目录流的指针(DIR *),由 opendir 返回。

返回值

  • 成功返回指向下一个目录条目的指针。
  • 到达目录末尾或出错时返回 NULL。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <dirent.h>
#include <stdio.h>
#include <errno.h>

int main() {
const char *path = "."; // 当前目录

DIR *dir = opendir(path);
if (dir) {
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
printf("Found file: %s\n", entry->d_name);
}
closedir(dir); // 记得关闭目录
} else {
perror("opendir failed");
}

return 0;
}

chdir 函数

chdir 用于改变当前工作目录。

定义

1
2
3
#include <unistd.h>

int chdir(const char *path);

参数

  • path:要切换到的目录路径。

返回值

  • 成功返回 0。
  • 失败返回 -1,并设置 errno 指示具体错误原因。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

int main() {
const char *path = "/tmp"; // 想要切换到的目录

if (chdir(path) == 0) {
printf("Directory changed successfully.\n");
// 显示当前工作目录
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("Current working dir: %s\n", cwd);
} else {
perror("getcwd failed");
}
} else {
perror("chdir failed");
}

return 0;
}

closedir() 函数用于关闭一个打开的目录流,以释放资源。这个函数通常配合 opendir()readdir() 函数使用,在读取完目录信息后应调用 closedir() 以确保资源被正确释放。

closedir() 函数

定义

1
2
3
#include <dirent.h>

int closedir(DIR *dirp);

参数

  • dirp:指向先前由 opendir() 返回的 DIR 类型的指针,表示要关闭的目录流。

返回值

  • 成功返回 0。
  • 失败返回 -1,并设置 errno 表示具体的错误原因。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <dirent.h>
#include <errno.h>

int main() {
const char *path = "."; // 打开当前目录
DIR *dir;
struct dirent *entry;

// 打开目录流
dir = opendir(path);
if (dir == NULL) {
perror("opendir");
return -1;
}

// 读取目录项
while ((entry = readdir(dir)) != NULL) {
printf("Found file: %s\n", entry->d_name);
}

// 关闭目录流
if (closedir(dir) == -1) {
perror("closedir");
return -1;
}

printf("Directory closed successfully.\n");

return 0;
}

rmdir() 是一个用于删除空目录的系统调用。它从文件系统中删除由给定路径名指定的目录,但要求该目录必须为空。如果目录不为空,则 rmdir() 将会失败并返回错误。

rmdir() 函数

定义

1
2
3
#include <unistd.h>

int rmdir(const char *pathname);

参数

  • pathname:指定要删除的目录的路径名。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误。

常见错误

rmdir() 可能会返回以下几种常见错误:

  • EACCES:当前进程没有写入权限或者搜索权限。
  • EBUSY:目录正在被其他进程用作其当前工作目录或其它操作。
  • EIO:发生了硬件I/O错误。
  • ENAMETOOLONG:路径名过长。
  • ENOENT:指定的路径不存在。
  • ENOTDIR:路径中的一部分不是目录。
  • EPERM or EACCES:删除的目录位于只读文件系统中。
  • ENOTEMPTY or EEXIST:目录不为空。

示例代码

下面是一个使用 rmdir() 删除空目录的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main() {
const char *dirPath = "testdir"; // 指定要删除的目录

// 尝试删除目录
if (rmdir(dirPath) == -1) {
// 删除目录失败,打印错误信息
perror("rmdir failed");
return 1;
}

printf("Directory '%s' removed successfully.\n", dirPath);
return 0;
}

注意

  • 目录必须为空才能被删除。如果目录中包含文件或其他目录,rmdir() 将返回错误 ENOTEMPTY

  • 必须确保有删除目录的权限,否则调用 rmdir() 会失败并返回错误 EACCES

  • 删除目录的目录项路径必须有效,即路径必须存在,否则会返回错误 ENOENT

  • 尝试删除当前工作目录(即 ...) 会导致错误,并返回 EINVAL

  • 检查返回值和 errno 可帮助诊断 rmdir() 调用失败的原因。

为确保程序的健壮性和安全性,在尝试删除目录之前应当验证其空状态,并在捕获到错误情况时提供适当的错误处理逻辑。

getcwd() 函数

定义

1
2
#include <unistd.h>
char *getcwd(char *buf, size_t size);

功能:获取当前工作目录的绝对路径。

参数

  • buf:用于存储当前工作目录路径的缓冲区。
  • size:缓冲区的大小。

返回值

  • 成功时,返回 buf 即是绝对路径的字符串。
  • 出现错误时,返回 NULL,并设置 errno 以指示错误原因。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include <stdio.h>

int main() {
char cwd[PATH_MAX];

if (getcwd(cwd, sizeof(cwd)) == NULL) {
perror("getcwd failed");
return 1;
}

printf("Current working directory is '%s'.\n", cwd);
return 0;
}

stat() 系列函数

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

struct stat {
dev_t st_dev; /* 包含文件的设备的ID */
ino_t st_ino; /* inode值 */
mode_t st_mode; /* 类型及权限 */
nlink_t st_nlink; /* 硬链接数量 */
uid_t st_uid; /* 所有者的用户ID */
gid_t st_gid; /* 所有者的组ID */
dev_t st_rdev; /* 设备ID(如果是特殊文件) */
off_t st_size; /* 总大小,以byte为单位 */
blksize_t st_blksize; /* 文件系统I/O的块大小 */
blkcnt_t st_blocks; /* 分配的512B块数 */
time_t st_atime; /* 最后访问时间 */
time_t st_mtime; /* 最后修改时间 */
time_t st_ctime; /* 上次状态更改的时间 */
};

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
int fstatat(int dirfd, const char *pathname, struct stat *buf,int flags);

功能:获取文件或目录的状态信息。

参数和返回值看见上方文件元数据中的分析

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

int main() {
const char *path = ".";
struct stat statbuf;

if (stat(path, &statbuf) == -1) {
perror("stat failed");
return 1;
}

printf("Directory '%s' status:\n", path);
printf(" Size: %lld bytes\n", (long long) statbuf.st_size);
printf(" Blocks: %lld\n", (long long) statbuf.st_blocks);
printf(" IO Block: %ld bytes\n", (long) statbuf.st_blksize);
return 0;
}

rename() 函数

rename 函数是一个标准的 C 库函数,用于重命名文件或目录。这个函数接受两个参数:旧路径名和新路径名。无论是旧路径名还是新路径名,都可以使用相对路径或绝对路径。

函数原型

1
2
3
#include <stdio.h>

int rename(const char *oldpath, const char *newpath);

参数

  • oldpath:要重命名的文件或目录的路径。
  • newpath:文件或目录的新名称或路径。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误类型。

错误

rename 操作失败时,可能会设置以下 errno 值:

  • EACCES:对旧路径或新路径的访问被拒绝。
  • EBUSY:若某些系统特性使得文件忙时,不允许重命名。
  • EEXIST:当新路径已存在并且目录不为空时。
  • EINVAL:在文件系统中存在非法值。
  • EISDIR:新路径是一个不同于老路径的非空目录。
  • ENOENT:旧路径或新路径不存在。
  • ENOTDIR:路径中的组件不是目录。
  • EPERM:不允许操作或跨不同的文件系统。
  • EROFS:文件在只读文件系统中。

示例代码

下面是一些示例代码,展示了如何使用 rename 函数来重命名文件和目录。

  • 重命名文件
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <errno.h>

int main() {
// 重命名一个文件
if (rename("oldfile.txt", "newfile.txt") == 0) {
printf("File renamed successfully.\n");
} else {
perror("Error renaming file");
return 1;
}
return 0;
}
  • 重命名目录
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <errno.h>

int main() {
// 重命名一个目录
if (rename("olddir", "newdir") == 0) {
printf("Directory renamed successfully.\n");
} else {
perror("Error renaming directory");
return 1;
}
return 0;
}

注意事项

  1. 跨文件系统:在某些文件系统实现中,rename 函数不允许跨不同的文件系统。如果尝试在不同文件系统之间重命名文件,函数可能会失败,并且 errno 将被设置为 EXDEV

  2. 目标文件已存在:如果目标路径已经存在,并且目标不是目录或目标目录为空,rename 会覆盖目标。具体行为可能根据操作系统有所不同,但这通常是典型的实现。

  3. 文件权限:如果对操作的文件或目录没有适当的权限(如读/写权限),rename 操作会失败,errno 将被设置为 EACCESEPERM

remove() 函数

remove 是 C 标准库提供的一个函数,用于删除文件或空目录。它是一个相对简单的函数,主要用来处理基本的文件操作。这个函数声明在 <stdio.h> 头文件中。

函数原型

1
int remove(const char *filename);

参数

  • filename: 一个指向要删除的文件或空目录的名称的字符串指针。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno 以指示具体错误。

用法

  1. 删除文件:可以直接使用 remove 函数删除文件,只需要提供文件的路径。
  2. 删除空目录:在某些实现中(如 POSIX 系统),remove 也可以用于删除空目录。

示例代码

以下是如何使用 remove 来删除文件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
const char *filename = "example.txt";

if (remove(filename) == 0) {
printf("File '%s' deleted successfully.\n", filename);
} else {
perror("remove failed");
}

return 0;
}

在这个示例中,程序尝试删除名为 example.txt 的文件,并使用 perror 打印错误信息(如果有的话)。

错误处理

如果 remove 失败,通常是由于以下几个原因:

  • 文件不存在:试图删除的文件在指定路径中不存在。
  • 无权限:没有足够的权限删除指定的文件或目录。
  • 目录非空:尝试删除一个非空目录(如果系统支持用 remove 删除目录)。
  • 其他 I/O 错误:如磁盘故障、文件系统被保护等。

通常可以通过检查 errno 来获取具体的错误原因,并采取相应的措施。

使用注意

  • 在删除文件之前,确保已经完成了对文件的所有操作,因为一旦文件被删除,所有指向该文件的指针以及文件描述符都会变得无效。
  • 在使用 remove 删除目录时(如果可能),确保目录确实为空,以避免错误。
  • 始终做好错误处理,特别是涉及到文件或目录操作时,以保证程序的健壮性和安全性。

unlink 是 POSIX 标准中的一个系统调用,用于删除文件名,使文件相关联的路径名与文件本身解除关联。它在 <unistd.h> 头文件中声明,主要用于文件系统操作。

函数原型

1
int unlink(const char *pathname);

参数

  • pathname: 指向要删除的文件路径名的字符串指针。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno 以指示具体错误原因。

功能与特性

  • 删除文件路径unlink 取消了一个文件名与实际文件数据的关联。如果该文件有其他硬链接,那么只有指定的文件名会被删除,文件本身还存在。
  • 删除符号链接:当 pathname 指向一个符号链接时,unlink 删除的只是符号链接本身,而不是它指向的目标文件。
  • 文件数据的删除:当文件的所有路径名都被 unlink 并且没有进程打开该文件时,文件数据才会真正从文件系统中移除。
  • 不可删除目录:使用 unlink 不能删除目录。如果要删除空目录,应该使用 rmdir

用法示例

以下是使用 unlink 删除文件的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include <stdio.h>

int main() {
const char *filename = "example.txt";

if (unlink(filename) == 0) {
printf("File '%s' unlinked successfully.\n", filename);
} else {
perror("unlink failed");
}

return 0;
}

在这个代码中,我们尝试删除 example.txt 文件,并使用 perror 输出可能的错误信息。

常见错误

  • ENOENT: 文件不存在。
  • EACCES: 没有删除文件的权限。
  • EPERMEISDIR: 试图使用 unlink 删除一个目录。
  • EBUSY: 文件在某些情况下可能被挂载或作为交换文件正在使用。

使用注意

  • 数据丢失:一旦调用 unlink 删除文件名,该文件名就立即无效,数据可能不可恢复。
  • 权限:确保有足够的权限来删除文件。
  • 文件句柄unlink 后,文件仍然可以通过已经打开的文件句柄读写,直到文件所有句柄都关闭后,文件系统才释放文件的资源。

unlink 是一个底层的文件操作函数,通常用于需要直接控制文件删除特性的应用程序中。对于一般的文件删除操作,通常使用更简单、更通用的库函数,例如 remove

readlink 函数是一个用于读取符号链接(symbolic link)内容的函数,通常用于获取符号链接所指向的实际文件路径。在 Linux 中,readlink 定义在 <unistd.h> 头文件中。下面是对 readlink 函数的详细介绍:

函数原型

1
2
3
#include <unistd.h>

ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);

参数说明

  • pathname: 符号链接的路径。它是一个 C 样式的字符串,指向了符号链接文件。
  • buf: 用于存储符号链接指向路径的缓冲区。
  • bufsiz: buf 的大小,也就是缓冲区的大小。为了避免缓冲区溢出,你需要确保它足够大。

返回值

  • 成功时:返回写入到 buf 的字节数。注意返回的内容并不是一个以空字符 '\0' 结尾的字符串。
  • 失败时:返回 -1,同时设置 errno 指示错误类型。

常见错误

  • EACCES: 没有权限读取符号链接。
  • EINVAL: 给定的路径并不是一个符号链接。
  • EFAULT: pathnamebuf 指向了无效的内存地址。
  • ENAMETOOLONG: 路径名太长。
  • ENOENT: 指定的路径不存在。
  • ENOMEM: 内存不足。
  • ENOTDIR: 路径的某一部分不是目录。
  • ELOOP: 符号链接的解析过程中存在循环。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main() {
char *linkname = "/path/to/symlink";
char buf[1024];
ssize_t len;

len = readlink(linkname, buf, sizeof(buf) - 1);
if (len != -1) {
buf[len] = '\0'; // 手动结束字符串
printf("Symbolic link points to: %s\n", buf);
} else {
perror("readlink");
}

return 0;
}

注意事项

  • readlink 不会在返回的路径中包含空字符,所以在打印或处理返回路径时需要手动添加结束符。
  • 使用 readlink 时一定要确保 buf 的大小足够大,以便存储完整的目标路径。如果 buf 缓冲区太小,返回的字节数可能是 bufsiz,而不是真正的路径长度,而且内容可能会被截断。
  • 虽然 readlink 可以用于获取符号链接的路径,但对于不同的文件系统或边界条件,建议结合其他系统调用和函数(如 lstat)进行健壮性检查。

ftellrewind是C标准库中用于文件操作的两个函数,它们用于获取文件流的位置和重置文件流的位置。以下是对这两个函数的详细解释:

ftell 函数

功能

ftell用于获取文件流的当前读写位置的偏移量。该偏移量是相对于文件的开头以字节为单位的。

原型

1
long ftell(FILE *stream);

参数

  • stream: 指向FILE对象的指针,该对象标识了需要获取位置的文件流。

返回值

  • 成功时,返回文件流的当前偏移量(以字节为单位)。
  • 失败时,返回-1L,并设置errno以指示错误。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}

// 移动文件指针到文件的某个位置
fseek(file, 10, SEEK_SET);

// 获取当前位置
long position = ftell(file);
if (position == -1L) {
perror("ftell failed");
} else {
printf("Current position: %ld\n", position);
}

fclose(file);
return 0;
}

rewind 函数

功能

rewind用于将文件流的位置指针重置到文件的开头。

原型

1
void rewind(FILE *stream);

参数

  • stream: 指向FILE对象的指针,该对象标识了需要重置位置的文件流。

行为

  • rewind等效于调用fseek(stream, 0L, SEEK_SET),但不同之处在于rewind会清除与流相关的错误标志。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}

// 移动文件指针到文件的某个位置
fseek(file, 10, SEEK_SET);

// 重置文件指针到文件开头
rewind(file);

// 验证文件指针是否已重置
if (ftell(file) == 0) {
printf("File pointer successfully reset to the beginning.\n");
} else {
printf("Failed to reset file pointer.\n");
}

fclose(file);
return 0;
}

*fcntl() 函数

fcntl 是 POSIX 标准中定义的一个系统调用,用于对打开的文件描述符执行多种控制操作。该函数提供了更为丰富的文件控制能力,如修改文件描述符的属性、获取文件状态标识等。

函数原型

1
2
3
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */);
  • fd: 这是要操作的文件描述符。
  • cmd: 指定要执行的操作,具体操作取决于后面的可选参数。
  • arg: 可选参数,根据cmd的不同可以是一个整数或者指向某种结构的指针。

常用命令 (cmd)

  1. F_DUPFD: 复制文件描述符。

    • arg: 这是新文件描述符应该使用的最小值。
    • 返回值是新的文件描述符。
  2. F_GETFD: 获取文件描述符标志。

    • arg不使用,仅作占位。
    • 返回当前文件描述符标志。
  3. F_SETFD: 设置文件描述符标志。

    • arg: 用于设置的标志值。
  4. F_GETFL: 获取文件状态标志。

    • 这可以返回文件打开时的状态标志,如阻塞/非阻塞、同步/异步等。
  5. F_SETFL: 设置文件状态标志。

    • arg: 新的状态标志。
    • 一些常用标志包括 O_NONBLOCK, O_APPEND 等。
  6. F_GETOWN: 获取文件的“拥有者”(用于异步I/O通知)。

    • arg不使用,仅作占位。
    • 返回拥有者的进程ID或进程组ID。
  7. F_SETOWN: 设置文件的“拥有者”。

    • arg: 进程ID或进程组ID。
  8. F_GETLK, F_SETLK, F_SETLKW: 文件锁管理。

    • 用来获取、设置和等待文件锁。
    • arg: 传递struct flock指针,用于描述锁的属性。

文件锁结构struct flock

当使用F_GETLKF_SETLKF_SETLKW时,你需要提供一个struct flock结构来描述锁的细节:

1
2
3
4
5
6
7
struct flock {
short l_type; // 锁类型:F_RDLCK, F_WRLCK, F_UNLCK
short l_whence; // 偏移量测量基准:SEEK_SET, SEEK_CUR, SEEK_END
off_t l_start; // 锁区域的起始偏移
off_t l_len; // 锁区域的长度
pid_t l_pid; // 持有锁的进程ID(仅F_GETLK时有效)
};

返回值

  • 成功时,fcntl返回某些命令所请求的结果,如新文件描述符或原文件状态标志。
  • 出错时,返回 -1,并设置 errno 以指示具体错误。

示例代码

以下是一个简单使用fcntl设置文件描述符为非阻塞模式的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}

int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl - F_GETFL");
close(fd);
return 1;
}

flags |= O_NONBLOCK; // 设置非阻塞标志

if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl - F_SETFL");
close(fd);
return 1;
}

// 继续进行其他操作...

close(fd);
return 0;
}

在这个例子中,文件”example.txt”在非阻塞模式下打开,以便在读取操作需要等待时不挂起。fcntl 的灵活性和强大的能力使其成为进程控制文件I/O行为的核心工具之一。

I/O 多路复用

在 Linux 中,I/O 多路复用是一种高效的机制,用于同时监视多个文件描述符,以便在任何一个文件描述符变为可读、可写或发生错误时进行相应的处理。I/O 多路复用在网络编程中尤为重要,因为它允许单个线程或进程同时处理多个网络连接。Linux 提供了几种实现 I/O 多路复用的系统调用,主要包括 selectpollepoll

select

select 是最早的 I/O 多路复用机制,适用于监视一组文件描述符。

函数原型

1
2
3
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数

  • nfds: 需要监视的文件描述符数量,通常是所有文件描述符中最大值加一。
  • readfds: 指向 fd_set 的指针,用于监视可读事件。
  • writefds: 指向 fd_set 的指针,用于监视可写事件。
  • exceptfds: 指向 fd_set 的指针,用于监视异常事件。
  • timeout: 指定超时时间,NULL 表示无限等待。

返回值

  • 成功时返回就绪的文件描述符数量。
  • 失败时返回 -1,并设置 errno

使用步骤

  1. 初始化 fd_set 结构。
  2. 使用 FD_SET 宏将文件描述符添加到 fd_set
  3. 调用 select
  4. 使用 FD_ISSET 宏检查哪些文件描述符已就绪。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);

struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;

int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
if (FD_ISSET(sockfd, &readfds)) {
// 处理可读事件
}
}

在使用 select 函数进行 I/O 多路复用时,fd_set 结构用于表示一组文件描述符。为了操作 fd_set,POSIX 提供了一组宏:FD_CLRFD_ISSETFD_SETFD_ZERO。这些宏用于管理和检查文件描述符集合。

FD_CLR

1
void FD_CLR(int fd, fd_set *set);
  • 功能: 从文件描述符集合中移除指定的文件描述符。
  • 参数:
    • fd: 要移除的文件描述符。
    • set: 指向 fd_set 结构的指针。
  • 用法: 当你不再需要监视某个文件描述符时,可以使用 FD_CLR 将其从集合中移除。

FD_ISSET

1
int FD_ISSET(int fd, fd_set *set);
  • 功能: 检查指定的文件描述符是否在集合中。
  • 参数:
    • fd: 要检查的文件描述符。
    • set: 指向 fd_set 结构的指针。
  • 返回值: 如果文件描述符在集合中,则返回非零值;否则返回零。
  • 用法: 在调用 select 之后,使用 FD_ISSET 检查哪些文件描述符已就绪。

FD_SET

1
void FD_SET(int fd, fd_set *set);
  • 功能: 将指定的文件描述符添加到集合中。
  • 参数:
    • fd: 要添加的文件描述符。
    • set: 指向 fd_set 结构的指针。
  • 用法: 在调用 select 之前,使用 FD_SET 将需要监视的文件描述符添加到集合中。

FD_ZERO

1
void FD_ZERO(fd_set *set);
  • 功能: 清空文件描述符集合。
  • 参数:
    • set: 指向 fd_set 结构的指针。
  • 用法: 在使用 fd_set 之前,通常先调用 FD_ZERO 初始化集合。

示例

以下是一个简单的示例,演示如何使用这些宏与 select 结合进行 I/O 多路复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <unistd.h>

int main() {
fd_set readfds;
int fd = 0; // 通常是标准输入

// 初始化文件描述符集合
FD_ZERO(&readfds);
FD_SET(fd, &readfds);

// 设置超时时间
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;

// 调用 select
int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select");
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("Timeout occurred! No data after 5 seconds.\n");
} else {
if (FD_ISSET(fd, &readfds)) {
printf("Data is available to read.\n");
// 处理可读事件
}
}

return 0;
}

总结

  • FD_ZERO 用于初始化 fd_set
  • FD_SET 用于将文件描述符添加到 fd_set
  • FD_CLR 用于从 fd_set 中移除文件描述符。
  • FD_ISSET 用于检查文件描述符是否在 fd_set 中。

这些宏提供了一种简单的方式来管理和检查文件描述符集合,以便与 select 函数一起使用。

poll

pollselect 的改进版本,克服了一些限制,如文件描述符数量限制。

函数原型

1
2
3
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数

  • fds: 指向 pollfd 结构数组的指针。
  • nfds: 数组中的元素数量。
  • timeout: 超时时间,以毫秒为单位,-1 表示无限等待。

返回值

  • 成功时返回就绪的文件描述符数量。
  • 失败时返回 -1,并设置 errno

使用步骤

  1. 初始化 pollfd 结构数组。
  2. 设置每个文件描述符的事件掩码。
  3. 调用 poll
  4. 检查 revents 字段以确定哪些文件描述符已就绪。

示例

1
2
3
4
5
6
7
8
9
10
struct pollfd fds[1];
fds[0].fd = sockfd;
fds[0].events = POLLIN;

int ret = poll(fds, 1, 5000);
if (ret > 0) {
if (fds[0].revents & POLLIN) {
// 处理可读事件
}
}

epoll

epoll 是 Linux 特有的 I/O 多路复用机制,适用于大规模文件描述符监视。

函数原型

  • epoll_create1:创建一个 epoll 实例。

    1
    2
    3
    #include <sys/epoll.h>

    int epoll_create1(int flags);
  • epoll_ctl:控制 epoll 实例中的文件描述符。

    1
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epoll_wait:等待事件的发生。

    1
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

使用步骤

  1. 使用 epoll_create1 创建 epoll 实例。
  2. 使用 epoll_ctl 添加、修改或删除文件描述符。
  3. 使用 epoll_wait 等待事件发生。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}

struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sockfd;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}

struct epoll_event events[10];
int nfds = epoll_wait(epfd, events, 10, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
// 处理可读事件
}
}

close(epfd);

epoll_createepoll_create1 是用于创建 epoll 实例的系统调用。epoll 是 Linux 特有的 I/O 多路复用机制,适用于高效地监视大量文件描述符。虽然这两个函数都用于创建 epoll 实例,但它们之间有一些区别。

epoll_create

1
2
3
#include <sys/epoll.h>

int epoll_create(int size);

参数

  • size: 这个参数在现代 Linux 内核中已经被忽略,但在早期版本中,它用于建议内核分配的文件描述符数量。尽管如此,仍然需要传递一个大于零的值。

返回值

  • 成功时返回一个新的 epoll 文件描述符。
  • 失败时返回 -1,并设置 errno

注意

  • epoll_create 在现代使用中,size 参数没有实际意义,但仍然需要提供一个正整数。
  • 该函数在 Linux 2.6.8 及更高版本中被 epoll_create1 所取代。

epoll_create1

1
2
3
#include <sys/epoll.h>

int epoll_create1(int flags);

参数

  • flags: 可以是 0 或 EPOLL_CLOEXECEPOLL_CLOEXEC 标志用于在执行 exec 系列函数时自动关闭 epoll 文件描述符。

返回值

  • 成功时返回一个新的 epoll 文件描述符。
  • 失败时返回 -1,并设置 errno

优势

  • epoll_create1 提供了更灵活的接口,允许设置标志(如 EPOLL_CLOEXEC),这在多线程或多进程环境中非常有用。
  • 该函数在 Linux 2.6.27 及更高版本中可用。

使用示例

以下是如何使用 epoll_create1 创建一个 epoll 实例的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>

int main() {
int epfd = epoll_create1(0); // 不使用任何标志
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}

// 使用 epoll 实例进行其他操作...

close(epfd); // 关闭 epoll 文件描述符
return 0;
}

总结

  • epoll_create: 旧的接口,size 参数在现代内核中被忽略,但仍然需要提供。
  • epoll_create1: 新的接口,支持 EPOLL_CLOEXEC 标志,推荐在现代应用中使用。

在编写新的代码时,建议使用 epoll_create1,因为它提供了更好的功能和灵活性。

总结

  • select: 简单易用,但有文件描述符数量限制。
  • poll: 改进了 select 的一些限制,但仍然需要遍历文件描述符。
  • epoll: 适用于大规模并发连接,效率高,是 Linux 上的推荐选择。

选择合适的 I/O 多路复用机制取决于应用程序的需求和环境。对于高并发的网络服务器,epoll 通常是最佳选择。

  • Title: Linux环境编程与内核之文件与IO
  • Author: 韩乔落
  • Created at : 2024-01-22 10:07:36
  • Updated at : 2025-03-27 17:15:16
  • Link: https://jelasin.github.io/2024/01/22/Linux环境编程与内核之文件与IO/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
Linux环境编程与内核之文件与IO