【Linux】文件描述符fd

一、C知识回顾

C语言文件操作的相关接口中,fopen函数的返回值为一个类型为FILE的结构体指针,这个FILE类型即为C语言概念中的描述文件的类型。而C语言库中默认存在着三个输入输出相关的文件流,这三个文件流会在进程运行的时候默认被打开,分别为:
<1> 标准输入:stdin,对应硬件设备:键盘
<2> 标准输出:stdout,对应硬件设备:显示器
<3> 标准错误:stderr,对应硬件设备:显示器

二、文件管理

往显示器上显示,往磁盘文件打开或写入文件,读写键盘,本质上是访问硬件,我们用户在访问硬件,不可能直接通过语言进行直接访问硬件的,必须要通过操作系统,

我们使用的C接口,看起来是直接访问硬件,其实是通过操作系统提供的系统调用接口,才能访问到硬件,所以我们使用的C接口,底层一定要封装对应的文件类的系统调用!

fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。

⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝ 

2.1 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);
  1. 参数pathname:文件名
  2. 参数flag:标志位,通过传递不同的标志为可以以不同的访问方式打开文件
  3. 参数mode:设置文件的初始权限位,可以使用系统接口umask来提前设置创建文件的权限掩码
  4. 返回文件描述符fd,数据类型为整形

flag标志位的常用参数

  1. O_RONLY,只读
  2. O_WRONLY,只写
  3. O_RDWR,读写
  4. O_TRUNC,清空文件,覆盖式写入
  5. O_APPEND,向文件结尾写入,追加
  6. O_CREAT,创建文件

 系统所给出的flag参数实质上为一个个定义好的宏,flag参数的类型为int,其有着32个bit位。
<1> 这些宏都是32个bit位中只有一位为1的int类型数据,并且它们的含1bit位都互相错位。
<2> 通过此种位操作宏定义与按位与|的方式,可以达到一次性传递多个标志位参数的效果,Linux操作系统中常用。

#include <stdio.h>
 
#define ONE (1<<0)//1   000001  左移0位
#define TWO (1<<1)//2   000010  左移1位
#define THREE (1<<2)//4 000100  左移2位
#define FOUR (1<<3)//16 001000
#define FIVE (1<<4)//32 010000
 
//code 1
void PrintTest(int flags)
{
    //都为1才为1,只要有一个不为1,就为0
    if(flags & ONE)
    {
        printf("one\n");
    }
    if(flags & TWO)
    {
        printf("two\n");
    }
    if(flags & THREE)
    {
        printf("three\n");
    }
    if(flags & FOUR)
    {
        printf("four\n");
    }
    if(flags & FIVE)
    {
        printf("five\n");
    }
}
 
int main()
{
    printf("=====================\n");
    PrintTest(ONE);
    printf("=====================\n");
    PrintTest(TWO);
    printf("=====================\n");
    PrintTest(THREE);
    printf("=====================\n");
    //只要两个操作数对应的位中有一个为1,那么结果位就为1。
    PrintTest(ONE | THREE);
    printf("=====================\n");
    PrintTest(ONE | TWO | THREE);
    printf("=====================\n");
    PrintTest(ONE | TWO | THREE | FOUR);
    printf("=====================\n");
    return 0;
}

现在我们来使用一下open函数,第二个参数传递 O_WRONLY | O_CREAT,以只写方式打开,如果文件不存在则创建:

#include <stdio.h>
#include <fcntl.h>
 
int main()
{
    //以只写方式打开,如果文件不存在则创建
    open("log.txt",O_WRONLY | O_CREAT);
    return 0;
}
-rwxrwxr-x 1 zxw zxw 8408 Jan 14 17:52 filecode
-rw-rw-r-- 1 zxw zxw 1193 Jan 14 17:52 filecode.c
-r--r-x--T 1 zxw zxw    0 Jan 14 17:52 log.txt //权限错乱
-rw-rw-r-- 1 zxw zxw   71 Jan 12 17:31 Makefile

这里我们发现新创建的文件,他的权限是错乱的,那是因为我们调用系统接口时,新创建文件,需要给文件进行权限设置,而我们语言级接口fopen不需要,是因为底层对其进行了封装。

所以用系统调用接口时,新建文件,还要告诉新建文件默认的起始权限是多少!告诉第三个接口mode_t mode。

int main()
{
    open("log.txt",O_WRONLY | O_CREAT,0666);
    return 0;
}
-rwxrwxr-x 1 zxw zxw 8360 Jan 14 18:07 filecode
-rw-rw-r-- 1 zxw zxw 1185 Jan 14 18:07 filecode.c
-rw-rw-r-- 1 zxw zxw    0 Jan 14 18:07 log.txt
-rw-rw-r-- 1 zxw zxw   71 Jan 12 17:31 Makefile

此外为什么我log.txt的权限是-rw-rw-r--而不是-rw-rw-rx-因为我传递的是0666

因为系统里有个umask,他会默认屏蔽掉一些权限,系统的umask = 0002,结合666,就会编成664,所以文件的最终权限会结合umask值来进行最终确认。

int main()
{
    //不让系统屏蔽某些权限
    umask(0);
    //以只写方式打开,如果文件不存在则创建
    open("log.txt",O_WRONLY | O_CREAT,0666);
    return 0;
}
-rwxrwxr-x 1 zxw zxw 8408 Jan 14 18:17 filecode
-rw-rw-r-- 1 zxw zxw 1198 Jan 14 18:17 filecode.c
-rw-rw-rw- 1 zxw zxw    0 Jan 14 18:18 log.txt
-rw-rw-r-- 1 zxw zxw   71 Jan 12 17:31 Makefile

对在代码中umask清0,并不会影响到系统的umask值(系统的umask还是0002)。最终文件权限就是是第三个参数传递的权限。

2.2 write

       #include <unistd.h>
       ssize_t write(int fd, const void *buf, size_t count);
  1. 参数fd:文件描述符,指定要写入的目标对象
  2. 参数buf:写入数据存储地址,缓冲区
  3. 参数count:写入数据的大小,单位字节
  4. 返回值为ssize_t,有符号整形,写入成功返回0,写入失败返回-1

2.3 read 

       #include <unistd.h>
       ssize_t read(int fd, void *buf, size_t count);
  1. 参数fd:文件描述符,指定要写入的目标对象
  2. 参数buf:读取的内容存储空间的地址
  3. 参数count:读取多少个字节
  4. 返回值ssize_t,有符号整形,读取成功返回读取了多少个字节,读取失败返回-1

2.4 close

       #include <unistd.h>
       int close(int fd);
  1. 传递参数文件fd关闭指定文件
  2. 关闭成功返回0,关闭失败返回-1

2.5 什么是文件描述符fd

我们重新回到2.1open的返回值那块看看

返回文件描述符fd,数据类型为整形

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
 
int main()
{
    //以只写方式打开,如果文件不存在则创建
    int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
    if(fd1<0)
    {
        perror("open");
        return 0;
    }
    printf("fd1: %d\n",fd1);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode 
fd1: 3

此时文件标识符为3,为什么文件打开从3开始呢?

因为进程启动,默认打开了三个标准的输入输出流:stdin,stdout,stderr

因为Linux下一切皆文件,这三个标准的输入输出流被当成文件打开了。

现在我们使用系统调用接口来进行文件写入,如下代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
 
int main()
{
    //以只写方式打开,如果文件不存在则创建
    int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
    if(fd1<0)
    {
        perror("open");
        return 0;
    }
    printf("fd1: %d\n",fd1);
 
    const char *message = "hello word\n";
    write(fd1,message,strlen(message));
    close(fd1);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt 
hello word

 紧接着,我们只修改一个message指向的内容,原本文件内容不变:、

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
 
int main()
{
    //以只写方式打开,如果文件不存在则创建
    int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
    if(fd1<0)
    {
        perror("open");
        return 0;
    }
    printf("fd1: %d\n",fd1);
 
    const char *message = "aaaaaa\n";
 
    write(fd1,message,strlen(message));
    close(fd1);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt 
aaaaaa
ord

因为在做操作时只告诉了写入,并没有告诉要清空,只覆盖在原来基础上进行覆盖式的写入!!!

所以我们再加个选项O_TRUNC,如果文件存在则进行先清空

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
 
int main()
{
    //以只写方式打开,如果文件不存在则创建,如果存在则清空
    int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    if(fd1<0)
    {
        perror("open");
        return 0;
    }
    printf("fd1: %d\n",fd1);
 
    const char *message = "aaaaaa\n";
 
    write(fd1,message,strlen(message));
    close(fd1);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt 
aaaaaa

内容被清空了,O_WRONLY | O_CREAT | O_TRUNC传参,最终效果就是和用fopen打开使用“w”方法一样,fopen的“w”方法,底层就是封装了这些。

[zxw@hcss-ecs-cc58 lesson17]$ ./filecode 
fd1: 3
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt 
aaaaaa
aaaaaa

O_WRONLY | O_CREAT | O_APPEND传参,最终效果就是和用fopen打开使用“a”方法一样,fopen使用“a”方法底层就是封装了这些。

fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。

open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝ 

fopen fclose fread fwrite底层就是对open close read write进行了封装。

关于fd的问题:

我们连续打开几个文件,看看文件描述符是多少?

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
	umask(0);
	int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
	int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
	int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
	int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
	int fd5 = open("log5.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

	printf("fd1 = %d\n", fd1);
	printf("fd2 = %d\n", fd2);
	printf("fd3 = %d\n", fd3);
	printf("fd4 = %d\n", fd4);
	printf("fd5 = %d\n", fd5);

	close(fd1);
	close(fd2);
	close(fd3);
	close(fd4);
	close(fd5);

	return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode 
fd1 = 3
fd2 = 4
fd3 = 5
fd4 = 6
fd5 = 7

观察到文件描述符从3开始依次创建 

前面我们说到,0,1,2被键盘,显示器,显示器占用,又说过,这些硬件在底层被包装成文件的形式,同样,我们能不能通过0,1,2进行对键盘文件,显示器文件进行写与读呢?

看下面代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
 
int main()
{
    const char *message = "hello write\n";
    write(1,message,strlen(message));
 
    return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode 
hello write

我们通过write,直接向文件描述符1,进行写入message指向的内容,结果的确打印在屏幕上

我们再来看看下面调用read进行读:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
 
int main()
{
    //abcd
    char buffer[128];
    ssize_t s = read(0,buffer,sizeof(buffer));
    if(s > 0)
    {
        //把回车改为0
        buffer[s-1] = 0;
        printf("%s\n",buffer);
    }
 
    return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode 
abcd
abcd

我们键盘输入abcd,然后打印,发现的确从标识符0进行读取。

我们来看一下下面代码来进行验证:因为stdin,stdout,stderr是FILE*类型,所以用他进行指向一个成员变量_fileno,就可以看到文件描述符: 

int main()
{
 
    printf("stdin:%d\n",stdin->_fileno);
    printf("stdout:%d\n",stdout->_fileno);
    printf("stderr:%d\n",stderr->_fileno);
    FILE *fp = fopen("log.txt","w");
    printf("fp:%d\n",fp->_fileno);
 
    return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode 
stdin:0
stdout:1
stderr:2
fp:3

所以任你文件怎么样,OS只认文件描述符 

在C语言上,用到的函数都是对系统调用的封装

不仅做了接口上的封装,还做了类型上的封装就是struct_file。

2.5.1 重新理解一切皆文件

上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过
struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取Linux系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解。 

2.5.2 文本写入VS二进程写入

在计算机里,OS系统层面上只有二进制概念,语言层看起来可以文本写入,也可以二进制写入。

我们向显示器写12345,写的是1‘’2‘’3‘’4‘’5‘字符。

int main()
{
    char *message = "hello\n";
    write(1,message,strlen(message));
    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test 
hello
int main()
{
    int a = 12345;
    write(1,&a,sizeof(a));
    printf("\n");
    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test 
90
int main()
{
    int a = 12345;
    char buffer[1024];
    snprintf(buffer,sizeof(buffer),"%d",a);
    write(1,buffer,strlen(buffer));
    printf("\n");
    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test 
12345

 所以为什么我们C语言要给我们很多接口做封装?

1.方便用户进行操作

2.提高语言的可移植性

在使用这些接口的时候,win和Linux或者其他平台提前给我们安装了一些东西,这个东西叫glibc的库,语言层使用的一些接口,在glibc中进行了封装,把库编成Linux版本的库,win版本的库等等

<think>好的,我现在需要回答用户关于文件描述符fd)在操作系统中的概念的问题。首先,我需要回忆之前学过的相关知识,并结合提供的引用内容来整理思路。 根据引用3,文件描述符是操作系统用来管理已打开文件的小整数,每个进程都有一个指向files_struct结构体的指针,里面包含了一个文件指针数组,而文件描述符就是这个数组的索引。这解释了fd的基本作用,即通过索引找到对应的文件结构体。 引用1提到,struct file中的inode指针指向的是VFS(虚拟文件系统),而不是具体的文件系统如ext4。这说明操作系统通过VFS层来抽象不同文件系统的差异,使得上层应用可以统一处理文件操作。这也与文件描述符的作用相关联,因为fd通过VFS间接访问实际文件系统的inode。 引用2补充了Python中fd的使用,指出fd是整数值,用于表示操作系统打开的文件,并通过os模块的函数进行操作。这说明不同编程语言中对文件描述符的处理方式类似,都是基于底层的系统调用。 引用4提到默认情况下,进程启动时会打开stdin、stdout、stderr,对应的fd为0、1、2。这解释了fd的分配规则,新的fd会从最小的未使用整数开始分配,比如第一次打开文件时fd为3。 接下来需要将这些信息结构化,用中文清晰解释fd的概念、结构、分配规则以及VFS的作用。同时,注意按照用户的系统级指令,行内公式用$...$,独立公式用$$...$$,但这里可能涉及较少数学公式,重点在于概念描述。确保引用正确,每个引用末尾加上标识符。 最后,生成相关问题时要围绕fd的相关知识点,比如分配规则、与文件操作的关系、VFS的作用等,确保问题有针对性,帮助用户深入理解。</think>### 文件描述符 fd 的概念 文件描述符(File Descriptor, fd)是**操作系统用于管理已打开文件的整数值标识符**,本质上是进程与文件交互的“句柄”。以下从核心概念、结构关联和分配规则展开说明: --- #### 1. 核心概念 - **小整数索引**:文件描述符是一个从0开始的小整数(如0、1、2、3等)。每个进程独立维护自己的文件描述符表[^3]。 - **作用**:通过fd,进程可以读写文件、操作设备(如键盘、屏幕)、网络通信等。例如: - `0` 对应标准输入(stdin),`1` 对应标准输出(stdout),`2` 对应标准错误(stderr)[^4]。 - 用户打开文件时,操作系统返回新的fd(如3、4等)作为操作入口。 --- #### 2. 结构关联 文件描述符通过以下层级结构关联到实际文件: 1. **进程的文件描述符表**:每个进程的`task_struct`结构体中包含`*files`指针,指向`files_struct`。 - `files_struct`包含一个**文件指针数组**,fd即为数组下标[^3]。 ```c // 示例:Linux内核结构简化表示 struct files_struct { struct file **fd_array; // 文件指针数组 }; ``` - 例如,`fd=3`对应`fd_array[3]`,指向一个`file`结构体。 2. **file结构体**:描述已打开文件的状态(如读写位置、权限等),并包含指向**VFS(虚拟文件系统)**的`inode`指针[^1][^3]。 - VFS是操作系统对多种文件系统(如ext4、NTFS)的抽象层,提供统一接口。 3. **VFS与具体文件系统**:VFS的`inode`进一步关联到具体文件系统的元数据,最终通过磁盘驱动访问物理文件。 --- #### 3. 分配规则 - **最小可用原则**:新打开的fd总是选择当前未使用的最小整数。 - 默认已分配`0`(stdin)、`1`(stdout)、`2`(stderr),因此用户首次打开文件时fd通常为3[^4]。 - **关闭后重用**:若关闭fd=3,下次打开文件可能重新分配3。 --- #### 4. 操作示例(Python) ```python import os # 打开文件,返回fd fd = os.open("test.txt", os.O_RDWR) # 通过fd写入 os.write(fd, b"Hello") # 关闭文件 os.close(fd) ``` 此处`os.open()`返回的fd即为操作系统分配的最小未使用整数[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值