Linux文件操作:系统调用与文件描述符

张开发
2026/4/21 1:20:28 15 分钟阅读

分享文章

Linux文件操作:系统调用与文件描述符
Linux 系统调用的文件操作在 Linux 系统中用户程序操作文件必须通过系统调用内核提供的接口流程是打开文件open→读/写文件read/write→关闭文件close1.文件描述符文件描述符是用户进程的文件描述符表中的一个索引非负整数每个索引对应内核中一个 “打开文件对象”包含文件的路径、权限、当前读写位置等状态。默认分配规则进程启动时会自动打开 3 个默认文件描述符0标准输入stdin1标准输出stdout2标准错误stderr分配顺序新打开文件时内核会分配当前未使用的最小非负整数作为文件描述符比如关闭0后新打开的文件会用0。1.1 文件描述符的核心特性进程私有性每个进程有独立的文件描述符表因此不同进程的同一个数字比如3可能对应完全不同的文件。例进程 A 的fd3对应a.txt进程 B 的fd3可能对应b.txt。共享性fork场景fork 后子进程继承fork创建的子进程会复制父进程的文件描述符表此时父、子进程的同一个fd指向同一个内核打开文件对象共享文件偏移量、打开状态。例父进程打开a.txt得到fd3fork后子进程的fd3也对应同一个打开文件对象父进程写了 10 字节后子进程的文件偏移量会自动跳到 10 字节处。资源有限性每个进程的文件描述符数量有上限默认通常是 1024可以通过ulimit -n查看 / 修改需要权限。若打开文件过多导致超出上限open会返回-1错误码为EMFILE。2.核心函数调用2.1.open打开 / 创建文件作用是获取文件描述符有两种格式// 格式1打开已存在的文件intopen(constchar*pathname,intflags);// 格式2创建新文件同时设置权限intopen(constchar*pathname,intflags,mode_tmode);参数说明pathname文件的路径 名称比如/home/test.txt。flags: 打开方式的 “标志位”可组合用|连接常用值需要引用 #include fcntl.h O_WRONLY只写打开O_RDONLY只读打开O_RDWR读写打开O_CREAT文件不存在时自动创建O_APPEND写数据时追加到文件末尾不覆盖原有内容O_TRUNC打开文件时清空原有内容重新写入mode仅当带O_CREAT时需要设置文件的访问权限比如0600表示 “所有者可读可写其他用户无权限”。返回值成功返回文件描述符非负整数失败返回-1。2.2.read从文件读数据作用是把文件内容读到内存缓冲区ssize_tread(intfd,void*buf,size_tcount);参数说明fdopen返回的文件描述符。buf内存缓冲区用来存读出来的数据。count计划读取的字节数。返回值成功返回实际读到的字节数可能小于count比如文件末尾失败返回-1读到文件末尾返回0。2.3.write向文件写数据作用是把内存缓冲区的数据写入文件ssize_twrite(intfd,constvoid*buf,size_tcount);参数说明fd文件描述符。buf要写入的数据所在的内存缓冲区。count计划写入的字节数。返回值成功返回实际写入的字节数一般和count一致磁盘满时可能更小失败返回-1。2.4.close关闭文件作用是释放文件描述符回收资源intclose(intfd);参数说明fd是要关闭的文件描述符。返回值成功返回0失败返回-1。3.代码示例#includestdio.h#includestdlib.h#includeunistd.h#includestring.h#includeassert.h#includefcntl.h//分析程序执行的打印结果intmain(){charbuff[128]{0};//myfile.txt 自己创建,并写入内容为“abcdef”intfdopen(myfile.txt,O_RDONLY);pid_tpidfork();assert(pid!-1);if(pid0){read(fd,buff,1);// 子进程第一次读读取1个字节到buff// 读的位置由「共享的文件偏移量」决定看父进程是否先操作printf(child buff%s\n,buff);sleep(1);// 子进程休眠1秒read(fd,buff,1);// 子进程第二次读继续读下1个字节偏移量已更新printf(child buff%s\n,buff);}else{read(fd,buff,1);// 父进程第一次读读取1个字节到buffprintf(parent buff%s\n,buff);sleep(1);// 父进程休眠1秒read(fd,buff,1);// 父进程第二次读继续读下1个字节}printf(parent buff%s\n,buff);//父子进程都会执行的公共逻辑close(fd);}fork()创建子进程时子进程的 PCB 会直接拷贝父进程的 PCB 内容。这就导致子进程 PCB 中的文件表指针与父进程的指针值完全相同因此父子进程会共享父进程在fork()调用之前打开的所有文件描述符。由此可见父进程预先打开的文件在fork()之后可以被子进程直接共享使用。yanyan-virtual-machine:~/mycode/file$ ./forkfile parentbuffa childbuffb parentbuffc childbuffd parentbuffdyanyan-virtual-machine:~/mycode/file$ ./forkfile parentbuffa childbuffb childbuffc parentbuffc parentbuffd运行发现多次运行结果可能会有不同其根本原因是Linux 内核对「父子进程的调度顺序」是抢占式、非确定性的第一次执行时子进程sleep(1)结束后先完成第二次 read 公共打印再轮到父进程执行第二次 read 公共打印第二次执行时父进程sleep(1)结束后比子进程先被调度唤醒先完成第二次 read 公共打印再轮到子进程执行第二次 read 公共打印。sleep仅“让出CPU不能保证”执行顺序“代码中的sleep(1)只是让当前进程休眠 1 秒但不保证 “休眠结束后一定是另一个进程先执行”sleep(1)的作用是 “降低调度随机性”但无法完全消除内核可能在父进程休眠 0.5 秒后就提前唤醒或子进程休眠后父进程优先被调度若想完全固定执行顺序需要用wait()/waitpid()等同步机制比如父进程fork后先wait(NULL)等待子进程执行完再执行自己的逻辑。修改后代码运行结果均为第二种else{read(fd,buff,1);printf(parent buff%s\n,buff);sleep(1);// 等待子进程执行完毕waitpid(pid,NULL,0);read(fd,buff,1);}4.练习(模拟cp命令)#includestdio.h#includestdlib.h#includeunistd.h#includestring.h#includefcntl.h// ./mycp a.jpg b.jpg// argc3, argv[0]mycp, argv[1]a.jpg,argv[2]b.jpgintmain(intargc,char*argv[]){if(argc!3){printf(arg err, eg: ./mycp a.jpeg b.jpeg\n);exit(1);}char*s_nameargv[1];//要复制的元文件名字char*t_nameargv[2];//新文件名intfdropen(s_name,O_RDONLY);//只读打开原文件if(fdr-1){printf(filename:%s open failed\n,s_name);exit(1);}intfdwopen(t_name,O_WRONLY|O_CREAT,0600);//以只写模式打开文件如果文件不存在则创建它且新文件的权限为仅文件所有者可读可写if(fdw-1){printf(create file:%s failed\n,t_name);exit(1);}intnum0;chardata[512];while((numread(fdr,data,512))0){write(fdw,data,num);//// 把缓冲区中实际读取到的num字节数据写入目标文件fdw}close(fdw);close(fdr);exit(0);}运行结果yanyan-virtual-machine:~/mycode/file$lsa.jpg b.jpg image image.c yanyan-virtual-machine:~/mycode/file$ ./image a.jpg b.jpg yanyan-virtual-machine:~/mycode/file$lsa.jpg b.jpg image image.c5.系统调用与库函数的区别5.1.系统调用与库函数1.系统调用定义是操作系统内核提供给用户程序的接口用于让用户程序请求内核执行 “只有内核有权限完成的操作”如访问硬件、操作文件、创建进程等。本质是 “用户态程序向内核态发起的服务请求”示例open打开文件、read读文件、fork创建进程等。2. 库函数定义是函数库如 C 标准库、第三方库提供的预定义函数是对 “系统调用、基础逻辑” 的封装用于简化用户程序的开发。本质是 “用户态的工具函数”部分库函数依赖系统调用部分仅在用户态完成操作示例依赖系统调用的库函数fopen封装open、printf封装write不依赖系统调用的库函数strlen计算字符串长度、memcpy内存拷贝。5.2 两者区别对比系统调用和库函数是用户程序与计算机资源交互的两种核心方式二者的本质差异在于实现位置、执行权限、依赖关系维度系统调用库函数实现位置内核空间操作系统内核代码中用户空间C 标准库 / 第三方函数库中执行权限需切换到内核态执行有硬件资源访问权限全程在用户态执行无直接硬件访问权限功能定位提供最基础的硬件 / 系统资源操作如文件读写、进程创建基于系统调用封装的 “高级功能”如fopen封装openprintf封装write调用开销高需用户态→内核态切换、上下文保存低直接用户态执行部分库函数甚至无系统调用依赖对象依赖操作系统内核不同系统的系统调用可能不同依赖函数库如 C 标准库是跨系统的5.2.1 系统调用的执行流程结合图示解析以open系统调用为例其执行过程是“用户态发起请求→内核态执行操作→返回用户态”的过程用户态准备系统调用号应用程序调用open时会先获取该系统调用对应的系统调用号图示中_NR_open的值是 5并将其存入Eax寄存器。触发软中断切换到内核态应用程序触发0x80号软中断这是 Linux 中用户态向内核态发起系统调用的通用方式此时 CPU 会从用户态切换到内核态并保存当前用户态的上下文。内核态查找并执行系统调用内核根据Eax寄存器中的系统调用号5在系统调用表中找到对应的内核函数图示中sys_open执行实际的文件打开操作如操作磁盘、分配文件描述符。返回用户态传递结果内核执行完成后将结果如文件描述符返回给应用程序并恢复用户态上下文程序回到用户态继续执行。5.2.2 库函数与系统调用的关系多数库函数是对系统调用的封装例如 C 标准库的fopen函数底层会调用open系统调用printf函数底层会调用write系统调用。部分库函数甚至不需要系统调用如strcpy字符串拷贝仅在用户态内存中操作。

更多文章