内核文件表 文件
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 ; unsigned long *close_on_exec; unsigned long *open_fds; unsigned long *full_fds_bits; struct rcu_head rcu ; }; struct files_struct { atomic_t count; struct fdtable __rcu *fdt ; struct fdtable fdtab ; spinlock_t file_lock ____cacheline_aligned_in_smp; int next_fd; struct embedded_fd_set close_on_exec_init ; struct embedded_fd_set open_fds_init ; struct file __rcu * fd_array [NR_OPEN_DEFAULT ]; };
下面看看files_struct
是如何使用默认的fdtab
和fd_array
的,init
是Linux
的第一个进程,它的文件表是一个全局变量,代码如下:
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.fdt
和init_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 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_struct
,fdtable
和files
的关系如下图所示。
除了文件名以外的所有文件信息,都存在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; gid_t i_gid; kdev_t i_rdev; loff_t i_size; struct timespec i_atime ; struct timespec i_mtime ; struct timespec i_ctime ; 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 ; 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 ) { 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_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, }; if (how.flags & O_PATH) how.flags &= O_PATH_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 ; spin_lock(&files->file_lock); repeat: fdt = files_fdtable(files); fd = start; if (fd < files->next_fd) fd = files->next_fd; if (fd < fdt->max_fds) fd = find_next_zero_bit(fdt->open_fds->fds_bits, fdt->max_fds, fd); error = expand_files(files, fd); if (error < 0 ) goto out; if (error) goto repeat; if (start <= files->next_fd) files->next_fd = fd + 1 ; FD_SET(fd, fdt->open_fds); if (flags & O_CLOEXEC) FD_SET(fd, fdt->close_on_exec); else FD_CLR(fd, fdt->close_on_exec); error = fd; #if 1 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 ); 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) ;
参数说明
**pathname
**:
这是指向文件路径的指针,表示要打开的文件的路径。可以是绝对路径或相对路径。
**flags
**:
这是一个整数,用于指定打开文件时的选项。可以是以下标志的组合:
**O_RDONLY
**:以只读方式打开文件。
**O_WRONLY
**:以只写方式打开文件。
**O_RDWR
**:以读写方式打开文件。
**O_CREAT
**:如果文件不存在则创建文件。需要指定 mode
参数。
**O_EXCL
**:与 O_CREAT
一起使用,如果文件存在则返回错误。
**O_TRUNC
**:如果文件存在并且以写入方式打开,则将其截断为零长度。
**O_APPEND
**:以追加模式打开文件。写入操作将添加到文件的末尾。
**O_NONBLOCK
**:以非阻塞模式打开文件。
**O_SYNC
**:以同步模式打开文件,保证写入操作立即写入底层存储设备。
**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); if (fd >= fdt->max_fds) goto out_unlock; filp = fdt->fd[fd]; if (!filp) goto out_unlock; rcu_assign_pointer(fdt->fd[fd], NULL ); FD_CLR(fd, fdt->close_on_exec); __put_unused_fd(files, fd); spin_unlock(&files->file_lock); retval = filp_close(filp, files); 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_CLR(fd, fdt->open_fds); 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, .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 = 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 = 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
**:
这是一个整数,表示要关闭的文件描述符。该描述符应该是先前通过 open
、socket
、dup
等系统调用获得的。
返回值
成功时,close
返回 0
。
失败时,返回 -1
,并设置 errno
来指示错误类型。
错误处理 常见的错误包括:
**EBADF
**:提供的文件描述符 fd
无效,可能是因为它从未被打开或已经被关闭。
**EINTR
**:close
调用被信号中断。此时通常需要重新尝试调用 close
。
**EIO
**:发生了输入/输出错误。
使用注意事项
资源释放 :close
函数的主要作用是释放与文件描述符关联的资源。未关闭的文件描述符可能导致资源泄漏,最终导致系统无法打开更多的文件。
多次关闭 :不要多次关闭同一个文件描述符。多次关闭可能会导致未定义行为,特别是在该描述符已经被其他资源重新使用的情况下。
文件缓冲区 :在关闭文件描述符之前,确保所有数据都已写入文件。如果使用标准 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; file = fget_light(fd, &fput_needed); if (!file) goto bad; retval = -EINVAL; if (origin <= SEEK_MAX) { loff_t res = vfs_llseek(file, offset, origin); retval = res; if (res != (loff_t )retval) retval = -EOVERFLOW; } 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 ); 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: if (offset == 0 ) { retval = file->f_pos; goto out; } offset += file->f_pos; break ; case SEEK_DATA: if (offset >= inode->i_size) { retval = -ENXIO; goto out; } break ; case SEEK_HOLE: if (offset >= inode->i_size) { retval = -ENXIO; goto out; } offset = inode->i_size; break ; } retval = -EINVAL; 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
**:计算出的文件偏移量超出了可表示的范围。
使用注意事项
文件类型 :lseek
仅适用于支持随机访问的文件类型。对于管道、FIFO 或套接字,lseek
会失败。
扩展文件 :如果通过 lseek
将偏移量设置到文件末尾之后的位置,并随后进行写入操作,文件将被扩展,中间的空隙通常被填充为零。
同步问题 :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); } 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; file = fget_light(fd, &fput_needed); if (file) { loff_t pos = file_pos_read(file); ret = vfs_read(file, buf, count, &pos); file_pos_write(file, pos); 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; if (unlikely(!access_ok(VERIFY_WRITE, buf, count))) return -EFAULT; ret = rw_verify_area(READ, file, pos, count); if (ret >= 0 ) { count = ret; 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 操作系统中,read
和 pread
函数用于从文件中读取数据。它们的主要区别在于 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' ; 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); } ssize_t bytesRead = pread(fd, buffer, 50 , 10 ); if (bytesRead == -1 ) { perror("pread" ); close(fd); exit (EXIT_FAILURE); } buffer[bytesRead] = '\0' ; 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 = fget_light(fd, &fput_needed); if (file) { loff_t pos = file_pos_read(file); ret = vfs_write(file, buf, count, &pos); file_pos_write(file, pos); 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; ret = rw_verify_area(WRITE, file, pos, count); if (ret >= 0 ) { count = ret; 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 操作系统中,write
和 pwrite
函数用于向文件中写入数据。它们的主要区别在于 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); } 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; struct file *file = fget_raw(fildes); if (file) { ret = get_unused_fd(); if (ret >= 0 ) { 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 ); 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) { if (unlikely(newfd == oldfd)) { struct files_struct *files = current->files; int retval = oldfd; rcu_read_lock(); if (!fcheck_files(files, oldfd)) retval = -EBADF; rcu_read_unlock(); return retval; } 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 ; if ((flags & ~O_CLOEXEC) != 0 ) return -EINVAL; if (unlikely(oldfd == newfd)) return -EINVAL; spin_lock(&files->file_lock); err = expand_files(files, newfd); 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); tofree = fdt->fd[newfd]; if (!tofree && FD_ISSET(newfd, fdt->open_fds)) goto out_unlock; get_file(file); rcu_assign_pointer(fdt->fd[newfd], file); FD_SET(newfd, fdt->open_fds); if (flags & O_CLOEXEC) FD_SET(newfd, fdt->close_on_exec); else FD_CLR(newfd, fdt->close_on_exec); spin_unlock(&files->file_lock); 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) ;
参数说明
返回值
返回新的文件描述符。
返回 -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) ;
参数说明
返回值
返回 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); } 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
函数dup3
是 dup2
的扩展版本,允许在复制文件描述符时指定额外的标志。
函数原型 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); } 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(0 ); sync_filesystems(1 ); 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){ 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); 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); 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 = fget(fd); if (file) { 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; err = filemap_write_and_wait_range(inode->i_mapping, start, end); if (err) return err; mutex_lock(&inode->i_mutex); ret = sync_mapping_buffers(inode->i_mapping); if (!(inode->i_state & I_DIRTY)) goto out; if (datasync && !(inode->i_state & I_DIRTY_DATASYNC)) goto out; 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 操作系统中,sync
、fsync
和 fdatasync
函数用于将数据从内存写入到存储设备,以确保数据的持久性。它们在数据完整性和文件系统一致性方面发挥着重要作用。以下是对这三个函数的详细解析:
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 () { 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); } if (write(fd, "Hello, fsync!\n" , 14 ) == -1 ) { perror("write" ); close(fd); exit (EXIT_FAILURE); } if (fsync(fd) == -1 ) { perror("fsync" ); close(fd); exit (EXIT_FAILURE); } close(fd); return 0 ; }
fdatasync
函数fdatasync
是 fsync
的一个变种,功能类似,但只同步文件数据和必要的元数据(如文件大小),而不包括其他元数据(如文件的最后访问时间)。
函数原型 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); } if (write(fd, "Hello, fdatasync!\n" , 18 ) == -1 ) { perror("write" ); close(fd); exit (EXIT_FAILURE); } 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; ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; off_t st_size; blksize_t st_blksize; blkcnt_t st_blocks; time_t st_atime; time_t st_mtime; time_t st_ctime; };
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; error = vfs_stat(filename, &stat); if (error) return error; 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; retval = security_inode_getattr(mnt, dentry); if (retval) return retval; if (inode->i_op->getattr) return inode->i_op->getattr(mnt, dentry, stat); 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; [...] 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_t fsuid = current_fsuid(); if (!(dir->i_mode & S_ISVTX)) return 0 ; if (current_user_ns() != inode_userns(inode)) goto other_userns; if (inode->i_uid == fsuid) return 0 ; 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; ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; off_t st_size; blksize_t st_blksize; blkcnt_t st_blocks; 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) ;S_ISREG(m) S_ISDIR(m) S_ISCHR(m) S_ISBLK(m) S_ISFIFO(m) S_ISLNK(m) S_ISSOCK(m) S_IFMT 0170000 S_IFSOCK 0140000 S_IFLNK 0120000 S_IFREG 0100000 S_IFBLK 0060000 S_IFDIR 0040000 S_IFCHR 0020000 S_IFIFO 0010000 S_ISUID 0004000 S_ISGID 0002000 S_ISVTX 0001000 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; error = get_write_access(inode); if (error) goto mnt_drop_write_and_out; 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; } ret = should_remove_suid(dentry); if (ret) newattrs.ia_valid |= ret | ATTR_FORCE; 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 ; if (truncate(filename, new_length) == 0 ) { printf ("Successfully truncated the file to %ld bytes.\n" , new_length); } else { perror("truncate" ); } return 0 ; }
注意事项
权限要求 :调用进程必须对文件具有写权限才能使用 truncate
函数。
文件类型 :truncate
只能用于常规文件,不能用于其他文件类型(例如目录或设备文件)。
文件延长 :如果文件被延长,新增的部分通常会填充零字节,但这依赖于文件系统的实现。
错误处理 :如果 truncate
失败,可以通过检查 errno
来获取错误的具体原因。例如,可能的错误包括没有足够的权限 (EACCES
)、文件不存在 (ENOENT
)、或路径名无效 (EINVAL
)。
兼容性 :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) { long ret = do_sys_ftruncate(fd, length, 1 ); 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 = fget(fd); if (!file) goto out; 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; 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 ; 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 ; }
注意事项
文件描述符 :ftruncate
使用文件描述符来指定文件,这意味着文件必须已经被打开,并且通常需要以写模式打开(例如 O_RDWR
或 O_WRONLY
)。
权限要求 :调用进程需要对文件具有写权限。
文件类型 :ftruncate
只能用于常规文件,不能用于其他文件类型(例如目录或设备文件)。
文件延长 :如果文件被延长,新增的部分通常会填充零字节,但这取决于文件系统的实现。
错误处理 :如果 ftruncate
失败,可以通过检查 errno
来获取错误的具体原因。例如,可能的错误包括没有足够的权限 (EACCES
)、无效的文件描述符 (EBADF
)、或无效的长度 (EINVAL
)。
兼容性 :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 ;extern struct _IO_FILE *stdin ; extern struct _IO_FILE *stdout ; extern struct _IO_FILE *stderr ; #ifdef __STDC__ #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) ;
参数解析
filename :
类型:const char *
说明:要打开的文件的路径,可以是相对路径或绝对路径。如果文件在当前工作目录下,可以只用文件名;如果在其他目录下,需要提供完整的路径。
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 ; 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 : 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 ; } 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); 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); 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; 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有两个返回值:
并且由于文件流的错误只是使用一个标志位_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, ); _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 ; (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) #if ATTEMPTS_MIN < TMP_MAX unsigned int attempts = TMP_MAX; #else unsigned int attempts = ATTEMPTS_MIN; #endif len = strlen (tmpl); if (len < 6 + suffixlen || memcmp (&tmpl[len - 6 - suffixlen], "XXXXXX" , 6 )) { __set_errno (EINVAL); return -1 ; } 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 value += random_time_bits ^ __getpid (); for (count = 0 ; count < attempts; value += 7777 , ++count) { uint64_t v = value; 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: fd = __open (tmpl, (flags & ~O_ACCMODE) | O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); break ; } if (fd >= 0 ) { __set_errno (save_errno); return fd; } else if (errno != EEXIST) { return -1 ; } } __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 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" ; if ((fd=mkstemp(temp_file))==-1 ) { printf ("Creat temp file faile./n" ); exit (1 ); } unlink(temp_file); close(fd); }
在需要使用临时文件时,不推荐使用tmpnam,而要用tmpfile和mkstemp。前者的局限在于不能指定路径,并且在文件名称冲突时会返回失败。后者可以由调用者来指定路径,并且在文件名称冲突时,会自动重新生成并重试。
除了上面介绍的几种方法,Linux环境还提供了这些接口的一些变种:tempnam、mkostemp、mkstemps等,分别对其原始形态进行了扩展,详细区别可以直接查看Linux手册。
目录/文件操作 在C语言中,操作目录和文件的过程中,会接触到一些相关的结构体,主要有以下几个关键的结构体:
DIR
结构体
struct dirent
结构体
struct stat
结构体
struct statvfs
结构体
下面是这些结构体的一些详细说明:
DIR
结构体DIR
结构体是用来表示目录流的,它的具体定义在 POSIX 标准中未公开,通常是在使用 opendir
打开一个目录后,得到一个 DIR *
类型的指针,通过该指针可进行目录流的操作。
1 2 3 4 5 6 #include <dirent.h> 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; unsigned char d_type; char d_name[256 ]; };
struct stat
结构体struct stat
结构体用于存储文件的状态信息,包括文件类型、权限、大小等。常与 stat
、fstat
、lstat
等函数一起使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <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; gid_t st_gid; dev_t st_rdev; off_t st_size; blksize_t st_blksize; 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 ; }
这个示例演示了如何使用 opendir
、readdir
和 stat
函数来读取当前目录中的文件和目录信息,并打印出每个条目的名称、类型、大小、权限和最后修改时间。
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" ); }
注意事项
权限变化 :access
检查的是当前进程的真实用户 ID 和组 ID 的权限,而不是有效用户 ID 和组 ID。这在使用 setuid
程序时尤其重要。
安全性 :使用 access
进行权限检查并不能保证后续操作的安全性,因为在检查和实际操作之间,文件的权限可能会发生变化。这种情况被称为 TOCTOU(Time Of Check to Time Of Use)漏洞。因此,access
不应该用于安全性检查,而只是用于提高用户体验。
符号链接 :如果 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) ;
参数 :
返回值 :
成功返回目录流的指针(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) ;
参数 :
返回值 :
成功返回 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) ;
参数 :
返回值 :
成功时返回 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; ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; off_t st_size; blksize_t st_blksize; blkcnt_t st_blocks; 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 ; }
注意事项
跨文件系统 :在某些文件系统实现中,rename
函数不允许跨不同的文件系统。如果尝试在不同文件系统之间重命名文件,函数可能会失败,并且 errno
将被设置为 EXDEV
。
目标文件已存在 :如果目标路径已经存在,并且目标不是目录或目标目录为空,rename
会覆盖目标。具体行为可能根据操作系统有所不同,但这通常是典型的实现。
文件权限 :如果对操作的文件或目录没有适当的权限(如读/写权限),rename
操作会失败,errno
将被设置为 EACCES
或 EPERM
。
remove()
函数remove
是 C 标准库提供的一个函数,用于删除文件或空目录。它是一个相对简单的函数,主要用来处理基本的文件操作。这个函数声明在 <stdio.h>
头文件中。
函数原型
1 int remove (const char *filename) ;
参数
filename
: 一个指向要删除的文件或空目录的名称的字符串指针。
返回值
成功:返回 0
。
失败:返回 -1
,并设置 errno
以指示具体错误。
用法
删除文件 :可以直接使用 remove
函数删除文件,只需要提供文件的路径。
删除空目录 :在某些实现中(如 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()
函数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
: 没有删除文件的权限。
EPERM
或 EISDIR
: 试图使用 unlink
删除一个目录。
EBUSY
: 文件在某些情况下可能被挂载或作为交换文件正在使用。
使用注意
数据丢失 :一旦调用 unlink
删除文件名,该文件名就立即无效,数据可能不可恢复。
权限 :确保有足够的权限来删除文件。
文件句柄 :unlink
后,文件仍然可以通过已经打开的文件句柄读写,直到文件所有句柄都关闭后,文件系统才释放文件的资源。
unlink
是一个底层的文件操作函数,通常用于需要直接控制文件删除特性的应用程序中。对于一般的文件删除操作,通常使用更简单、更通用的库函数,例如 remove
。
readlink()
函数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
: pathname
或 buf
指向了无效的内存地址。
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
)进行健壮性检查。
ftell
和rewind
是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, ... ) ;
fd
: 这是要操作的文件描述符。
cmd
: 指定要执行的操作,具体操作取决于后面的可选参数。
arg
: 可选参数,根据cmd
的不同可以是一个整数或者指向某种结构的指针。
常用命令 (cmd
)
F_DUPFD : 复制文件描述符。
arg
: 这是新文件描述符应该使用的最小值。
返回值是新的文件描述符。
F_GETFD : 获取文件描述符标志。
arg
不使用,仅作占位。
返回当前文件描述符标志。
F_SETFD : 设置文件描述符标志。
F_GETFL : 获取文件状态标志。
这可以返回文件打开时的状态标志,如阻塞/非阻塞、同步/异步等。
F_SETFL : 设置文件状态标志。
arg
: 新的状态标志。
一些常用标志包括 O_NONBLOCK
, O_APPEND
等。
F_GETOWN : 获取文件的“拥有者”(用于异步I/O通知)。
arg
不使用,仅作占位。
返回拥有者的进程ID或进程组ID。
F_SETOWN : 设置文件的“拥有者”。
F_GETLK , F_SETLK , F_SETLKW : 文件锁管理。
用来获取、设置和等待文件锁。
arg
: 传递struct flock
指针,用于描述锁的属性。
文件锁结构struct flock
当使用F_GETLK
、F_SETLK
和F_SETLKW
时,你需要提供一个struct flock
结构来描述锁的细节:
1 2 3 4 5 6 7 struct flock { short l_type; short l_whence; off_t l_start; off_t l_len; pid_t l_pid; };
返回值
成功时,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 多路复用的系统调用,主要包括 select
、poll
和 epoll
。
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
。
使用步骤
初始化 fd_set
结构。
使用 FD_SET
宏将文件描述符添加到 fd_set
。
调用 select
。
使用 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_CLR
、FD_ISSET
、FD_SET
和 FD_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 ) ;
功能 : 清空文件描述符集合。
参数 :
用法 : 在使用 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 ; 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
poll
是 select
的改进版本,克服了一些限制,如文件描述符数量限制。
函数原型 1 2 3 #include <poll.h> int poll (struct pollfd *fds, nfds_t nfds, int timeout) ;
参数
fds
: 指向 pollfd
结构数组的指针。
nfds
: 数组中的元素数量。
timeout
: 超时时间,以毫秒为单位,-1
表示无限等待。
返回值
成功时返回就绪的文件描述符数量。
失败时返回 -1
,并设置 errno
。
使用步骤
初始化 pollfd
结构数组。
设置每个文件描述符的事件掩码。
调用 poll
。
检查 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 实例。
使用 epoll_ctl
添加、修改或删除文件描述符。
使用 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_create
和 epoll_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_CLOEXEC
。EPOLL_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); } close(epfd); return 0 ; }
总结
epoll_create
: 旧的接口,size
参数在现代内核中被忽略,但仍然需要提供。
epoll_create1
: 新的接口,支持 EPOLL_CLOEXEC
标志,推荐在现代应用中使用。
在编写新的代码时,建议使用 epoll_create1
,因为它提供了更好的功能和灵活性。
总结
select
: 简单易用,但有文件描述符数量限制。
poll
: 改进了 select
的一些限制,但仍然需要遍历文件描述符。
epoll
: 适用于大规模并发连接,效率高,是 Linux 上的推荐选择。
选择合适的 I/O 多路复用机制取决于应用程序的需求和环境。对于高并发的网络服务器,epoll
通常是最佳选择。