深入理解 Linux 基础 IO —— 从文件抽象到系统调用实战

张开发
2026/4/3 12:50:55 15 分钟阅读
深入理解 Linux 基础 IO —— 从文件抽象到系统调用实战
目录一、理解“文件”1.1狭义理解1.2广义理解1.3文件操作的归类认知1.4系统角度二、回顾C文件接口2.1打开文件2.2写文件2.3读文件2.4输出信息到显示器的几种方法2.5stdinstdoutstderr2.6打开文件的方式三、系统文件I/O3.1一种传递标志位的方法3.2写文件3.3读文件3.4接口介绍3.6文件描述fd3.6.1 0 1 23.6.2文件描述符的分配规则3.6.3重定向3.6.4使用dup2系统调用3.6.5在minishell中添加重定向功能四、理解“一切皆文件”五、缓冲区5.1什么是缓冲区5.2为什么要引入缓冲区机制5.3缓冲类型5.4FILE5.5简单设计libc库一、理解“文件”1.1狭义理解文件在磁盘外设里磁盘是永久性存储介质因此文件在磁盘上的存储是永久性的磁盘是外设即是输出设备也是输⼊设备磁盘上的文件本质是对文件的所有操作都是对外设的输入和输出简称IO1.2广义理解Linux下⼀切皆文件键盘、显示器、网卡、磁盘……这些都是抽象化的过程1.3文件操作的归类认知1.4系统角度对文件的操作本质是进程对文件的操作磁盘的管理者是操作系统文件的读写本质不是通过C语⾔/C的库函数来操作的这些库函数只是为用户提供方便而是通过文件相关的系统调用接口来实现的二、回顾C文件接口2.1打开文件2.2写文件#include stdio.h #include string.h int main() { FILE *fp fopen(myfile, w); if(!fp){ printf(fopen error!\n); } const char *msg hello bit!\n; int count 5; while(count--){ fwrite(msg, strlen(msg), 1, fp); } fclose(fp); return 0; }2.3读文件#include stdio.h #include string.h int main() { FILE *fp fopen(myfile, r); if(!fp){ printf(fopen error!\n); return 1; } char buf[1024]; const char *msg hello bit!\n; while(1){ //注意返回值和参数此处有坑仔细查看man⼿册关于该函数的说明 ssize_t s fread(buf, 1, strlen(msg), fp); if(s 0){ buf[s] 0; printf(%s, buf); } if(feof(fp)){ break; } } fclose(fp); return 0; }稍作修改实现简单 cat 命令#include stdio.h #include string.h int main(int argc, char* argv[]) { if (argc ! 2) { printf(argv error!\n); return 1; } FILE *fp fopen(argv[1], r); if(!fp){ printf(fopen error!\n); return 2; } char buf[1024]; while(1){ int s fread(buf, 1, sizeof(buf), fp); if(s 0){ buf[s] 0; printf(%s, buf); } if(feof(fp)){ break; } fclose(fp); return 0; }2.4输出信息到显示器的几种方法2.5stdinstdoutstderrC默认会打开三个输入输出流分别是stdin,stdout,stderr仔细观察发现这三个流的类型都是FILE*,fopen返回值类型文件指针2.6打开文件的方式三、系统文件I/O打开文件的方式不仅仅是fopenifstream等流式语⾔层的方案其实系统才是打开文件最底层的方案。不过在学习系统文件IO之前先要了解下如何给函数传递标志位该方法在系统⽂件IO接口中会使用到3.1一种传递标志位的方法3.2写文件3.3读文件3.4接口介绍int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); pathname: 要打开或创建的目标⽂件 flags: 打开⽂件时可以传⼊多个参数选项⽤下⾯的⼀个或者多个常量进⾏“或”运算构成flags。 参数: O_RDONLY: 只读打开 O_WRONLY: 只写打开 O_RDWR : 读写打开 这三个常量必须指定⼀个且只能指定⼀个 O_CREAT : 若⽂件不存在则创建它。需要使⽤mode选项来指明新⽂件的访问权限 O_APPEND: 追加写 返回值 成功新打开的⽂件描述符 失败-13.6文件描述fd通过对open函数的学习我们知道了文件描述符就是⼀个小整数。3.6.1 0 1 23.6.2文件描述符的分配规则3.6.3重定向重定向就是更改文件描述符表指针的指向数组下标是不变的3.6.4使用dup2系统调用printf是C库当中的IO函数⼀般往stdout中输出但是stdout底层访问文件的时候找的还是fd:1, 但此时fd:1下标所表示内容已经变成了myfifile的地址不再是显示器文件的地址所以输出的任何消息都会往文件中写入进而完成清空式输出重定向。输入重定向3.6.5在minishell中添加重定向功能四、理解“一切皆文件”⾸先在windows中文件的东西它们在linux中也是文件其次⼀些在windows中不是文件的东 西比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件你可以使用访问文件的方法访问它们获得信息甚至管道也是文件将来我们要学习网络编程中的socket套接字这样的东西,使用的接口跟文件接口也是⼀致的。这样做最明显的好处是开发者仅需要使用⼀套API和开发⼯具即可调取Linux系统中绝大部分的 资源。举个简单的例子Linux中几乎所有读读文件读系统状态读PIPE的操作都可以用read 函数来进行几乎所有更改更改文件更改系统参数写PIPE的操作都可以用write 函 数来进行。 之前我们讲过当打开⼀个文件时操作系统为了管理所打开的文件都会为这个文件创建⼀个file结构体该结构体定义在 /usr/src/kernels/3.10.0- 1160.71.1.el7.x86_64/include/linux/fs.h 下以下展示了该结构部分我们关系的内容struct file { ... struct inode *f_inode; /* cached value */ const struct file_operations *f_op; ... atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数如果有多个⽂件指针指向它就会增加f_count的值。 unsigned int f_flags; // 表⽰打开⽂件的权限 fmode_t f_mode; // 设置对⽂件的访问模式,例如只读只写等。所有的标志在头⽂件fcntl.h 中定义 loff_t f_pos; // 表⽰当前读写⽂件的位置 ... } __attribute__((aligned(4))); /* lest something weird decides that 2 is OK*/值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。struct file_operations { struct module *owner; //指向拥有该模块的指针 loff_t (*llseek) (struct file *, loff_t, int); //llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -EINVAL(Invalid argument) 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个signed size 类型, 常常是⽬标平台本地的整数类型). ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负,返回值代表成功写的字节数. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); //初始化⼀个异步读 -- 可能在函数返回前不结束的读操作. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); //初始化设备上的⼀个异步写. int (*readdir) (struct file *, void *, filldir_t); //对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤. unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); //mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤返回 -ENODEV. int (*open) (struct inode *, struct file *); //打开⼀个⽂件 int (*flush) (struct file *, fl_owner_t id); //flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤; int (*release) (struct inode *, struct file *); //在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL. int (*fsync) (struct file *, struct dentry *, int datasync); //⽤⼾调⽤来刷新任何挂着的数据. int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); //lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实现它. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t*, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); };file_operation 就是把系统调用和驱动程序关联起来的关键数据结构这个结构的每⼀个成员都对应着⼀个系统调用。读取 file_operation 中相应的函数指针接着把控制权转交给函数从而完成了Linux设备驱动程序的⼯作。五、缓冲区5.1什么是缓冲区缓冲区是内存空间的⼀部分。也就是说在内存空间中预留了⼀定的存储空间这些存储空间用来缓冲输入或输出的数据这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备分为输输入缓冲区和输出缓冲区。5.2为什么要引入缓冲区机制读写文件时如果不会开辟对文件操作的缓冲区直接通过系统调用对磁盘进行操作(读、写等)那么每次对文件进行一次读写操作时都需要使用读写系统调用来处理此操作即需要执行一次系统调用执行⼀次系统调用将涉及到CPU状态的切换即从用户空间切换到内核空间实现进程上下文的切换这将损耗⼀定的CPU时间频繁的磁盘访问对程序的执行效率造成很大的影响。为了减少使用系统调用的次数提高效率我们就可以采用缓冲机制。比如我们从磁盘里取信息可以在磁盘文件进行操作时可以一次从文件中读出大量的数据到缓冲区中以后对这部分的访问就不需要再使用系统调用了等缓冲区的数据取完后再去磁盘中读取这样就可以减少磁盘的读写次数 再加上计算机对缓冲区的操作大大快于对磁盘的操作故应用缓冲区可大大提高计算机的运行速度。又比如我们使用打印机打印文档由于打印机的打印速度相对较慢我们先把文档输出到打印机相应的缓冲区打印机再自行逐步打印这时我们的CPU可以处理别的事情。可以看出缓冲区就是⼀块内存区它用在输⼊输出设备和CPU之间用来缓存数据。它使得低速的输⼊输出设备和高速的CPU能够协调工作避免低速的输入输出设备占用CPU解放出CPU使其能够⾼效率工作。5.3缓冲类型标准I/O提供了3种类型的缓冲区全缓冲区这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通 常使用全缓冲的方式访问。行缓冲区在行缓冲情况下当在输⼊和输出中遇到换行符时标准I/O库函数将会执行系统调用操作。当所操作的流涉及⼀个终端时例如标准输入和标准输出使用行缓冲方式。因为标准 I/O库每行的缓冲区长度是固定的所以只要填满了缓冲区即使还没有遇到换行符也会执行I/O系统调用操作默认行缓冲区的大小为1024。无缓冲区无缓冲区是指标准I/O库不对字符进行缓存直接调用系统调用。标准出错流stderr通 常是不带缓冲区的这使得出错信息能够尽快地显示出来。5.4FILE因为IO相关函数与系统调用接口对应并且库函数封装系统调用所以本质上访问文件都是通 过fd访问的。所以C库当中的FILE结构体内部必定封装了fd。对进程实现重定向5.5简单设计libc库//my_stdio.h #pragma once #define SIZE 1024 #define FLUSH_NONE 0 #define FLUSH_LINE 1 #define FLUSH_FULL 2 struct IO_FILE { int flag; // 刷新⽅式 int fileno; // ⽂件描述符 char outbuffer[SIZE]; int cap; int size; // TODO }; typedef struct IO_FILE mFILE; mFILE *mfopen(const char *filename, const char *mode); int mfwrite(const void *ptr, int num, mFILE *stream); void mfflush(mFILE *stream); void mfclose(mFILE *stream);//my_stdio.c #include my_stdio.h #include string.h #include stdlib.h #include sys/stat.h #include sys/types.h #include fcntl.h #include unistd.h mFILE *mfopen(const char *filename, const char *mode) { int fd -1; if(strcmp(mode, r) 0) { fd open(filename, O_RDONLY); } else if(strcmp(mode, w) 0) { fd open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666); } else if(strcmp(mode, a) 0) { fd open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666); } if(fd 0) return NULL; mFILE *mf (mFILE*)malloc(sizeof(mFILE)); if(!mf) { close(fd); return NULL; } mf-fileno fd; mf-flag FLUSH_LINE; mf-size 0; mf-cap SIZE; return mf; } void mfflush(mFILE *stream) { if(stream-size 0) { // 写到内核⽂件的⽂件缓冲区中! write(stream-fileno, stream-outbuffer, stream-size); // 刷新到外设 fsync(stream-fileno); stream-size 0; } } int mfwrite(const void *ptr, int num, mFILE *stream) { // 1. 拷⻉ memcpy(stream-outbufferstream-size, ptr, num); stream-size num; // 2. 检测是否要刷新 if(stream-flag FLUSH_LINE stream-size 0 stream-outbuffer[stream-size-1] \n) { mfflush(stream); } return num; } void mfclose(mFILE *stream) { if(stream-size 0) { mfflush(stream); } close(stream-fileno); }//main.c #include my_stdio.h #include stdio.h #include string.h #include unistd.h int main() { mFILE *fp mfopen(./log.txt, a); if(fp NULL) { return 1; } int cnt 10; while(cnt) { printf(write %d\n, cnt); char buffer[64]; snprintf(buffer, sizeof(buffer),hello message, number is : %d, cnt); cnt--; mfwrite(buffer, strlen(buffer), fp); mfflush(fp); sleep(1); } mfclose(fp); }

更多文章