> 技术文档 > 【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)


小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述


目录

    • 前言
    • 一、共识原理
    • 二、c语言文件接口
      • fopen fclose
      • fwrite
      • fprintf
      • >输出重定向 >>追加重定向
    • 三、文件系统调用
      • 过渡到系统
      • 比特位方式传递标志位
      • open close
      • write
      • 得出结论
      • 使用一下stdin,stdout,stderr对应的文件描述符0,1,2
      • read
    • 四、访问文件的本质
    • 总结

前言

【linux】自定义shell——bash命令行解释器小程序——书接上文 详情请点击<——
本文由小编为大家介绍——【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)


一、共识原理

在正式讲解基础IO前,关于文件,我们已经有了一些共识性的知识

  1. 文件 = 内容 + 属性

  2. 文件分为打开的文件和没打开的文件

  3. 对于打开的文件:谁来打开?其实是由进程打开,本质是研究进程和文件的关系

  4. 对于没打开的文件:是放在哪里?其实是放在磁盘上。我们最关注的是什么问题?其实也就是是文件的存储问题,即没有被打开的文件非常多,那么文件如何被分门别类的放置好?文件被分门别类的放置好后,进而我们就可以快速的对文件进行增删查改,也就是快速的找到文件了
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  5. 文件被打开,那么文件就必须要先被加载到内存,在进程被启动的时候,操作系统要默认为我们打开三个文件流,stdin,stdout,stderror,那么此时一个进程中就有多个被打开的文件,所以进程和文件的关系一定是一对多的关系,即进程:打开的文件 = 1:n

  6. 所以在操作系统内部,一定有大量被打开的文件,操作系统肯定要对这些打开的文件进行管理,那么如何管理?六字真言来了——先描述再组织,在内核中,一个被打开的文件,必须有自己的文件打开对象,并且这个文件打开对象其中必定包含了文件的很多属性。struct xxxx{文件属性; struct xxxx* next},并且要对这些文件打开对象进行组织,所以这些文件打开对象内势必也会有指向下一个文件对象的指针,这些文件打开对象最终都要以双链表链接起来,这样对文件的管理就转化为了对双链表的增删查改

  7. 本文小编研究的目标是打开的文件

二、c语言文件接口

fopen fclose

本文会对c语言文件接口进行部分讲解便于本文引出后续的文件系统调用,在之前的文章中,小编已经对c语言文件接口进行详细的讲解,如果想学习全部的c语言文件接口感兴趣的请点击后方蓝字拦截进行学习——【c语言】c语言文件操作1fputc 2fgetc 3fputs 4fgets 5fprintf 6fscanf 7fwrite 8fread (超详细)详情请点击<——

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. fopen可以打开一个文件,对于它的返回值,如果打开失败那么会返回NULL指针,如果打开成功会返回一个FILE*的指针对象,其中这个指针指向的是一个结构体,我们可以使用这个指针对象对文件进行操作,打开文件方式有r只读,w,w+,a,a,本文仅研究w写,a追加写这两种的方式
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
  2. 文件打开,我们对文件进行操作后要使用fclose对文件及时进行关闭,否则就会造成文件打开长时间占用内存资源,造成资源泄漏
  3. 那么我们首先使用fopen以写w的方式打开一个不存在的文件log.txt
#include int main(){ FILE* fp = fopen(\"log.txt\", \"w\"); if(fp == NULL) { perror(\"fopen\"); return 1; } fclose(fp); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 先查看我们的目录,当前目录下并没有log.txt文件,当我们运行之后目录下有了log.txt文件了
  2. 说明当我们使用fopen以w的方式打开文件,如果当前目录下没有该文件,那么就会在当前目录下创建这个文件
  1. 当前路径是什么?当前路径是当前进程的工作路径,即进程的cwd,那么我更改一下当前进程的cwd是不是就可以将文件新建到其它目录呢?下面我们来验证一下
  2. 同样的,在上面代码的基础上,小编打印出当前进程的pid,并且关闭文件之后让进程休眠1000秒,便于我们查看进程的当前工作目录
#include #include #include int main(){ FILE* fp = fopen(\"log.txt\", \"w\"); if(fp == NULL) { perror(\"fopen\"); return 1; } printf(\"i am process, pid: %d\\n\", getpid()); fclose(fp); sleep(1000); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 在根目录/的proc目录下,会包含有当前正在运行的进程pid,使用ls -l查看进程的pid里面会有进程的各项属性信息,其中就有cwd当前进程的工作目录

  2. 而我们当前左边的进程就在休眠式的运行,并且左边进程的pid已经打印出来了,所以我们就可以在右边的根目录/的proc目录中找到我们左边的pid查看进程的各项属性信息,其中就有进程的cwd,即进程的工作目录

  3. 所以我们仅仅使用一个fclose以w的方式打开不存在的文件,此时就会在当前目录创建一个文件,操作系统怎么知道要给我们放在哪里?为什么是当前进程的工作目录,为什么不能是其它目录,就是因为有进程当前的工作目录cwd的存在,当我们使用一个fclose以w的方式打开不存在的文件的时候,当没有写文件的绝对路径的时候,操作系统就会默认将进程的cwd当前的工作目录和要创建的文件名进行拼接,作为创建文件的路径

  4. 如果我们使用了绝对路径打开不存在的文件,那么此时操作系统就不会将当前进程的工作目录和文件名进行拼接,而是在我们指定的绝对路径下创建文件
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  5. 所以当我们退出当前进程的时候,由于我们没有使用绝对路径创建文件,而是仅仅使用文件名创建文件,所以操作系统就会将当前进程的工作目录和文件名进行拼接,所以创建的文件就会默认在当前进程的工作目录了

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 那有的读者友友可能会问了,小编小编,那你可不可以将当前进程的工作目录改掉,让新创建的文件创建到更改后的进程的工作目录下呢?可以的,我们学过一个系统调用chdir可以更改当前进程的工作目录,那么下面小编就演示一下
  2. 由于小编使用的是普通用户,由于权限的问题,操作系统不会允许,同时我们也不能将这个进程工作目录更改到任意目录,否则会出现问题,但是小编却可以将进程的工作目录更改到当前普通用户的家目录下即/home/wzx,因为我普通用户可以将进程的工作路径更改到当前普通用户的家目录的任意路径下,即/home/wzx这个路径下
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
#include #include #include int main(){ chdir(\"/home/wzx\"); FILE* fp = fopen(\"log.txt\", \"w\"); if(fp == NULL) { perror(\"fopen\"); return 1; } printf(\"i am process, pid: %d\\n\", getpid()); fclose(fp); sleep(1000); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 在运行进程前,无论是当前进程的工作目录(左边),还是即将要更改后的进程的工作目录(右边),此时都没有新建的文件log.txt

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 那么此时我们将进程运行起来,进程运行起来之后,chidr会将运行起来的进程的工作目录更改至我们指定的当前普通用户的家目录下,此时我们在右边进行查看,确实当前进程的工作目录被修改到了当前普通用户的家目录/home/wzx了

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 那么此时我们在右边普通用户的家目录下使用ls -l查看目录下的文件,出现了!新建文件真的就出现在了小编使用chdir将进程修改后的工作目录/home/wzx了,此时我们终止左边的进程,查看左边进程是否会出现新创建的文件

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 此时左边就没有出现新创建的文件了
  1. 所以如果我更改了当前进程的工作目录cwd,使用fopen以w的方式打开一个没有使用绝对路径的不存在的文件,此时就会将文件创建在更改后的工作目录

fwrite

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 当文件是以w写,a追加写的方式使用fopen打开的时候,fwrite可以向文件中写入内容,一次传入写入的字符串,字符串的大小(可以使用strlen进行计算),字符串的份数,以及文件流
  2. 我们知道,在c语言中字符串是包含结尾的\\0的,所以在fwrite传入写入字符串的大小的时候,要不要将计算后的strlen进行加1将\\0也算进去呢?其实是不需要的,因为字符串以\\0结尾,是你c语言的规定,和我文件有什么关系。在c语言中由于只有字符这个类型,没有字符串这个类型,所以在内存中为了区分字符和字符串,c语言规定字符串必须以\\0进行结尾,但是在文件中,文件中是有字节流的,文件中没有这样的规定,文件可能被多种语言进行读取,文件中的字节流内容需要是普适性的可以被任何语言进行读取的,难道其它语言也都是诸如以\\0结尾的吗?所以字符串以\\0结尾是c语言的规定,在c语言中需要遵循这样的规定,但是使用fwrite将字节流写入文件后,就不需要遵循以\\0进行结尾,所以当将字符串写入文件中的时候,只需要将字符串的内容写入文件即可,不需要将\\0也写入文件
  3. 那么下面我们先以w的方式fopen打开文件,接下来使用一下这个fwrite接口向文件中写入一些信息
#include #include #include #include int main(){ FILE* fp = fopen(\"log.txt\", \"w\"); if(fp == NULL) { perror(\"fopen\"); return 1; } const char* message = \"aaa\\n\"; fwrite(message, strlen(message), 1, fp); fclose(fp); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时已经成功的hello linux写入到log.txt文件中了

  1. 那么下面同样以w的方式fopen打开文件,使用fwrite接口向文件中写入不同的信息
#include #include #include #include int main(){ FILE* fp = fopen(\"log.txt\", \"w\"); if(fp == NULL) { perror(\"fopen\"); return 1; } const char* message = \"aaa\\n\"; fwrite(message, strlen(message), 1, fp); fclose(fp); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时我们看到原本的hello linux在我们运行程序后,已经修改为aaa,并且这个aaa不是在hello linux后面追加写为hello linuxaaa,也不是类似于覆盖式的开头写为aaalo linux,而是直接为aaa,其实这是由于当我们以w的方式fopen打开文件的时候,在进行写入前,文件中的内容就已经被清空了,即w是清空并且从头写,所以才会出现aaa

  1. 那么接下来我们看一下以a方式fopen打开会发生什么
#include #include #include #include int main(){ FILE* fp = fopen(\"log.txt\", \"a\"); if(fp == NULL) { perror(\"fopen\"); return 1; } const char* message = \"rrr\\n\"; fwrite(message, strlen(message), 1, fp); fclose(fp); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
当我们以a方式fopen打开文件进行写入的时候,此时log.txt中原本的aaa\\n不会被清空,并且也不会被覆盖,而是会在后面进行追加式的写入,所以w/a都是写入,w是清空并从头写,a是在文件的结尾,追加写

fprintf

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. fprintf是向文件流中格式化输入,fprintf的使用方法和printf类似,只不过fprintf的输入文件是我们显示指定的文件流,printf输入的文件是显示屏文件
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
  2. linux下一切皆文件,c语言程序在启动的时候,默认会为我们打开三个输入输出流(文件),其中stdin是键盘文件(c++中是cin),stdout是显示文件(c++中是cout),stderr是显示器文件(c++中是cerr),所以我们就可以使用fprintf向显示器文件stdout以及stderr中输入,看显示器是否会进行显示
#include #include #include #include int main(){ FILE* fp = fopen(\"log.txt\", \"w\"); if(fp == NULL) { perror(\"fopen\"); return 1; } const char* message = \"rrr\\n\"; fprintf(stdout, \"%s\", message); fprintf(stderr, \"%s\", message); fclose(fp); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
我们可以看到信息确实打印在显示器文件上了,我们并没有类似于fp打开这两个stdout和stderr文件流,但是却可以直接进行使用,说明c语言程序在启动的时候确实是默认会为我们打开三个输入输出流(文件)

>输出重定向 >>追加重定向

  1. 那么我们现在回过头看输出重定向>,它可以将输出重定向到文件中,如果文件不存在它会在当前工作路径下创建一个文件

  2. echo可以打印字符串,那么此时我们就可以尝试将它打印的字符串输出重定向到一个不存在的文件中
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  3. 所以这个输出重定向要先打开一个文件,如果文件不存在,那么就要在当前路径下创建这个文件,之后就要将信息重定向输出到文件中,并且观察上面小编第二个输出重定向aaa,此时文件的内容会先清空,再向文件中输出aaa,是不是类似于上面小编讲解的以w方式fclose打开一个文件,使用write向文件中写入

  4. 同时还有追击重定向>>,此时我们回过头来看这个追加重定向,那么如果我们要追加重定向的文件不存在,这个追加重定向要先在当前路径下创建这个文件,之后再向这个文件的结尾,再追加写入
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  5. 所以同样的,这个追加重定向>>同样要打开一个文件,并且是要以a的方式打开,当文件不存在的时候,要先在当前路径下创建这个文件,之后在文件的结尾追加写入,是不是类似于上面小编讲解的以a方式fclose打开一个文件,使用write向文件中写入

  6. 所以此时我们再看 >输出重定向 >>追加重定向就没有什么神秘的了

三、文件系统调用

过渡到系统

  1. 文件其实是在磁盘上的,磁盘是硬件设备,所以访问磁盘文件其实就是访问硬件

用户
程序 std lib <——c/c++
系统调用
操作系统
硬件驱动
硬件

  1. 我们知道要访问硬件,就要访问硬件驱动,要访问硬件驱动就要先访问操作系统,由于操作系统不相信任何人,所以操作系统会对外提供系统调用,任何程序(库函数)想要访问硬件只能通过系统调用自上而下的贯穿操作系统体系访问硬件
  2. 所以诸如printf/fprintf/fscanf/fwrite/fread/fgets/gets等库函数都要访问硬件,所以它们底层必定封装了某种文件系统调用,下面小编就来带领大家学习一下文件系统调用

比特位方式传递标志位

  1. 由于open需要使用到比特位方式传递标志位的方式,所以小编在这里先给读者友友铺垫一下如何使用比特位方式传递标志位
  2. 我们知道一个整数有32个比特位,当我们需要传递标志位的时候,仅仅是0,1就可以进行传递,如果我们想要传递5个,甚至10个标志位的时候,难道要在函数参数中,设置10个整数的参数用于传递标志位吗?这未免太过麻烦了,我们其实仅仅传入一个整数使用位运算的方式进行传递标志位即可
  3. 对于要标志位,那么我们使用宏定义,使用1进行左移<<的位运算,这样最多可以达到设置32个标志位的效果,这样就保证了标志位一定不会冲突,这里小编设置5个标志位便于讲解
  4. 在show函数内部,通过位运算按位与&,将传入的整数flags与设置的标志位进行逐个式的判断,如果成立,那么就去执行对应的功能
  5. 在main函数传参调用这个show函数,通过位运算按位或|,将要进行传参的标志位按位或在一起形成一个整数进行传参,这样这个整数内部的32位的比特位上,对于我们想要传参的标志位对应的比特位上都为1
  6. 所以这样就实现了比特位方式传递多个标志位
#include #include #include #include #define ONE (1 << 0) //1#define TWO (1 << 1) //2#define THREE (1 << 2) //4#define FOUR (1 << 3) //8#define FIVE (1 << 4) //16void show(int flags){ if(flags & ONE) printf(\"function1\\n\"); if(flags & TWO) printf(\"function2\\n\"); if(flags & THREE) printf(\"function3\\n\"); if(flags & FOUR) printf(\"function4\\n\"); if(flags & FIVE) printf(\"function5\\n\"); printf(\"\\n\");}int main(){ show(ONE | FIVE); show(TWO | THREE | FOUR); show(ONE | TWO | THREE | FOUR | FIVE); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

open close

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. open是在2号手册,open是一个文件系统调用,open的返回值是一个整数,这个整数其实就是文件描述符fd,open可以通过传入不同的标志位的方式打开一个文件,对于open有两个函数,参数个数不同,前两个参数第一个是文件路径,第二个是标志位,对于第三个表示创建文件的默认权限,这个标志位其实也就宏,可以通过位运算按位与 | 的方式传入多个标志位,原理上面小编已经在比特位方式传递标志位中讲解过了,下面使用红色框框出来的是我们比较常用的一些标志位
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
  2. O_RDONLY 是只读,O_WRONLY 是只写(O_RDONLY 和 O_WRONLY 和O_RDWR可读可写,这三个标志位传入的时候只能传入一个),O_CREAT 是文件不存在则在当前工作路径下创建文件,O_TRUNC 是将文件内容截取为0,也就是打开文件后清除文件内容,O_APPEND 是打开文件后从文件末尾开始追加式写入
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
  3. close是一个文件系统调用,close传入一个文件描述符fd后,它关闭文件描述符对应的文件
  4. 那么接下来小编就会逐步演示如何正确的创建文件,以及打开文件,那么我们先使用open两个参数的函数,看是否可以打开一个不存在的文件,那么小编使用库函数的方式,以只写O_WRONLY的方式打开一个不存在的文件,看是系统否可以为我们在当前工作目录下创建一个文件
#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
创建失败,所以库函数的方式是不可以正确的调用文件系统接口的

  1. 那么有的读者友友可能会记得,小编小编,我记得你还介绍了O_CREAT,你把这个选项通过按位与 | 的方式也带上,看看是否可以打开一个不存在的文件?好,那么接下来小编尝试一下
#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY | O_CREAT); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
确实创建出来了,但是但是但是,观察一下,这个log.txt的权限和普通文件的权限不一样,这就是个坑了

  1. 其实使用两个参数的open不能够正确的创建文件,因为使用文件系统调用创建文件的时候,需要指定创建文件的默认权限,仅仅是两个参数的open无法指定创建文件的默认权限,应该使用三个参数的open,三个参数的open,使用第三个参数可以指定创建文件的权限,默认创建文件的权限是0666,经过将umask文件掩码0002去掉之后变成0664
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

关于umask文件掩码的计算以及使用更多详情请点击<——

#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY | O_CREAT, 0666); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时使用三个参数的open就可以创建一个普通文件了

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 小编小编,如果我非要将文件权限设置为0666呢?有方法的,那么umask不仅可以查看文件掩码,而且也可以修改文件掩码,我们将文件掩码修改为0之后,那么新创建的文件的权限就为0666了
#include #include #include #include #include #include int main(){ umask(0); int fd = open(\"log.txt\", O_WRONLY | O_CREAT, 0666); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时权限为0666的文件就被创建出来了

write

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 那么接下来就是向文件中写入了,此时我们可以使用到文件系统调用write,依次传入文件描述符fd,信息(字符串),信息的大小(字符串的大小)即可向文件中写入
#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY | O_CREAT, 0666); const char* message = \"hello linux\\n\"; write(fd, message, strlen(message)); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时我们向文件中写入成功

  1. 可是现在我们真的实现了类似于以w方式fopen打开文件的方式了吗?实则不然,下面小编修改一下写入信息,我们再来观察一下现象
#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY | O_CREAT, 0666); const char* message = \"aaa\"; write(fd, message, strlen(message)); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时运行程序之后,原本文件的内容并没有进行清空,而是直接在文件开头覆盖式的去写,那么并不能达到类似于以w方式fopen打开文件的方式

  1. 那么如果想要打开文件前对文件内容进行清空应该再使用位运算按位与 | 上宏O_TRUNC即可
#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY | O_CREAT | O_TRUNC, 0666); const char* message = \"aaa\\n\"; write(fd, message, strlen(message)); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时就可以达到和c语言中的以w方式fopen打开文件的方式

  1. 那么接下来我们尝试一下使用文件系统调用模拟实现c语言中的a方式fopen打开文件的方式,我们知道a方式fopen对于打开不存在的文件会在当前路径下创建一个文件,并且也是对文件进行写入,并且在文件结尾进行追加式的写入
  2. 所以我们在原来的基础上将宏O_TRUNC清空修改为宏O_APPEND在结尾追加即可
#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY | O_CREAT | O_APPEND, 0666); const char* message = \"bbb\\n\"; write(fd, message, strlen(message)); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时就达到了使用文件系统调用模拟实现c语言中的a方式fopen打开文件的方式,当文件不存在的时候,在当前工作路径下创建一个文件,并且写入的时候在文件的结尾,追加式的写入

得出结论

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 所以根据上面,我们可以推断出,对于FILE这个struct结构体中必定有封装的文件描述符fd,因为任何语言,包括c语言都是建立在操作系统上面的,建立在系统调用上面的,所以c语言必须要顺从操作系统的规则进行调用,所以c语言中fopen的返回值 FILE* 指向的FILE这个struct结构体中必定有封装的文件描述符fd
  2. 那么下面我们先来查看一下文件系统调用open的返回值,即文件描述符fd的值是多少
#include #include #include #include #include #include int main(){ int fd = open(\"log.txt\", O_WRONLY | O_CREAT | O_APPEND, 0666); printf(\"fd: %d\\n\", fd); const char* message = \"bbb\\n\"; write(fd, message, strlen(message)); close(fd); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
嘶,我们在进程内打开的这个文件的返回值,即文件描述符fd的值居然是3

  1. 下面小编再多打开几个文件,看看其返回的文件描述符fd的值是多少
#include #include #include #include #include #include int main(){ int fd1 = open(\"log1.txt\", O_WRONLY | O_CREAT | O_APPEND, 0666); int fd2 = open(\"log2.txt\", O_WRONLY | O_CREAT | O_APPEND, 0666); int fd3 = open(\"log3.txt\", O_WRONLY | O_CREAT | O_APPEND, 0666); int fd4 = open(\"log4.txt\", O_WRONLY | O_CREAT | O_APPEND, 0666); int fd5 = open(\"log5.txt\", O_WRONLY | O_CREAT | O_APPEND, 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;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
3,4,5,6,7这个顺序很类似于数组下标

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 那么前三个呢?即0,1,2呢?相信聪明的读者友友已经想到了问题的答案,也就是c语言会默认帮我们打开三个文件流,分别是stdin,stdout,stderr,这三个文件流分别对应0,1,2
  2. 当然这也仅仅是我们的猜测,这三个文件流的的类型是FILE*,我们知道FILE是一个c库定义的一个struct结构体,而这三个文件流的类型就是结构体指针,所以我们可以使用箭头->去访问它的成员,由于FILE这个结构体必然封装了文件描述符fd,所以我们必然可以使用箭头->去访问到它内部封装的文件描述符fd,其中这个文件描述fd对应的是一个_fileno的成员
#include #include #include #include #include #include int main(){ printf(\"stdin->fd: %d\\n\", stdin->_fileno); printf(\"stdout->fd: %d\\n\", stdout->_fileno); printf(\"stderr->fd: %d\\n\", stderr->_fileno); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 通过打印结果我们可以看出,噢,原来前三个0,1,2对应的真的是stdin,stdout,stderr这三个文件流
  2. 之前小编将:c程序默认在启动的时候,会为我们打开三个输入输出流(文件),也就是stdin(键盘文件),stdout(显示器文件),stderr(显示器文件)这三个文件流,但是这是c语言程序的特性吗?
  3. 不是的,这是操作系统的特性,我们想一下,当我们的计算机启动之后,我们电脑的显示器以及键盘就已经就绪了,即可以使用了,因为这也是我们使用电脑所必须的,日常进行编程的时候程序员也必须依赖并使用显示器和电脑,所以操作系统在电脑启动的时候才要为我们准备好键盘,显示器,而c语言程序运行起来之后是进程,进程跑到操作系统之上,所以进程默认才会打开键盘,显示器,显示器

使用一下stdin,stdout,stderr对应的文件描述符0,1,2

  1. 既然我们知道其实stdin对应的文件描述符是1(键盘文件),stdout对应的文件描述符是2(显示器文件),stderr对应文件描述符的是3(显示器文件)
  2. 那么我们就可以使用文件系统调用去直接对这些文件描述符0,1,2进行使用,例如使用write直接对文件描述符1,2对应的显示器文件进行打印
#include #include #include #include #include #include int main(){ const char* message = \"hello linux\\n\"; write(1, message, strlen(message)); write(2, message, strlen(message)); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

read

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 那么我们就可以使用read,从文件描述符0对应的键盘文件中读取数据,如果读取成功read会返回读取字符的个数,如果读取失败,那么会返回一个小于0的数,即-1
#include #include #include #include #include #include int main(){ char buffer[1024]; ssize_t s = read(0, buffer, sizeof(buffer)); if(s < 0) { return 1; } buffer[s] = \'\\0\'; printf(\"%s\\n\", buffer); return 0;}

运行结果后,此时由于要从文件描述符0对应的键盘文件中读取数据,但是此时我们并没有进行输入数据,所以进程就会等待我们输入,即键盘资源就绪
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时小编输入数据,按下回车
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
此时数据就被read将数据读取进buffer字符数组内了,由于文件中并没有对于字符串以\\0结尾的规定,由于我们使用的是c语言,c语言在内存中无法区分出字符串,所以要在字符串的结尾处添加\\0作为字符串结尾的标志,此时字符串才可以正常被打印出来,所以我们最后按下的换行也是字符,所以也会被读取,所以才会出现两个换行

四、访问文件的本质

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)

  1. 上图的左上角,文件系统调用open的返回值是int,这个int类型的返回值叫做文件描述符fd,文件描述符fd的本质就是数组下标
  2. 先看上图的右边,由于一个进程对应多个文件,当文件被打开的时候,此时操作系统势必会有多个被打开的文件,那么多个被打开的文件就要被管理起来,如何管理?先描述再组织,使用一个结构体描述一个打开的文件的诸多信息,例如:在磁盘的什么位置,基本属性,权限,大小,读写位置,谁打开的,以及文件的内核缓冲区信息,当我们对文件进行写入的时候,会先将内容写入到这个文件的内核缓冲区中,当合适的时机,例如文件关闭,操作系统内核自动检测,系统调用函数等等原因将文件的内核缓冲区的内容刷新到磁盘文件中
  3. 当我们有了一个用于描述的被打开文件的结构体对象的时候仅仅是描述,那么如何组织?其实这个打开文件对象内会有指针指向下一个打开文件对象,这样这些用于描述被打开文件的打开文件对象就被以双链表的形式组织起来,这样对文件的管理,就转化为对双链表的增删查改
  4. 在进程的PCB中会有一个struct files_struct* files 结构体指针,这个结构体指针指向一个struct files_struct结构体指针数组,这个struct files_struct结构体指针数组叫做文件描述符表,在进程打开的时候,进程会默认为我们打开三个文件流,分别为stdin,stdout,stderr分别放到文件描述符0,1,2下标数组位置处,0号下标会指向键盘文件打开对象,1,2号下标会指向显示器文件打开对象,此时就出现了两个指针(文件流stdout,stderr)指向一个显示器文件打开对象,所以在文件打开对象内部就会采用引用计数的方式管理这个文件打开对象,例如由于此时有两个指针指向显示器文件打开对象,所以此时显示器文件打开对象的引用计数是2,当stdout进行close关闭的时候,此时仅仅是将计数减一,此时计数变为1,将对应文件描述符表中的指针置空即可完成close,当stderr进行close关闭的时候,此时计数减一,但是此时计数变为了0,所以此时就会刷新文件的内核缓冲区信息,将显示器文件打开对象释放,将对应文件描述符表中的指针置空即可完成close
  5. 那么此时小编将stdout对应的1号下标对应的显示器文件打开对象关闭,那么此时printf就无法进行printf(printf打印的显示器文件对象是stdout)打印了
#include #include #include #include #include #include int main(){ close(1); printf(\"stdin->fd: %d\\n\", stdin->_fileno); printf(\"stdout->fd: %d\\n\", stdout->_fileno); printf(\"stderr->fd: %d\\n\", stderr->_fileno); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
无法向stdout对应的显示器文件进行打印了

  1. 但是由于stderr和stdout对应的是同一个显示器文件,虽然stdout关闭了,但是stderr没有关闭,即stderr还可以进行打印,那么此时我们便用fprintf向stderr文件流中打印,其实printf也是有返回值的,printf的返回值是向显示器打印了多少个字符
#include #include #include #include #include #include int main(){ close(1); int ret = printf(\"stdin->fd: %d\\n\", stdin->_fileno); printf(\"stdout->fd: %d\\n\", stdout->_fileno); printf(\"stderr->fd: %d\\n\", stderr->_fileno); fprintf(stderr, \"%d\\n\", ret); return 0;}

运行结果如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
我们可以看到打印的printf的返回值是13,即printf认为自己向显示器打印成功,并且认为自己打印了13个字符,实际上由于stdout对应的显示器文件被我们关闭了,所以此时printf并不会打印成功,由于stderr没有被我们关闭,所以我们使用fprintf向stderr这个文件流对应的显示器文件中打印的时候会成功

  1. 使用open打开一个文件的时候,会先创建一个文件打开对象,里面描述有这个文件的信息,然后将其链接到文件管理的双链表中,此时PCB会使用struct files_struct* files 结构体指针找到对应的struct files_struct结构体,即文件描述符表中数组下标内容为空的下标中填入文件打开对象的指针,这样就完成了一个文件的创建与打开
    【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
  2. 例如c语言文件操作中的fwrite,它要对文件进行操作,那么就必定要封装系统调用,文件系统调用中有的文件系统调用会使用到文件描述符fd,所以类型为FILE*的文件流对象stream中必定要对文件描述符fd进行封装,同样的不仅仅c语言如此,c++乃至其它语言中也是如此,c++中的fstream,它要访问文件,它其中必定要包含一个字段,即文件描述符fd,任何语言想要在系统中访问文件,必定要包含文件描述符fd

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!