【Linux】文件描述符 - fd

2024-03-21 22:20
文章标签 linux fd 描述符

本文主要是介绍【Linux】文件描述符 - fd,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1. open 接口介绍
    • 1.1 代码演示
    • 1.2 open 函数返回值
  • 2. 文件描述符 fd
    • 2.1 0 / 1 / 2
    • 2.2 文件描述符的分配规则
  • 3. 重定向
    • 3.1 dup2 系统调用函数
  • 4. FILE 与 缓冲区

在这里插入图片描述

1. open 接口介绍

使用 man 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: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR  : 读,写打开这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限O_APPEND: 追加写	
返回值:成功:新打开的文件描述符失败:-1

open 函数具体使用哪个,和具体应用场景有关。如:目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限;否则使用两个参数的 open。

write read close lseek ,类比 C 文件相关接口。

1.1 代码演示

操作文件,除了使用 C 语言的接口【Linux】回顾 C 文件接口,还可以采用系统接口来进行文件访问;

写文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{umask(0);int fd = open("myfile", O_WRONLY | O_CREAT, 0644);if (fd < 0){perror("open");return 1;}int count = 5;const char* msg = "hello open!\n";int len = strlen(msg);while (count--){write(fd, msg, len);// fd : 下面介绍// msg : 缓冲区首地址// len : 本次读取,期望写入多少个字节的数据// 返回值 : 实际写了多少字节数据}close(fd);return 0;
}

读文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{int fd = open("myfile", O_RDONLY);if (fd < 0){perror("open");return 1;}const char* msg = "hello open!\n";char buf[1024];while (1){ssize_t s = read(fd, buf, strlen(msg)); // 类比writeif (s > 0){printf("%s", buf);}else{break;}}close(fd);return 0;
}

1.2 open 函数返回值

在认识返回值之前,先来认识两个概念:系统调用库函数

  • fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数(libc);
  • open close read write lseek 都属于系统提供的接口,称之为系统调用接口;

在这里插入图片描述

  • 系统调用接口与库函数的关系如上图;
  • 所以,可以认为,f# 系列的函数,都是对系统调用的封装,方便二次开发。

2. 文件描述符 fd

  • 文件描述符的本质,就是数组下标!!!

2.1 0 / 1 / 2

  • Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是标准输入 0,标准输出 1,标准错误 2
  • 0,1,2 对应的物理设备一般是:键盘,显示器,显示器;
  • 所以输入输出也可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>int main()
{char buf[1024];ssize_t s = read(0, buf, sizeof(buf));if (s > 0){buf[s] = 0;write(1, buf, strlen(buf));write(2, buf, strlen(buf));}return 0;
}

在这里插入图片描述

  • 现在我们知道,文件描述符就是从 0 开始的小整数;
  • 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,于是就有了 file 结构体,表示一个已经打开的文件对象;
  • 而进程执行 open 系统调用,就必须让进程和文件关联起来;
  • 每个进程都有一个指针 *files ,指向一张表 files_struct ,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针;
  • 所以,本质上,文件描述符就是该数组的下标,只要拿着文件描述符,就可以找到对应的文件。

2.2 文件描述符的分配规则

直接看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("myfile", O_RDONLY);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

输出发现是 fd: 3

关闭 0 或者 2,再看:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(0);//close(2);int fd = open("myfile", O_RDONLY);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

发现结果是:fd: 0 或者 fd: 2

可见,文件描述符的分配规则:在 files_struct 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符,会分配给最新打开的文件。

3. 重定向

那如果关闭 1 呢?看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>int main()
{close(1);int fd = open("myfile", O_WRONLY | O_CREAT, 00644);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中 fd = 1。这种现象叫做输出重定向。

常见的重定向有:>>><

那重定向的本质是什么呢?

在这里插入图片描述

3.1 dup2 系统调用函数

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);

函数简介:

makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
将newfd设置为oldfd的副本,并在必要时先关闭newfd,但请注意以下事项:*	If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.如果oldfd不是有效的文件描述符,则调用失败,newfd不会关闭。*	If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.如果oldfd是一个有效的文件描述符,并且newfd与oldfd具有相同的值,那么dup2()什么都不做,并返回newfd。

示例代码:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("./log", O_CREAT | O_RDWR, 0644);if (fd < 0){perror("open");return 1;}close(1);dup2(fd, 1);int i = 0;for (i = 0; i < 5; i++){char buf[1024] = { 0 };ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0){perror("read");break;}printf("%s", buf);fflush(stdout);}return 0;
}
  • printf 是 C 库当中的 IO 函数,一般往 stdout 中输出,但是 stdout 底层访问文件的时候,找的还是 fd:1
  • 但此时 fd:1 下标所表示的内容已经变成了 log 的地址,不再是显示器文件的地址;
  • 所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

4. FILE 与 缓冲区

  • 因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd 访问的。
  • 所以 C 库当中的 FILE 结构体内部,必定封装了 fd。
  • 缓冲区就是一块内存区域,其存在目的是为了提升使用者的效率(用空间换时间)。
  • 我们这里说的缓冲区是语言层面的缓冲区,也就是 C 自带的缓冲区,跟内核中的缓冲区没有关系。
  • 缓冲区刷新方式:
    • 无缓冲 - 无刷新;
    • 行缓冲 - 行刷新 :写满一行才刷新,我们平时写代码经常会遇到缓冲区的问题;
    • 全缓冲 - 全部刷新:在普通文件中写入时,缓冲区被写满,才刷新!
    • 强制刷新:使用各种方法让缓冲区强制刷新,如:fflush() 函数;
    • 自动刷新:程序退出的时候会自动刷新。

来段代码研究一下:

#include <stdio.h>
#include <string.h>int main()
{const char* msg0 = "hello printf\n";const char* msg1 = "hello fwrite\n";const char* msg2 = "hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}

运行出结果:

hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢?./a.out > file ,我们发现结果变成了:

hello write
hello printf
hello fwrite
hello peintf
hello fwrite

我们发现 printffwrite(库函数)都输出了 2 次,而 write 只输出了一次(系统调用)。

为什么呢?肯定和 fork 有关:

  • 一般 C 库函数写入文件是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数会自带缓冲区(进度条例子可以说明【Linux】编写第一个小程序:进度条),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,即使是 fork 之后;
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲。

综上:printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS 也会提供相关内核级缓冲区,不过不在我们讨论范围之内。那这个缓冲区谁提供呢?printf fwrite 是库函数,writre 是系统调用,库函数在系统调用的“上层”,是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由 C 标准库提供。


END

这篇关于【Linux】文件描述符 - fd的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/833982

相关文章

在Linux中改变echo输出颜色的实现方法

《在Linux中改变echo输出颜色的实现方法》在Linux系统的命令行环境下,为了使输出信息更加清晰、突出,便于用户快速识别和区分不同类型的信息,常常需要改变echo命令的输出颜色,所以本文给大家介... 目python录在linux中改变echo输出颜色的方法技术背景实现步骤使用ANSI转义码使用tpu

linux hostname设置全过程

《linuxhostname设置全过程》:本文主要介绍linuxhostname设置全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录查询hostname设置步骤其它相关点hostid/etc/hostsEDChina编程A工具license破解注意事项总结以RHE

Linux中压缩、网络传输与系统监控工具的使用完整指南

《Linux中压缩、网络传输与系统监控工具的使用完整指南》在Linux系统管理中,压缩与传输工具是数据备份和远程协作的桥梁,而系统监控工具则是保障服务器稳定运行的眼睛,下面小编就来和大家详细介绍一下它... 目录引言一、压缩与解压:数据存储与传输的优化核心1. zip/unzip:通用压缩格式的便捷操作2.

Linux中SSH服务配置的全面指南

《Linux中SSH服务配置的全面指南》作为网络安全工程师,SSH(SecureShell)服务的安全配置是我们日常工作中不可忽视的重要环节,本文将从基础配置到高级安全加固,全面解析SSH服务的各项参... 目录概述基础配置详解端口与监听设置主机密钥配置认证机制强化禁用密码认证禁止root直接登录实现双因素

在Linux终端中统计非二进制文件行数的实现方法

《在Linux终端中统计非二进制文件行数的实现方法》在Linux系统中,有时需要统计非二进制文件(如CSV、TXT文件)的行数,而不希望手动打开文件进行查看,例如,在处理大型日志文件、数据文件时,了解... 目录在linux终端中统计非二进制文件的行数技术背景实现步骤1. 使用wc命令2. 使用grep命令

Linux如何快速检查服务器的硬件配置和性能指标

《Linux如何快速检查服务器的硬件配置和性能指标》在运维和开发工作中,我们经常需要快速检查Linux服务器的硬件配置和性能指标,本文将以CentOS为例,介绍如何通过命令行快速获取这些关键信息,... 目录引言一、查询CPU核心数编程(几C?)1. 使用 nproc(最简单)2. 使用 lscpu(详细信

linux重启命令有哪些? 7个实用的Linux系统重启命令汇总

《linux重启命令有哪些?7个实用的Linux系统重启命令汇总》Linux系统提供了多种重启命令,常用的包括shutdown-r、reboot、init6等,不同命令适用于不同场景,本文将详细... 在管理和维护 linux 服务器时,完成系统更新、故障排查或日常维护后,重启系统往往是必不可少的步骤。本文

基于Linux的ffmpeg python的关键帧抽取

《基于Linux的ffmpegpython的关键帧抽取》本文主要介绍了基于Linux的ffmpegpython的关键帧抽取,实现以按帧或时间间隔抽取关键帧,文中通过示例代码介绍的非常详细,对大家的学... 目录1.FFmpeg的环境配置1) 创建一个虚拟环境envjavascript2) ffmpeg-py

Linux脚本(shell)的使用方式

《Linux脚本(shell)的使用方式》:本文主要介绍Linux脚本(shell)的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录概述语法详解数学运算表达式Shell变量变量分类环境变量Shell内部变量自定义变量:定义、赋值自定义变量:引用、修改、删

Linux链表操作方式

《Linux链表操作方式》:本文主要介绍Linux链表操作方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、链表基础概念与内核链表优势二、内核链表结构与宏解析三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势六、典型应用场景七、调试技巧与