linux - 基础IO之操作与文件描述符全解析:从C语言到系统调用底层实现

server/2025/3/16 15:58:52/

目录

1.回顾c语言中所学的文件

linux%E5%9F%BA%E7%A1%80io%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5%E7%9A%84%E5%AD%A6%E4%B9%A0%EF%BC%89-toc" name="tableOfContents" style="margin-left:0px">2.提炼对文件的理解(linux基础io第一阶段的学习)

a.在操作系统内部,一个进程和一个被打开的文件,他们到后面会变成两种对象之间的指针关系。

b.文件 = 属性 + 内容

c.在c语言中,以w的方式打开文件,默认打开文件的时候,就会先把目标文件给清空。a则是追加

linux-%E5%9F%BA%E6%9C%AC%E6%8C%87%E4%BB%A4%E4%B8%8E%E7%94%A8%E6%B3%9502%EF%BC%88%E8%B6%85%E8%AF%A6%E7%BB%86%EF%BC%81%EF%BC%81%EF%BC%89_linux%E7%9A%8402-CSDN%E5%8D%9A%E5%AE%A2-toc" name="tableOfContents" style="margin-left:40px">d.输出重定向  ‘  > ’--->先清空再写入(以w方式打开文件),追加重定向 '>>'具体可以看博客linux-基本指令与用法02(超详细!!)_linux的02-CSDN博客

3.理解文件

open()系统调用函数

参数说明

返回值

打开现有文件(只读)

  独占创建文件(避免竞态条件)

创建新文件(读写权限,若存在则清空)

设计传递位图标记位的函数

close()系统调用函数

close() 的作用

close()参数:

write()系统调用函数

参数说明:

返回值:

write() 与 fwrite() 对比

什么叫做fd(文件描述符):

为什么不返回 0、1、2?

fd的本质是什么?

fd的本质是:内核的进程:文件映射关系的数组的下标

一切皆文件 

c语言上的文件操作函数,本质底层都是对系统调用的封装

c语言为什么要这么做:


1.回顾c语言中所学的文件

进行文件操作,前提是我们的程序在执行了,所谓的文件的打开和关闭,是cpu在执行代码,比如执行fopen(),才将文件打开的。

当一个代码执行的时候已经变成了一个进程,所以在建立文件时,他就会默认的接在当前进程所处路径后,拼上所创建的文件名,创建的这个文件。

(进程启动时所处的路径:当前进程的当前工作路径。)

linux%E5%9F%BA%E7%A1%80io%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5%E7%9A%84%E5%AD%A6%E4%B9%A0%EF%BC%89" name="2.%E6%8F%90%E7%82%BC%E5%AF%B9%E6%96%87%E4%BB%B6%E7%9A%84%E7%90%86%E8%A7%A3%EF%BC%88linux%E5%9F%BA%E7%A1%80io%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5%E7%9A%84%E5%AD%A6%E4%B9%A0%EF%BC%89">2.提炼对文件的理解(linux基础io第一阶段的学习)

a.在操作系统内部,一个进程和一个被打开的文件,他们到后面会变成两种对象之间的指针关系。

  • 打开文件,本质是进程打开文件。
  • 当文件本就存在时,文件没有被打开的时候,存在磁盘上
  • 一个进程可以打开多个文件。
  • 系统当中可以存在多个进程,大多数情况下,OS内部,一定存在大量被打开的文件,每个进程可能都打开很多文件。
  • 文件未被打开时处于磁盘上,处于一个硬件上,因此只能被操作系统(操作系统是硬件的管理者)打开(用的是c语言接口),因此操作系统会对这些被打开的文件进行管理(先描述,再组织)。
  • 每一个被打开的文件,在os内部,一定会存在对应的描述文件属性的结构体,类似于PCB。

b.文件 = 属性 + 内容

当我在磁盘上新建一个文本文件,但并不打开,并不往其中填写任何数据,他在磁盘上所占据的大小是0KB,此时他是否会占据磁盘空间?会占据,文件的名字、文件建立的时间、文件的大小等等文件的属性就已经占据了磁盘空间了。0kb指的是内容为0。结构体放的就是文件的属性。

1.打开文件 2.并向文件 写入3.再关闭。

 

1.打开文件  --- w ,不存在就在当前路径下创建指定文件。 2.并向文件 写入3.再关闭。都是进程让cpu在执行自己的代码。通过这样的方式发开文件,访问文件。

c.在c语言中,以w的方式打开文件,默认打开文件的时候,就会先把目标文件给清空。a则是追加

只以写的方式打开文件:

延续上面的操作本来log.txt中是有内容的,现在被清空了

linux-%E5%9F%BA%E6%9C%AC%E6%8C%87%E4%BB%A4%E4%B8%8E%E7%94%A8%E6%B3%9502%EF%BC%88%E8%B6%85%E8%AF%A6%E7%BB%86%EF%BC%81%EF%BC%81%EF%BC%89_linux%E7%9A%8402-CSDN%E5%8D%9A%E5%AE%A2" name="d.%E8%BE%93%E5%87%BA%E9%87%8D%E5%AE%9A%E5%90%91%C2%A0%20%E2%80%98%C2%A0%20%3E%20%E2%80%99---%3E%E5%85%88%E6%B8%85%E7%A9%BA%E5%86%8D%E5%86%99%E5%85%A5%EF%BC%88%E4%BB%A5w%E6%96%B9%E5%BC%8F%E6%89%93%E5%BC%80%E6%96%87%E4%BB%B6%EF%BC%89%EF%BC%8C%E8%BF%BD%E5%8A%A0%E9%87%8D%E5%AE%9A%E5%90%91%20'%3E%3E'%E5%85%B7%E4%BD%93%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%8D%9A%E5%AE%A2linux-%E5%9F%BA%E6%9C%AC%E6%8C%87%E4%BB%A4%E4%B8%8E%E7%94%A8%E6%B3%9502%EF%BC%88%E8%B6%85%E8%AF%A6%E7%BB%86%EF%BC%81%EF%BC%81%EF%BC%89_linux%E7%9A%8402-CSDN%E5%8D%9A%E5%AE%A2">d.输出重定向  ‘  > ’--->先清空再写入(以w方式打开文件),追加重定向 '>>'具体可以看博客linux-基本指令与用法02(超详细!!)_linux的02-CSDN博客

3.理解文件

a,操作文件、本质:进程在操作文件。进程和文件的关系

b.文件 --> 磁盘(外设)---> 硬件  ----> 向文件中写入,本质是向硬件中写入。 ---->  用户没有权利直接向硬件写入  ---->  硬件的管理者是操作系统 ------>  用户无法绕过操作系统去处理硬件(嵌入式除外)----> 用户必须通过OS来写入 ---->  操作系统给用户提供系统调用 ----> c语言 / c++ ...都是对系统调用接口的封装   ---->  访问文件,就可以用系统调用

c++写入文件:

#include<iostream>
#include<string>
#include<fstream>#define FILENAME "log.txt"using namespace std;int main()
{std::ofstream out(FILENAME, std::ios::binary);if(!out.is_open()) return 1;string message("hello c++\n");out.write(message.c_str(), message.size());out.close();return 0;
}

open()系统调用函数

       #include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);

参数说明

pathname

类型:const char*

作用:要打开或创建的文件路径(绝对或相对路径)。

flags

类型:int

作用:指定文件的打开方式,多个标志可通过按位或(|)组合。

常用标志:

必选标志(三选一):

O_RDONLY: 只读模式、O_WRONLY: 只写模式、O_RDWR: 读写模式。

可选标志:(O代表open的意思)

O_CREAT:

        文件不存在时创建新文件,需配合mode参数设置权限。

O_TRUNC:

        若文件存在且为普通文件,将其长度截断为0。

O_APPEND:

        追加写入(每次写操作前移动到文件末尾)。

O_EXCL:

        O_CREAT联用时,若文件已存在则返回错误(用于原子性创建文件)。

O_NONBLOCK:

         非阻塞模式(对设备文件或管道有效)。

mode

类型:mode_t

作用:创建文件时的权限(仅当使用O_CREAT时需指定)。

常见值(八进制表示):

0644: 用户可读写,组和其他用户只读。

0755: 用户可读写执行,组和其他用户可读执行。

注意:实际权限为 mode & ~umaskumask用于过滤权限位)。

返回值

  • 成功:返回文件描述符(非负整数),用于后续操作(如readwrite)。

  • 失败:返回-1,并设置errno指示错误类型(如ENOENT文件不存在、EACCES权限不足)

打开现有文件(只读)

当已经存在文件时,可以不传权限的参数

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>int main() {int fd = open("test.txt", O_RDONLY);if (fd == -1) {perror("open failed");return 1;}close(fd);return 0;
}

  独占创建文件(避免竞态条件)

int fd = open("unique.txt", O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd == -1) {perror("file already exists");return 1;
}

创建新文件(读写权限,若存在则清空)

创建新文件时,一定要传权限码,例如:0666:用户、组、其他,默认都是可读可写的权限

-rwxr-xr--  1 user  group  4096 Jun 1 10:00 file.txt
▲ ▲▲▲ ▲▲▲ ▲▲▲ 
│ │││ │││ │││ 
│ └──────┬─────── 权限(用户u、组g、其他o)
└───────── 文件类型(`-`普通文件,`d`目录)

八进制:

 数字表示(八进制):

  • r=4, w=2, x=1,三者相加:

    • rwxr-xr-- → 7(4+2+1) 5(4+0+1) 4(4+0+0) → 权限数字 754

int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd == -1) {perror("open");return 1;
}

没有传权限码就会出现权限处乱码:

传了0666:

这是因为:最终权限 = 默认最大权限 - umask 值(实际是位运算 默认权限 & ~umask

如何一次性创建好需求权限的文件:

umask()系统调用

 #include <sys/types.h>#include <sys/stat.h>mode_t umask(mode_t mask);

修改原代码:将系统掩码设置为0,没设置的时候就用系统默认的

  umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd < 0){perror("open");return 1;}

输出:

补充:文件权限计算

  • 默认最大权限:666(二进制 110 110 110

  • umask 值:002(二进制 000 000 010

  • 实际权限:666 - 002 = 664

    (即 rw-rw-r--,用户和组可读写,其他只读)

将这个整数参数看作是一张位图

设计传递位图标记位的函数

通过设计一个传递位图标记位的函数来理解os 设计很多系统调用接口的方法:

#include <stdio.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define ONE    1     // 1 0000 0001
#define TWO   (1<<1) // 2 0000 0010
#define THREE (1<<2) // 4 0000 0100
#define FOUR  (1<<3) // 8 0000 1000void print(int flag)
{if(flag & ONE)printf("one\n");if(flag & TWO)printf("two\n");if(flag & THREE)printf("three\n");if(flag & FOUR)printf("four\n");
}int main()
{print(ONE);printf("\n");print(TWO);printf("\n");print(ONE | TWO);printf("\n");print(ONE | TWO | THREE);printf("\n");print(ONE | FOUR);printf("\n");print(ONE | TWO | THREE | FOUR);printf("\n");return 0;
}

通过标志位来让我们实现对应的功能,向指定的函数传递多种标记位的方法,标记位传参

close()系统调用函数

close() 的作用

释放资源

  • 每个打开的文件描述符都会占用系统资源(如内核中的文件表项、缓冲区等)。
  • 调用 close() 后,系统会释放这些资源。

刷新缓冲区

  • 如果文件是以写入模式打开的,close() 会确保所有缓冲区的数据写入磁盘(类似于 fflush())。

解除文件描述符的绑定

  • 关闭后,文件描述符不再与任何文件或资源关联,可以被重新用于其他文件。

避免资源泄漏

  • 如果不关闭文件描述符,可能会导致文件描述符耗尽(每个进程有文件描述符数量限制)。

close()参数:

#include <unistd.h>int close(int fd);
  • fd:要关闭的文件描述符(通常由 open()socket() 等函数返回)。

返回值:

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 表示错误原因。

write()系统调用函数

一、函数原型与头文件

#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);

参数说明:

fd(文件描述符)

  • 已打开文件的描述符(由 open()socket() 等函数返回)。
  • 必须具有 可写权限(例如以 O_WRONLY 或 O_RDWR 模式打开)。

buf(数据缓冲区)

  • 指向用户空间缓冲区的指针,包含待写入的数据。
  • 可以是任意类型的数据(如字符串、二进制数据)。

count(写入字节数)

  • 指定从 buf 中写入的字节数。
  • 实际写入的字节数可能小于 count(需检查返回值)。

返回值:

成功时:返回实际写入的字节数(0 ≤ 返回值 ≤ count)。

  • 返回值为 0 表示未写入数据(例如写入到已满的管道)。
  • 返回值小于 count 表示部分写入(需处理剩余数据)。

失败时:

  • 返回 -1,并设置全局变量 errno 表示错误类型

write() 与 fwrite() 对比

特性write()fwrite()
接口层级系统调用(底层)标准库函数(高层)
缓冲无缓冲(直接写入内核)带用户空间缓冲区
错误处理通过 errno 和返回值通过返回值与 ferror()
适用场景需要精细控制的场景(如非阻塞)常规文件操作(更便捷)

什么叫做fd(文件描述符):

strlen 函数:只用写入有效字符串

输出:

很显然第二次的写入是在上一次的基础上从头开始写的。

以写的方式打开,不存在就创建,并且先清空文件内容

int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

open()的返回值fd是什么?这里我创建了四个文件并记录他们的返回值

输出结果:

为什么不返回 0、1、2?

因为:                                                                                                                                                                                        

  • 1:标准输入 - 键盘
  • 2:标准输出 - 显示器
  • 3:标准错误 - 显示器

这里和c语言的进行对比

stdin、stdout、stderr对应的类型都是文件指针,与c语言的fopen、fdopen、freopen的返回值是一样的

这些说明,在c语言中我们把键盘显示器也是当做文件来看的

也就是说,由于write()是根据open返回的fd,来查找文件并写入的,那我直接往1中写入不就是往显示器文件中写吗:

编译运行:

fd  --->   文件描述符   --->  文件描述符的本质是什么?

fd的本质是什么?

1.在打开文件的时候,会在操作系统中创建一个struct file,文件的内核数据结构所包含的是文件的属性(权限,什么方式被打开,标记位),所有被打开的文件,他们的内核数据结构以双链表的形式被链接起来,操作系统对文件的管理转变成对链表的增删查改,每一个struct file内部都有一个指向与 该文件所对应的文件内核级的缓存的 指针,操作系统给文件申请的内存。

一个磁盘上的文件,会经过属性struct file内核数据结构 初始化,内容直接存到这个文件的缓存当中。未来直接从缓存当中读写修改。

os中有多个进程,每个进程都有可能打开多个文件,进程和文件的关系是1:n,进程的内核数据结构中存在一个struct files_struct *file属性。os中还会存在一个struct file_struct内核数据结构,整个结构中会包含一个指针数组struct file* fd_array[N]。

想让进程和对应的文件产生关系:

将描述文件的结构体变量的地址(文件属性的地址),依次填入到 fd_array[],特定的数组中,因此一个进程想要找到对应的文件,只需要把对应文件数组的下标返回给上层比如说 int fd,就可以访问文件了。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

fd的本质是:内核的进程:文件映射关系的数组的下标

读的本质是:将文件缓冲区的内容拷贝到需要的文件当中去,如果对应的内容不在缓冲区里,os就会把这个打开文件的进程阻塞住,挂起,os再将磁盘中的数据搬到缓存当中,再唤醒进程。

写和修改内容:没有内容的时候上层就直接将内容拷贝到缓冲区,当文件中本来就有内容的时候,现将内容拷贝到缓冲区,再在内存当中修改,定期由os再刷新

1.无论读写,都必须让os把内容读到对应的文件缓冲区内,在内存中修改,再刷新到磁盘

2.open()在:

a.创建file

b.开辟文件缓冲区的空间,加载文件数据(延后)

c.查进程的文件描述符表

d.将file地址,填入对应的表中

e.返回下标

3.write()、read()函数的本质是拷贝函数

一切皆文件 

像是键盘、鼠标、显示器、网卡、磁盘这些外设,他们可以由一个设备结构体来记录他们的属性,但是,他们每一个的操作方法都不同,这是通过驱动来控制的。对每一个设备os都会构建一个struct file,里面就会包含他们的读写的函数指针,再指向驱动层的方法。使用同一个类,其中包含的读写函数指针指向不同的设备,因此我们就不用再管底层的差异了,因为底层外设的方法--->归于函数指针  ---》一切皆文件   

这就像c++中的多态

这是一个指针指向一张操作表:

每一个被打开的文件还会有一张,操作底层方法的指针表

在操作系统中,这就叫做虚拟文件系统:virtual file system

在操作系统中,系统访问文件时只认文件描述符fd

如何理解c语言通过FILE* 访问文件? 这个FILE是一个结构体

因此这个FILE里面一定封装了fd文件描述符

c语言上的文件操作函数,本质底层都是对系统调用的封装

写如下代码:

  FILE* fp = fopen("log.txt", "w");if(fp == NULL){perror("fopen");return 1;}printf("fd: %d\n",fp->_fileno);fwrite("hello\n", 5, 1, fp);fclose(fp);

运行结果:这就证明了我们的FILE结构体中封装了fd文件描述符,

更进一步:

代码:

  FILE* fp1 = fopen("log1.txt", "w");if(fp1 == NULL){perror("fopen");return 1;}printf("fd1: %d\n",fp1->_fileno);FILE* fp2 = fopen("log2.txt", "w");if(fp2 == NULL){perror("fopen");return 1;}printf("fd2: %d\n",fp2->_fileno);FILE* fp3 = fopen("log3.txt", "w");if(fp3 == NULL){perror("fopen");return 1;}printf("fd3: %d\n",fp3->_fileno);fclose(fp1);fclose(fp2);fclose(fp3);

输出结果:

c语言的stdout:stdin:stderr:

printf("stdin ->fd: %d\n", stdin->_fileno);printf("stdout->fd: %d\n", stdout->_fileno);printf("stderr->fd: %d\n", stderr->_fileno);

最后输出

c语言为什么要这么做:

本来可以使用系统调用,也可以使用语言提供的文件方法

系统不一样,系统调用接口就不一样,代码不具有跨平台性,而为什么c语言、c++....等所有的语言都具有跨平台性的原因和作用我们现在就知道了。

如图:

所有的语言要对不同的平台的系统调用进行封装,不同语言封装时,文件接口就有差别了

在c++中的cin、cout、cerr可以向文件,显示器都写,我们称他们为流,但cin、cout、cerr在c++中都叫做类,他们内部一定包含了文件描述符。

通过进程pid找到fd

终端文件:

这个终端也是属于一个文件,因此实际上我也可以向这个终端直接写东西:

运行结果:

结语:

       随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。    

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。               

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。


http://www.ppmy.cn/server/175467.html

相关文章

基于自定义线程池手写一个异步任务管理器

我们在后端执行某些耗时逻辑操作时往往会导致长时间的线程阻塞&#xff0c;在这种情况之下&#xff0c;我们往往会引一条异步线程去处理这些异步任务&#xff0c;如果每次都创建新的线程来处理这些任务&#xff0c;不仅会增加代码冗余&#xff0c;还可能造成线程管理混乱&#…

[设计模式]1_设计模式概览

摘要&#xff1a;设计模式原则、设计模式的划分与简要概括&#xff0c;怎么使用重构获得设计模式并改善代码的坏味道。 本篇作概览与检索用&#xff0c;后续结合源码进行具体模式深入学习。 目录 1、设计模式原理 核心原则&#xff08;语言无关&#xff09; 本质原理图 原…

深入理解 RTP、RTCP、RTMP、RTSP、HLS 及 live555 推拉流实现

流媒体技术在音视频传输中起着关键作用&#xff0c;其中 RTP、RTCP、RTMP、RTSP 和 HLS 是最常见的协议。本文将详细介绍它们的区别&#xff0c;并探讨为什么 HLS 逐渐取代 RTMP。此外&#xff0c;还将解析 RTSP 作为控制协议的作用&#xff0c;并讲解 live555 如何实现音视频的…

网络安全就业形势

网络安全是一个日益增长的行业&#xff0c;对于打算进入或转行进入该领域的人来说&#xff0c;制定一个清晰且系统的职业规划非常重要。2025年&#xff0c;网络安全领域将继续发展并面临新的挑战&#xff0c;包括不断变化的技术、法规要求以及日益复杂的威胁环境。 第一部分&am…

DeepSeek 与 ChatGPT的主要区别

DeepSeek 是由中国公司 DeepSeek AI &#xff08;杭州深度求索人工智能基础技术研究有限公司&#xff09;开发的 AI 聊天机器人&#xff0c;于 2024 年推出。相比之下&#xff0c;ChatGPT 是由美国 AI 研究实验室 OpenAI 创建的&#xff0c;自 2022 年以来就已上市。两者都是专…

【部署】ubuntu部署olmOCR

目录 一、安装依赖二、安装conda新环境和sglang三、PDF解析1. 运行2. 原始 PDF 并排查看结果3. 更换模型 四、可能出现的问题1.note: This error originates from a subprocess, and is likely not a problem with pip.2.转换单个PDF命令运行时3.ImportError: libnccl.so.2: ca…

【NLP】10. 机器学习模型性能评估指标(含多类别情况), ROC,PRC

机器学习模型性能评估指标&#xff08;含多类别情况&#xff09; 1. 模型评估指标简介 在机器学习中&#xff0c;模型的性能评估非常重要。常用的模型评估指标有&#xff1a; 准确率&#xff08;Accuracy&#xff09;精度&#xff08;Precision&#xff09;召回率&#xff0…

Dijkstra算法

Dijkstra算法&#xff08;迪杰斯特拉算法&#xff09;是一种经典的单源最短路径算法&#xff0c;用于在加权图中找到从一个源点到所有其他顶点的最短路径。它要求图中不能有负权边&#xff0c;因为负权边可能会导致算法的贪心策略失效。 Dijkstra算法的基本思想 Dijkstra算法…