[基础IO]文件描述符{C库函数\系统接口\初识fd}

文章目录

1.基础知识

1.1对文件的认识

  1. 文件 = 内容 + 属性(也是数据)
  2. 文件的所有操作: a. 对内容 b. 对属性 对内容操作可能引起属性操作 如: 增加内容 文件变大 标识文件大小的属性变化
  3. 文件在磁盘上存放 磁盘是硬件 用户访问文件: 代码--编译---可执行程序---运行---进程通过OS提供的系统调用接口访问文件
  4. 向磁盘(硬件)写入内容 只有OS有权利 普通用户要想写入 只能使用OS提供的文件类系统调用接口

1.2对系统调用接口的认识

  1. 要想用系统调用接口 首先得了解怎么用 及使用者需要对系统有一定的认识与了解 而系统调用接口使用较难或者说系统太难理解 学习计算机的同学都是从易到难学习的 C/C++/java等等 或者说先是学习较简单的语言 然后再学例如计算机组成原理/编译原理/操作系统等较难课程 而从计算机发展历史看 实际上是先有的计算机组成/OS 而后才有的语言 但是我们不得不从语言开始学 因为一开始就学OS根本就学不懂 这就好比我们先学语言 再学数据结构一样 我们首先需要具备一定的语言知识 才能更好的学习底层 其次 底层配合语言让我们对计算机的理解更加深刻
  2. 为了让初学者去使用接口 或者说让这些系统调用接口能够被更好的使用 在接口上做了封装 形成各类语言的库函数 学习这些语言的人就能调用库函数来间接实现对接口的调用
  3. 不同的语言有不同的风格和体系 这也造成了不同语言的文件函数使用上具有很大差异 但是底层都是封装的系统接口
  4. 而你使用的OS基本上只有一个 基于此OS的系统调用接口 只有一套 如果我们学习了本质即底层 那么学习其他语言的接口将会是如鱼得水
  5. 上面为我们讲到 语言对系统调用接口封装形成库函数一大目的是更好更方便更简单的使用系统调用接口 那么还有第二大目的就是实现语言的跨平台性 如果语言不提供对文件的系统调用接口的封装 那么用该语言写出的代码一旦涉及到访问文件的操作都必须/只能调用系统调用接口 而在linux下写的语言就必须调用Linux的接口 这份语言 到windows下 由于Windows没有linux的接口或者说二者根本就无法用统一接口调用对应操作 使得这份语言不具备跨平台性

语言是如何实现跨平台的?

把基于所有平台(windows/Linux/MacOS)的代码都实现一遍条件编译 动态裁剪 在什么机器上跑 就用对应的封装

对显示器的认识

  1. 显示器是硬件 pintf/cout向显示器打印 本质上也是向硬件写入
  2. 磁盘我们无法向显示器一样直观看到 所以向显示器打印内容时 我们不觉得奇怪 因为一眼就能看到
  3. 向显示器打印和磁盘上文件的写入本质没有区别

1.3如何理解LInux下一切皆文件?

在Linux中,一切皆文件的原则是指Linux将所有的资源都视为文件或文件类型的一种。这包括硬件设备、目录、文件、进程、套接字等等。在Linux中,每个文件都有一个唯一的文件描述符,它是一个非负整数,用于标识打开的文件。通过文件描述符,我们可以对文件进行读写等操作。这种一切皆文件的设计理念使得Linux系统的操作变得非常简单和统一,因为所有的资源都可以通过相同的方式进行访问和操作。同时,这种设计理念也为Linux系统提供了很好的可扩展性和灵活性,因为新的设备和资源可以很容易地被添加到系统中,并且可以通过相同的方式进行管理和访问。

举个例子,如果你想要查看CPU的温度,你可以在Linux中打开一个文件,该文件位于/sys/class/thermal/thermal_zone0/temp,然后读取该文件的内容,就可以得到CPU的温度信息。这个文件就是一个虚拟文件,它代表了CPU的温度传感器,通过读取该文件的内容,我们就可以获取到CPU的温度信息。

另外,Linux中的文件类型也非常丰富,包括普通文件、目录文件、字符设备文件、块设备文件、套接字文件等等。每种文件类型都有自己的特点和用途,例如普通文件用于存储文本、二进制数据等,目录文件用于存储其他文件和目录等。理解Linux中的文件类型对于正确地使用和管理Linux系统非常重要。

如何理解文件?

  1. 最初我们认识到的文件是从语言级别认识的 例如C语言调用fopen/fread/fwrite等函数可以对文件进行打开关闭读写操作
  2. 后来我么认识并了解到 显示器和键盘也具有文件的一些属性如 读和写 printf/cout向显示器打印内容 实际上就是一种write scanf/cin从键盘获取数据 实际上就是一种read 至于为什么从键盘输入的内容能显示到显示器上 这实际上是一种回显 这些调用库函数实现对文件的读写操作 是站在程序的角度 而程序要想能够执行这些操作 首先他得运行成为进程 即代码和数据加载到内存 即以上这些过程 是站在内存的角度 通过进程来完成的
  3. 从文件中读数据到文件中的流程: 进程调用fopen/fread(Input)函数从A文件中读入数据到进程的内部(内存) 然后调用fwrite(Outpute)函数写到B文件中
  4. 由此我们得出文件的概念: 站在系统的角度 能够被input读取或者能够被output写出的设备 就叫做文件
  5. 之前认识的文件是文件的子集,是狭义上的文件即磁盘上的文件
  6. 广义上的文件如显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设,都可以称之为文件

2.C语言的库函数

2.1FILE *fopen(const char *path, const char *mode);

mode的不同作用和目的

r: 为了读打开一个文件 文件流定位在文件首 原文件不存在则报错

w: 为了写打开一个文件 清空文件原有内容 文件不存在创建一个新文件 文件流定位在文件首

a: 为了追加 打开一个文件 文件不存在创建一个新文件 文件流定位在文件尾

(r+ w+ a+ : 为了读和写打开文件 其他对应一致)

2.2对fopen()的mode的w/a的深层认识

w/a 打开不存在的文件时 会创建一个新文件 这个文件创建在哪里?

  1. 当前路径
  2. 准确一点是源代码对应的可执行程序所在的路径
  3. 精确一点是Linux下源代码对应的可执行程序运行变为进程所处的工作路径 (Windows-VS下的源代码和可执行程序不在同一路径 有所区别)
  4. fopen()会将不存在文件名建立在 "当前路径" 下 以此作为新文件的路径

谁创建的?

源代码对应的可执行程序运行起来成为进程后进程调用系统接口创建的 ==> OS创建的!

ls /proc/pid -l.会显示什么?

  1. cwd: current work directory 当前进程所执行的程序的所在目录
  2. exe: 当前进程所执行的可执行程序路径+文件名
    即 当一个进程运行时 会记录自己当前所处的工作路径 可执行程序的路径变化 对应的进程的工作路径也会变化
    继续回答上面的问题 当前路径指的是什么?. 一个进程运行时 当前所处的工作路径

2.3fclose()

2.4size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

回顾之前的字符串函数

2.5put()系列 -- fputs()

#include <stdio.h>

int fputc(int c, FILE *stream);

int fputs(const char *s, FILE *stream);

int putc(int c, FILE *stream);

int putchar(int c);

int puts(const char *s);

2.6get()系列 -- fgets()

#include <stdio.h>

int fgetc(FILE *stream);

char *fgets(char *s, int size, FILE *stream);

int getc(FILE *stream);

int getchar(void);

char *gets(char *s);

int ungetc(int c, FILE *stream);

2.7C语言文件库函数

1.用C语言将字符串写到文件里 计算字符串长度时strlen()函数用+1吗?

  1. 在C语言里字符串末尾有一个标识字符串结尾的标识符叫\0 它不属于字符串的内容 \0是C语言自己的规定
  2. 用C语言将字符串写到文件后 文件上的字符串与C语言再无瓜葛 文件要保存的是数据 不用也压根没必要遵守某种语言的规定 所以答案是不用+1
  3. 如果+1 文件内容会出现某一处乱码 因为文件无法对\0做有效解读

那文件里的字符串没有\0读的时候怎么分辨?\n怎么分辨?

读是用语言读的 语言级别上读的时候识别到后加上就行了 这就是系统与语言的区别

命令行下模拟w操作

echo hello txt > test.txt//向文件写内容.
> test.txt//输出重定向: 先打开文件 清空内容 又什么都不写 模拟完成.

实际上 echo命令 底层就是C语言调用fopen函数执行w操作完成的

按行读取char *fgets(char *s, int size, FILE *stream);

会自动在读到的字符串尾部+\0

C语言程序执行后三个标准输入输出流

FILE* : 把硬件看成了文件

extern FILE *stdin; --键盘

extern FILE *stdout; --显示器

extern FILE *stderr; --显示器

2.代码理解

c 复制代码
int main(int argc, char *argv[])
{
    if(argc != 2) 
    {
        printf("argv error!\n");
        return 1;
    }

    //FILE* fopen(const char* path, const char* mode);
    FILE* fp = fopen(argv[1], "r"); /* ./myfile test.txt */
    //./mycat log.txt 模拟实现输出文件内容
    if(fp == NULL)
    {
        perror("fopen");
        return 2;
    }

    //按行读取
    char line[64];
    //char* fgets(char* s, int size, FILE * stream);  
    // fgetstring -- fgets 自动在所读取的字符结尾添加\0
    while(fgets(line, sizeof(line), fp) != NULL)
    {
        //printf("%s", line);
        fprintf(stdout, "%s", line);
    }

    //文件操作
    //size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE* stream);
    //const char *s1 = "hello file\n"; 
    //fwrite(s1, strlen(s1), 1, fp);

    //const char *s2 = "hello fprintf\n";
    //fprintf(fp, "%s", s2);

    //const char *s3 = "hello fputs\n";
    //fputs(s3, fp); int fputs(const char *s, FILE *stream);

    fclose(fp);

    return 0;
}

3.系统调用接口

C库函数: fopen fclose fread fwrite

系统接口: open close read write

3.1如何用函数传递标志位 -- 数据结构之位图

应用层上一个简单的动作,在系统接口层面/OS层面,要做非常多的操作

c 复制代码
//用比特位 标识状态
#define ONE 0x1   //0000 0001
#define TWO 0x2   //0000 0010
#define THREE 0x4 //0000 0100

void show(int flags) 
{
    if(flags & ONE)    //0000 0001
        printf("hello one\n");
    if(flags & TWO)    //0000 0010
        printf("hello two\n");
    if(flags & THREE)  //0000 0100
        printf("hello three\n");
}

int main()
{
    show(ONE); //000 0001
    printf("-----------------------------------------\n");

    show(TWO); //0000 0010
    printf("-----------------------------------------\n");

    show(ONE | TWO);  //000 0001 | 0000 0010 == 0000 0011
    printf("-----------------------------------------\n");
   
    show(ONE | THREE);
    printf("-----------------------------------------\n");

    show(ONE | TWO | THREE); //000 0001 | 0000 0010 | 0000 0100 == 0000 0111
    printf("-----------------------------------------\n");

    return 0;
}

fd: file descriptor 文件描述符 为什么创建一个文件从3开始? 0 1 2 呢?

3.2FILE 是什么?

  1. FILE是个C标准库提供的结构体 内部有很多成员 其中就封装了fd
  2. C库函数底层不一定都封装了系统接口 C关于文件的库函数底层都调用了系统接口 系统角度认识fd 但不认识C的FILE

3.3fd是什么?

  1. 进程要访问文件 必须先打开文件
  2. 一个进程可以打开多个文件 进程 : 文件 == 1 : n
  3. 文件被进程直接访问的前提是加载到内存中
  4. 一个进程有可以打开多个文件 多个进程会打开超多文件 此时系统中会存在大量的被打开的文件 OS要把如此多的文件管理起来 ==> 先描述再组织
  5. 在内核中 fd本质是一个数组下标

3.4内核中如何看待被打开的文件?

OS内部为管理每一个被打开的文件 为每一个被打开的文件构建一个file结构体对象 充当被打开的文件 然后把这些文件对象用双链表组织起来

c 复制代码
struct FILE
{
	struct file* prev;
	struct file* next;
	//包含一个被打开的文件的几乎所有内容(权限,缓冲区,链接信息,属性等)
}

3.5ssize_t read(int fd, void *buf, size_t count);

  1. 从文件描述符fd向buf缓冲区读取最多count个字节
  2. 读取成功返回所读的字节数
  3. 读取错误返回-1

3.6int fprintf(FILE * stream, const char* format, ...);

3.7ssize_t write(int fd, const void *buf, size_t count);`

3.8int open(const char *pathname, int flags, mode_t mode);`



3.9代码理解

c 复制代码
int main()
{
    //int open(const char* pathname, int flags, mode_t mode);
    
    //不被系统的umask掩码影响 设置umask掩码为0 (就近)
    //目的是调用open()接口时 传的mode直接作为文件的权限
    umask(0);

    //只读
    // int fd = open("log.txt", O_RDONLY);
  
    //只写 目标文件不存在需创建新文件 每次打开清空原有内容  fopen("log.txt", "w");
    //int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //rw-rw-rw-
    //if(fd < 0)
    //{
    //    perror("open");
    //    return 1;
    //}
    //char buffer[64];
    //memset(buffer, '\0', sizeof(buffer));
    // 
    //  fread()调用read时会执行一个操作: 
    // 在所读到的字符串结尾 + \0 
    // 而系统接口read()不会 read是按字节数读的 
    // 所以将buf数组全部置成\0 可以使得读完后最后有一个\0
    // 
    //read(fd, buffer, sizeof(buffer));  //ssize_t read(int fd, void *buf, size_t count);
    //printf("%s", buffer);
   
    //const char *s = "hello write\n";
    //const char *s = "hi\n";
    //write(fd, s, strlen(s)); //ssize_t write(int fd, const void *buf, size_t count);
    
    //只写 目标文件不存在需创建新文件 追加写入  fopen("log.txt", "a");
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 
    printf("open success, fd: %d\n", fd1);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    printf("open success, fd: %d\n", fd2);
    int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    printf("open success, fd: %d\n", fd3);
    int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    printf("open success, fd: %d\n", fd4);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);

    return 0;
}

int main()
{
    char input[16] = { 0 };
    ssize_t s = read(0, input, sizeof(input));
    if (s > 0)
    {
        input[s] = '\0';
        printf("%s\n", input);
    }

    int a = 0;
    fscanf(stdin, "%d", &a);  //scanf
    fprintf(stdout, "%d\n", a); //printf

    //ssize_t write(int fd, const void *buf, size_t count);
    const char* s = "hello stdout(1)\n";
    write(1, s, strlen(s));
    return 0;
}
相关推荐
FFDUST几秒前
C++ 优先算法 —— 无重复字符的最长子串(滑动窗口)
c语言·c++·算法·leetcode
shiming88791 分钟前
C/C++链接数据库(MySQL)超级详细指南
c语言·数据库·c++
前端白袍2 分钟前
C语言:C语言实现对MySQL数据库表增删改查功能
c语言·数据库·mysql
scoone3 分钟前
解决Ubuntu 22.04系统中网络Ping问题的方法
linux·网络
与君共勉1213811 分钟前
Jenkins-Ansible 插件相关用法
linux·运维·ansible·jenkins
m0_5195231014 分钟前
Linux——环境变量
linux·运维·服务器
ShangCode16 分钟前
Linux系统三剑客之AWK
linux·运维·awk
robin_suli17 分钟前
Java多线程八股(三)一>多线程环境使用哈希表和ArrayList
java·开发语言·多线程·哈希表
@无名用户17 分钟前
Linux进程与资源管理
linux·运维·服务器
NiNg_1_23421 分钟前
Java中的多线程
java·开发语言