• Linux基础IO


    01.stdin & stdout & stderr

    • C默认会打开三个输入输出流,分别是stdin, stdout, stderr
    • 三个流的类型都是 FILE*, fopen 返回值类型,文件指针

     02.接口介绍

        open man open
    • mode_t理解:直接 man 手册,比什么都清楚。
    • open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限, 否则,使用两个参数的 open

     03.open函数返回值

     (1)文件描述符fd

    • 通过对open函数的学习,我们知道了文件描述符就是一个小整数

     (2)0 & 1 & 2

    • Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是标准输入 0 , 标准输出 1 , 标准错误 2。
    • 0,1,2 对应的物理设备一般是:键盘,显示器,显示器。

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

    04.文件描述符的分配规则

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

    (1)重定向

         常见的重定向有 :>, >>, <
         那重定向的本质是什么呢?

     (2)使用 dup2 系统调用

         printf C 库当中的 IO 函数,一般往 stdout 中输出,但是 stdout 底层访问文件的时候,找的还是 fd:1, 但此时, fd:1下标所表示内容,已经变成了myfifile 的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

    (3)FILE

    • 因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd 访问的。
    • 所以 C 库当中的 FILE 结构体内部,必定封装了 fd。
    1. #include
    2. #include
    3. int main()
    4. {
    5. const char *msg0="hello printf\n";
    6. const char *msg1="hello fwrite\n";
    7. const char *msg2="hello write\n";
    8. printf("%s", msg0);
    9. fwrite(msg1, strlen(msg0), 1, stdout);
    10. write(1, msg2, strlen(msg2));
    11. fork();
    12. return 0;
    13. }

        运行出结果:

    1. hello printf
    2. hello fwrite
    3. hello write
        但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
    1. hello write
    2. hello printf
    3. hello fwrite
    4. hello printf
    5. hello fwrite
          我们发现 printf fwrite (库函数)都输出了 2 次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
    • 一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
    • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
    • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至 fork 之后。
    • 但是进程退出之后,会统一刷新,写入文件当中。
    • 但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
    • write 没有变化,说明没有所谓的缓冲。
          综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS 也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
       
          那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的 上层 , 是对系统调用的“ 封装 ,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由 C 标准库提供。

     05.理解文件系统

        我们使用 ls -l 的时候看到的除了看到文件名,还看到了文件元数据。
    1. [root@localhost linux]# ls -l
    2. 总用量 12
    3. -rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
    4. -rw-r--r--. 1 root root 654 "9月 13 14:56" test.c
         每行包含 7 列:
    • 模式
    • 硬链接数
    • 文件所有者
    • 大小
    • 最后修改时间
    • 文件名

          ls -l读取存储在磁盘上的文件信息,然后显示出来

        其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息。

    1. [root@localhost linux]# stat test.c
    2. File: "test.c"
    3. Size: 654 Blocks: 8 IO Block: 4096 普通文件
    4. Device: 802h/2050d Inode: 263715 Links: 1
    5. Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
    6. Access: 2017-09-13 14:56:57.059012947 +0800
    7. Modify: 2017-09-13 14:56:40.067012944 +0800
    8. Change: 2017-09-13 14:56:40.069012948 +0800
        上面的执行结果有几个信息需要解释清楚
         inode
         为了能解释清楚 inode 我们先简单了解一下文件系统:

         Linux ext2 文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block 。一个 block 的大小是由格式化的时候确定的,并且不可以更改。例如 mke2fs -b 选项可以设定block 大小为 1024 2048 4096 字节。而上图中启动块( Boot Block )的大小是确定的。
    • Block Group ext2 文件系统会根据分区的大小划分为数个 Block Group 。而每个 Block Group 都有着相同的结构组成。政府管理各区的例子。
    • 超级块( Super Block ):存放文件系统本身的结构信息。记录的信息主要有: bolck inode 的总量,未使用的block inode 的数量,一个 block inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了。
    • GDT Group Descriptor Table :块组描述符,描述块组属性信息。
    • 块位图( Block Bitmap ): Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用。
    • inode 位图( inode Bitmap ):每个 bit 表示一个 inode 是否空闲可用。
    • i 节点表 : 存放文件属性 如 文件大小,所有者,最近修改时间等。
    • 数据区:存放文件内容。
         将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过 touch 一个新文件来看看如何工作。
    1. [root@localhost linux]# touch abc
    2. [root@localhost linux]# ls -i abc
    3. 263466 abc
          为了说明问题,我们将上图简化:
         创建一个新文件主要有一下4 个操作:
    1. 存储属性。内核先找到一个空闲的i节点(这里是263466),内核把文件信息记录到其中。
    2. 存储数据。该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
    3. 记录分配情况 。文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
    4. 添加文件名到目录。新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

    06.理解硬链接

         我们看到,真正找到磁盘上文件的并不是文件名,而是 inode 。 其实在 linux 中可以让多个文件名对应于同一个 inode。 [root@localhost linux]# touch abc [root@localhost linux]# ln abc def [root@localhost linux]# ls -1i abc def 263466 abc 263466 def。
    • abc def 的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数, inode 263466 的硬连接数为 2
    • 我们在删除文件时干了两件事情: 1. 在目录中将对应的记录删除, 2. 将硬连接数 -1 ,如果为 0 ,则将对应的磁盘释放。

    07.软链接 

         硬链接是通过 inode 引用另外一个文件,软链接是通过名字引用另外一个文件,在 shell 中的做法。
    1. 263563 -rw-r--r--. 2 root root 0 915 17:45 abc
    2. 261678 lrwxrwxrwx. 1 root root 3 915 17:53 abc.s -> abc
    3. 263563 -rw-r--r--. 2 root root 0 915 17:45 def

        下面解释一下文件的三个时间 :

    • Access 最后访问时间
    • Modify 文件内容最后修改时间
    • Change 属性最后修改时间

    08.动态库和静态库

    (1)静态库与动态库

    • 静态库( .a ):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
    • 动态库( .so ):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
    • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
    • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking )。
    • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
    1. 测试程序
    2. /add.h/
    3. #ifndef __ADD_H__
    4. #define __ADD_H__
    5. int add(int a, int b);
    6. #endif // __ADD_H__
    7. /add.c/
    8. #include "add.h"
    9. int add(int a, int b)
    10. {
    11. return a + b;
    12. }
    13. /sub.h/
    14. #ifndef __SUB_H__
    15. #define __SUB_H__
    16. int sub(int a, int b);
    17. #endif // __SUB_H__
    18. /add.c/
    19. #include "add.h"
    20. int sub(int a, int b)
    21. {
    22. return a - b;
    23. }
    24. ///main.c
    25. #include
    26. #include "add.h"
    27. #include "sub.h"
    28. int main( void )
    29. {
    30. int a = 10;
    31. int b = 20;
    32. printf("add(10, 20)=%d\n", a, b, add(a, b));
    33. a = 100;
    34. b = 20;
    35. printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
    36. }

     (2)生成静态库

    1. [root@localhost linux]# ls
    2. add.c add.h main.c sub.c sub.h
    3. [root@localhost linux]# gcc -c add.c -o add.o
    4. [root@localhost linux]# gcc -c sub.c -o sub.o
    5. 生成静态库
    6. [root@localhost linux]# ar -rc libmymath.a add.o sub.o
    7. ar是gnu归档工具,rc表示(replace and create)
    8. 查看静态库中的目录列表
    9. [root@localhost linux]# ar -tv libmymath.a
    10. rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
    11. rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
    12. t:列出静态库中的文件
    13. v:verbose 详细信息
    14. [root@localhost linux]# gcc main.c -L. -lmymath
    15. -L 指定库路径
    16. -l 指定库名
    17. 测试目标文件生成后,静态库删掉,程序照样可以运行

    (3)库搜索路径

    • 从左到右搜索-L指定的目录。
    • 由环境变量指定的目录 ( LIBRARY_PATH
    • 由系统指定的目录
    1. /usr/lib
    2. /usr/local/lib

     (4)生成动态库

    • shared: 表示生成共享库格式
    • fPIC :产生位置无关码 (position independent code)
    • 库名规则:libxxx.so
    示例: [root@localhost linux]# gcc -fPIC -c sub.c add.c [root@localhost linux]# gcc -shared -o libmymath.so  *.o [root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o

    (5)使用动态库

         编译选项
    • l :链接动态库,只要库名即可 ( 去掉 lib 以及版本号 )
    • L :链接库所在的路径

         示例: gcc main.o -o main –L. -lhello 

    (6) 运行动态库

    • 拷贝.so文件到系统共享库路径下, 一般指/usr/lib
    • 更改 LD_LIBRARY_PATH
      1. [root@localhost linux]# export LD_LIBRARY_PATH=.
      2. [root@localhost linux]# gcc main.c -lmymath
      3. [root@localhost linux]# ./a.out
      4. add(10, 20)=30
      5. sub(100, 20)=80
    • ldconfifig 配置 /etc/ld.so.conf.d/ ldconfifig 更新
    1. [root@localhost linux]# cat /etc/ld.so.conf.d/bit.conf
    2. /root/tools/linux
    3. [root@localhost linux]# ldconfig

    (7)使用外部库

          系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses 库)。
    1. #include
    2. #include
    3. int main(void)
    4. {
    5. double x = pow(2.0, 3.0);
    6. printf("The cubed is %f\n", x);
    7. return 0;
    8. }
    9. gcc -Wall calc.c -o calc -lm
         -lm 表示要链接 libm.so 或者 libm.a 库文件

    (8)库文件名称和引入库的名称

        如: libc.so -> c 库,去掉前缀 lib ,去掉后缀 .so,.a
  • 相关阅读:
    电子电器架构 —— 车载网关边缘节点路由转发策略
    信息化工程测试验收管理制度
    Mybatis常用代码
    [BSidesCF 2020]Had a bad day1
    Python学习:魔法函数
    【CSS】尝试性使用一下css容易混淆的属性选择器
    使用Testconainers来进行JAVA测试
    Docket安装nginx
    MySQL 锁常见知识点&面试题总结
    趣解设计模式之《小王的披萨店续集》
  • 原文地址:https://blog.csdn.net/zhao19971014/article/details/127382393