Linux系统文件IO
目录1 理解一切皆文件2 位图传递标志位3 ofen4 write5 close6 浅剖内核6.1 关键数据结构6.2 文件描述符6.3 重定向6.3.1 dup2系统调用1 理解一切皆文件在Linux系统中“一切皆文件”是一个核心设计哲学它抽象了系统资源的访问方式使得几乎所有硬件设备、进程、网络连接等都可以通过统一的文件接口进行操作这样做最明显的好处是开发者仅需要使⽤⼀套 API 和开发⼯具即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦Linux 中⼏乎所有读读⽂件读系统状态读PIPE的操作都可以⽤read 函数来进⾏⼏乎所有更改更改⽂件更改系统参数写 PIPE的操作都可以⽤ write 函数来进⾏当打开⼀个⽂件时操作系统为了管理所打开的⽂件都会为这个⽂件创建⼀个 file 结构体// linux/fs.h struct file { struct path f_path; // 文件路径含dentry和vfsmount struct inode *f_inode; // 对应的inode文件元数据 const struct file_operations *f_op; // 文件操作函数如read/write atomic_long_t f_count; // 引用计数 unsigned int f_flags; // 打开标志如O_RDONLY、O_NONBLOCK fmode_t f_mode; // 访问模式FMODE_READ / FMODE_WRITE loff_t f_pos; // 当前文件偏移量用于read/write struct fown_struct f_owner; // 异步I/O事件通知如SIGIO // ... };值得关注的是const struct file_operations *f_op 成员是一个指向文件操作函数表的指针。这个函数表定义了针对该文件的所有操作如读、写、控制等的具体实现// linux/fs.h struct file_operations { struct module *owner; // 调整文件读写偏移量lseek系统调用。 // 参数文件指针、目标偏移量、起始位置SEEK_SET等。 loff_t (*llseek) (struct file *, loff_t, int); // 同步读取文件内容read系统调用。 /// 参数文件指针、用户态缓冲区、读取长度、当前偏移量指针。 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 同步写入文件内容write系统调用。 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 打开文件时调用非必需。 // 参数关联的inode、新创建的file结构体。 int (*open) (struct inode *, struct file *); // 文件关闭时清理资源close系统调用 int (*release) (struct inode *, struct file *); // 其他成员省略... };file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构这个结构的每⼀个成员都对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针接着把控制权转交给函数从⽽完成了Linux设备驱动程序的⼯作。上图中的外设每个设备都可以有⾃⼰的read、write但⼀定是对应着不同的操作⽅法但通过struct file 下 file_operation 中的各种函数回调让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源这便是“linux下⼀切皆⽂件”的核⼼理解。2 位图传递标志位在学习系统⽂件IO之前先要了解下如何给函数传递标志位该⽅法在系统⽂件IO接⼝中会使⽤到1示例#include stdio.h // 位图操作 #define SIGN1 (10) #define SIGN2 (12) #define SIGN3 (13) #define SIGN4 (14) #define SIGN5 (15) void test(int flag) { if (flag SIGN1) printf(sign1 ); if (flag SIGN2) printf(sign2 ); if (flag SIGN3) printf(sign3 ); if (flag SIGN4) printf(sign4 ); if (flag SIGN5) printf(sign5 ); } int main() { test(SIGN1); printf(\n);// sign1 test(SIGN1 | SIGN2); // sign1 sign2 printf(\n); test(SIGN1 | SIGN2 | SIGN3); // sign1 sign2 sign3 printf(\n); test(SIGN1 | SIGN2 | SIGN3 | SIGN4); // sign1 sign2 sign3 sign4 printf(\n); test(SIGN1 | SIGN2 | SIGN3 | SIGN4 | SIGN5); // sign1 sign2 sign3 sign4 sign5 printf(\n); return 0; }3 ofen1功能用于打开或创建文件/设备的底层系统调用函数2函数原型#include fcntl.h #include sys/types.h #include sys/stat.h int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);3函数参数pathname要打开/创建的文件路径绝对或相对路径。flags指定文件的访问模式和操作选项通过位掩码组合用 | 连接基本访问模式必选其一1.O_RDONLY只读。2. O_WRONLY只写。3. O_RDWR读写。常用可选标志1. O_CREAT文件不存在时创建需配合 mode 参数。2. O_TRUNC若文件存在且为普通文件将其长度截断为0。3. O_APPEND追加写入每次写操作前移动指针到文件末尾。4. O_EXCL与 O_CREAT 联用若文件已存在则返回错误用于原子性检查。5. O_NONBLOCK非阻塞模式对设备文件或管道有效。6. O_SYNC同步I/O写操作等待数据物理写入磁盘。mode可选创建文件时指定权限如0644受当前进程的umask影响。仅当flags包含O_CREAT时有效。4返回值成功返回文件描述符int类型非负整数后续操作如read/write通过该描述符进行。失败返回-1并设置errno表示错误原因如ENOENT文件不存在、EACCES权限不足等。5示例#include fcntl.h #include sys/types.h #include sys/wait.h int main() { // 打开现有文件只读 int fd open(/etc/passwd, O_RDONLY); // 创建新文件读写权限若存在则截断 int fd open(test.txt, O_RDWR | O_CREAT | O_TRUNC, 0644); // 追加写入文件不存在则创建 int fd open(log.txt, O_WRONLY | O_CREAT | O_APPEND, 0644); return 0; }4 write1功能用于将数据写入文件描述符对应的文件或设备。2函数原型#include unistd.h ssize_t write(int fd, const void *buf, size_t count);3函数参数fd文件描述符由open、socket等调用返回buf包含待写入数据的缓冲区指针count请求写入的字节数4返回值成功返回实际写入的字节数可能小于请求的count失败返回-1并设置errno5 close1功能用于关闭文件描述符file descriptor的系统调用函数2函数原型#include unistd.h int close(int fd);3函数参数fd要关闭的文件描述符由open()、socket()、pipe()等函数返回。4返回值成功返回0。失败返回-1并设置errno表示错误原因如EBADF表示无效的文件描述符。6 浅剖内核在Linux内核中每个进程的进程控制块PCB, Process Control Block由struct task_struct表示定义在Linux/sched.h其中管理文件描述符的核心结构是struct files_struct它存储了进程打开的所有文件信息。6.1 关键数据结构1struct task_struct进程描述符// linux/sched.h struct task_struct { // ... struct files_struct *files; // 指向进程的文件描述符表 // ... };files指向进程的文件描述符表struct files_struct管理所有打开的文件。2struct files_struct文件描述符表// linux/fdtable.h struct files_struct { atomic_t count; // 引用计数 struct fdtable __rcu *fdt; // 动态管理的文件描述符表 struct fdtable fdtab; // 默认的文件描述符表初始分配 unsigned int next_fd; // 下一个可用的文件描述符 struct file *fd_array[NR_OPEN_DEFAULT]; // 默认的文件描述符数组大小通常为64 };count引用计数用于共享文件描述符表如fork()后的子进程。fdt/fdtab动态扩展的文件描述符表struct fdtable。fd_array默认的静态数组存储struct file*文件对象指针初始大小为NR_OPEN_DEFAULT通常64。next_fd记录下一个可分配的文件描述符通常从0开始分配但0/1/2默认是stdin/stdout/stderr。3struct files文件对象// linux/fs.h struct file { struct path f_path; // 文件路径含dentry和vfsmount struct inode *f_inode; // 对应的inode文件元数据 const struct file_operations *f_op; // 文件操作函数如read/write atomic_long_t f_count; // 引用计数 unsigned int f_flags; // 打开标志如O_RDONLY、O_NONBLOCK fmode_t f_mode; // 访问模式FMODE_READ / FMODE_WRITE loff_t f_pos; // 当前文件偏移量用于read/write struct fown_struct f_owner; // 异步I/O事件通知如SIGIO // ... };f_path包含文件的目录项dentry和挂载点vfsmount。f_inode指向文件的inode存储文件元数据如权限、大小等。f_op文件操作函数表如.read read()、.write write()。f_flags文件的打开标志O_RDONLY、O_NONBLOCK等。f_pos当前读写位置lseek()修改此值。6.2 文件描述符文件描述符的本质就是进程的文件描述符表的下标文件描述符的下标从3开始1文件描述符表的 0 1 2 下标Linux进程默认情况下会有3个缺省打开的⽂件描述符分别是标准输⼊0键盘 标准输出1屏幕 标准错误2屏幕#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include string.h #include unistd.h int main() { char buf[1024]; int s read(0, buf, sizeof(buf)); if (s 0) { buf[s] 0; write(1, buf, strlen(buf)); write(2, buf, strlen(buf)); } return 0; }2⽂件描述符的分配规则⽂件描述符的分配规则在files_struct数组当中找到当前没有被使⽤的最⼩的⼀个下标作为新的⽂件描述符#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include string.h #include unistd.h int main() { // 关闭输出流 close(0); // 创建新文件读写权限若存在则截断 int fd open(test.txt, O_RDWR | O_CREAT | O_TRUNC, 0644); // 在files_struct数组当中找到当前没有被使⽤的最⼩的⼀个下标作为新的⽂件描述符 printf(fd:%d\n, fd); // 0 close(fd); return 0; }6.3 重定向在Linux中重定向的本质是通过修改FD指向的 struct file改变数据流的来源或目标。这一过程通过操作文件描述符表files_struct实现核心依赖系统调用如open()、dup2()、close()等。6.3.1 dup2系统调用1功能用于复制文件描述符的系统调用函数2函数原型#include unistd.h int dup2(int oldfd, int newfd);3函数参数oldfd源文件描述符必须是一个已经打开的文件描述符newfd目标文件描述符若是已打开则先自动关闭若等于 oldfd 则直接返回4返回值成功返回newfd与oldfd指向同一个文件对象。失败返回-1并设置errno常见错误见下文。5核心功能复制文件描述符使newfd指向oldfd相同的文件对象struct file。如果newfd已打开dup2()会先自动关闭它类似隐式调用close(newfd)。原子性操作整个复制过程是原子的避免了竞态条件如多线程下先close(newfd)再dup(oldfd)可能被其他线程干扰。文件描述符共享oldfd和newfd共享相同的文件偏移量、访问模式等属性因为它们指向同一个struct file。#include unistd.h #include fcntl.h #include stdio.h #include sys/types.h #include sys/wait.h int main() { pid_t pid fork(); if (pid 0) { // 子进程 int fd open(test.txt, O_WRONLY | O_CREAT | O_TRUNC, 0644); // 将进程的文件描述表的 1 号文件stdout——屏幕文件替换为进程创建的文件 test.txt dup2(fd, 1); // 重定向stdout到文件 close(fd); // 执行ls -l // ls命令默认只会向进程的文件描述表的 1 号文件进行输出 execlp(ls, ls, -l, NULL); } // 父进程 waitpid(NULL); // 等待子进程结束 return 0; }问题一子进程如何看待父进程打开的文件问题二程序替换后不会创建新进程会影响历史打开的文件吗