Linux----IO(初级)
- 1)C文件I/O
- 2)系统文件I/O
- ①Linux一切皆文件(补缓冲区)
- ②IO系统接口
- ③站在系统角度理解
- ④file descriptor(文件与进程)
- ⑤分配文件描述符的规则
- ⑥C中的FILE
- ⑦重定向
- 3)文件系统
- ①将磁盘组织,管理
- ②inode
- ③软链接
- ④硬链接
- stat时间
- 4)动态库和静态库
- ①命名和链接
- ②打包静态库
- ③使用静态库
- ④打包动态库
- ⑤使用动态库
1)C文件I/O
复习:
C语言----文件操作
补充:(输出到显示器的几种方式)
const char *msg = "hello fwrite\n"; fwrite(msg, strlen(msg), 1, stdout); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n");
任何C程序,都默认会打开三个文件
:
- 标准输入(stdin), 键盘文件
- 标准输出(stdout)) ,显示器文件
- 标准错误(stderr),显示器文件
所有的外设硬件,本质对应的核心操作都是read和write,不同的硬件,对应的读写方式肯定是不一样的
2)系统文件I/O
①Linux一切皆文件(补缓冲区)
Linux一切皆文件理解:
每一个结构体里有两个函数指针,指向每个硬件的读写操作
OS(用结构体描述,再通过双链表组织起来
),通过一层软件的虚拟,所以就都可以看作文件
Linux中有三种缓冲区:
全缓冲
:缓冲区写满才写入(对于驻留在磁盘上的文件通常由标准I/O库实施全缓冲。调用fflush函数冲洗一个流。冲洗意味着将缓冲区的内容写到磁盘上)行缓冲
:遇到换行符\n才写入(涉及一个终端时,通常使用行缓冲,如getchar())无缓冲
:直接写入(如cerr)
②IO系统接口
头文件:
#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
flags参数:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR : 读写打开
- O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND: 追加写
注意:
- 前三个常量,必须指定一个且只能指定一个
- 系统喜欢用比特位不重叠(
只有一个比特位是唯一的
)的二进制序列来表示不同的标志(定义宏),系统内部可以通过按位与来检测,如:#define O_RDONLY 0x01;#define O_WRONLY 0x02;...
这也解释了为什么flags参数可以进行"|"运算
在/usr/include/bits/fcntl-linux.h
目录下可以查看此头文件
mode:
(参考Linux权限8进制方式)
mode 参数指定创建新文件时应用的文件模式位。 在flags中指定 O_CREAT 或 O_TMPFILE 时必须提供此参数;如果既没有指定 O_CREAT 也没有指定 O_TMPFILE,则忽略mode
返回值:
- 成功:新打开的文件描述符(file descriptor)
- 失败:-1
头文件:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write() 将缓冲区中从 buf 开始的 count 个字节写入文件描述符 fd 所引用的文件
fd
:open()返回的文件描述符
buf
:buf指向要写入的内存
返回值:
- 成功时,返回写入的字节数(零表示未写入任何内容)。 如果此数字小于请求的字节数,则不是错误; 例如,这可能是因为磁盘设备已满
- 出错时,返回 -1
头文件:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read() 从 buf 开始,尝试将文件描述符 fd 中的 count 个字节读入缓冲区
如果 count 为零,read() 可能会检测到下面描述的错误。 在没有任何错误的情况下,或者如果 read() 不检查错误,则计数为 0 的 read() 返回零并且没有其他效果
返回值:
- 成功时,返回读取的字节数(零表示文件结束)可能会小于请求的字节数,例如因为现在实际可用的字节数较少
- 出错时,返回 -1
头文件:
#include <unistd.h>
int close(int fd);
返回值:
- 成功:0
- 失败:-1
头文件:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
略
参考man 2 lseek
int main() { int fd=open("log.txt",O_WRONLY|O_CREAT,0644); // int fd=open("log.txt",O_RDONLY); if(fd<0){ perror("open"); return 1; } // char buffer[1024]; // ssize_t s=read(fd,buffer,sizeof(buffer)-1); // if(s>0){ // buffer[s]='\0'; // printf("%s\n",buffer); // } const char *msg="hello world\n"; write(fd,msg,strlen(msg)); write(fd,msg,strlen(msg)); write(fd,msg,strlen(msg)); close(fd); return 0; }
③站在系统角度理解
每个语言都有自己的库函数,自己的IO接口,因为系统调用成本较高,且不具备可以执行(Linux<->Windows)
以C语言为例,在我们调用fopen/fwrite等函数时,会自动根据平台选择自己底层对应的文件接口
④file descriptor(文件与进程)
先看下面一段代码:
int fd1=open("log1.txt",O_WRONLY|O_CREAT,0644); int fd2=open("log2.txt",O_WRONLY|O_CREAT,0644); int fd3=open("log3.txt",O_WRONLY|O_CREAT,0644); int fd4=open("log4.txt",O_WRONLY|O_CREAT,0644); int fd5=open("log5.txt",O_WRONLY|O_CREAT,0644); printf("fd:%d",fd1); printf("fd:%d",fd2); printf("fd:%d",fd3); printf("fd:%d",fd4); printf("fd:%d",fd5);
可以发现是从3开始的连续数字,大胆猜测这就是数组的下标
而缺少的0 1 2
正好对应默认打开的标准输入,标准输出,标准错误
一个进程可以打开多个文件
同时操作系统为了管理这多个文件会用struct file结构体描述起来并组织在一起组织方式就是通过数组;
每一个task_struct
里面都有一个struct file_struct*
的指针指向struct file_struct
同时struct file_struct
里会有一个struct file*
类型的fd_array
数组,每一个元素指向一个系统描述的文件也就是struct file
(每新”描述“一个文件就会新增一个struct file
,同时让fd_array
末尾新增的指针指向它,再将它的下标fd返回给task_struct
)
对应的一堆操作(读写等)
所以:用户层看到的fd,本质系统中维护进程和文件对应关系的数组的下标
所谓的默认打开文件,标准输入,标准输出,标准错误,其实是有底层系统支持的,默认在一个进程运行的时候,就会打开0,1,2
⑤分配文件描述符的规则
系统中分配文件描述符的规则
:最小的,没有被使用的,进行分配
⑥C中的FILE
CentOS8好像没有<libio.h>头文件了,在\VS2022\Common7\IDE\VC\Linux\include\usr\include下可以找到
stdio.h中
libio.h中
这里看到的_fileno
就是文件描述符
下面代码测试:
(stdin stdout stderr默认对应0 1 2 )printf("%d\n",stdin->_fileno); printf("%d\n",stdout->_fileno); printf("%d\n",stderr->_fileno); FILE *fp=fopen("log.txt","w"); printf("%d\n",fp->_fileno);
FILE中包含的另外一个重要的就是
缓冲区数据
(VS中将FILE typedef为了io_buf(io缓冲区))
当printf输出的时候其实并没有通过operation向fd_array[2]指向的显示器文件内核缓冲区写入,而是
写入到了stdout结构体自己的缓冲区中
,当遇到\n或fflush(stdout)时,才会通过文件描述符找到对应的显示器文件,写入
引入重定向概念+代码验证1:
close(1); int fd=open("log.txt",O_CREAT|O_WRONLY,0644); if(fd<0){ perror("open"); return 1; } printf("fd:%d\n",fd); //fflush(stdout);//刷新缓冲区 close(fd);
没有fflush(stdout)时:
加上fflush(stdout):
重点解释:
- 这里的fd为1,是因为我们先close(1)关掉底层显示器输出文件,由于系统中分配文件描述符的规则,新打开的log.txt文件会找到最小的未被占用的位置也就是1(初步认识重定向)
- printf()末尾有’\n’还是不能刷新到log.txt文件,
因为一般C库函数写入文件时是全缓冲的(需要fflush),而写入显示器是行刷新(只用\n),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲
代码验证2:
printf("printf\n"); fprintf(stdout,"fprintf\n"); fputs("fputs\n",stdout); //system call const char *p="write\n"; write(1,p,strlen(p)); fork();
直接打印
输出重定向到log.txt
解释:(写时拷贝+数据刷新策略+文件差异)
- 当重定向到log.txt(普通文件)的时候,直到fork()完,进程退出的时候才会统一刷新,写入文件当中
- 但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新(二次缓冲区
写入到内核缓冲区
)的时候,子进程也就有了同样的一份数据,随即产生两份数据- printf fputs fprintf库函数会自带用户级缓冲区,而 write 系统调用没有带用户级缓冲区(
大多数情况下写时拷贝是拷贝属于进程的数据和用户级数据,内核级数据很少
)- 同时可以证明这些缓冲区是二次加上的,由C标准库提供
⑦重定向
unistd.h里提供了优雅的解决方案dup()库函数,而不用每次都close()
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE
/ See feature_test_macros(7) /
#include <fcntl.h>
/ Obtain O_ constant definitions /
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);
这里的fd_array[newfd]是fd_array[oldfd]的拷贝
注意:
不一定会close(newfd)
- 如果 oldfd 不是有效的文件描述符,则调用失败,并且 newfd 不会关闭
- 如果 oldfd 是一个有效的文件描述符,并且 newfd 与 oldfd 具有相同的值,则 dup2() 什么都不做,并返回
返回值
:成功后,这些系统调用会返回新的文件描述符。 出错时返回 -1
示例代码:
// int fd= open("log.txt",O_CREAT|O_WRONLY,0644); int fd=open("log.txt",O_RDONLY); if(fd<0){ perror("open"); return 1; } // dup2(fd,1);//输出重定向到显示器 dup2(fd,0);//输入重定向键盘 char buffer[1024]; ssize_t s=read(0,buffer,sizeof(buffer)-1); if(s>0){ buffer[s]=0; printf("echo:%s\n",buffer); } // const char*p="dup2\n"; // write(1,p,strlen(p)); // write(1,p,strlen(p)); // write(1,p,strlen(p)); // write(1,p,strlen(p)); return 0;
输出重定向>
输入重定向<
问题:程序替换的时候,会不变影响直定向对应的数据结构数据?
答
:不影响,task_struct里两个指针分别指向file_struct和mm_struct 互不干扰
简易shell添加重定向功能:Linux----50行简易shell(待补充)
3)文件系统
①将磁盘组织,管理
内存的基本单位是1B,但是站在OS的角度,使用的时候,基本单位一般是4KB
类比:磁盘站在OS的角度,存储的基本单位一般是扇区(512B,也有4KB的),格式化的时候相当于写入文件系统
我们可以将圆形磁盘抽象成线性的一个数组
解释:
Block Group
:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成Super Block
:存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏Group Descriptor Table
:块组描述符,描述块组属性信息Block Bitmap
:Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用inode Bitmap
:每个bit表示一个inode是否空闲可用inode Table
:存放文件属性 如 文件大小,所有者,最近修改时间等Data blocks
:存放文件内容
②inode
申请1号inode,1号inode里的属性里有一个数组,记录这个inode和datablocks数据块的对应关系
注意
:当文件太大的时候需要多级索引,在Data blocks的块上存储类似inode里的数组的对应关系(类似多级页表)
通过inode Bitmap和Block Bitmap (二进制比特位) 我们可以迅速的添加删除对应的位置
注意:
- 一个文件一个inode (
包括目录,也会有对应的数据块
)- inode是一个 文件的所有的属性集合(即便是空数据属性集合也是数据,也要占据空间)
- 真正标识文件的不是文件名,而是文件的inode编号
- inode是可以和特定的数据块产生关联的
- 我们通过路径定位文件,也可以认为是目录,在一个文件创建时申请inode,对应数据块写入数据的同时,这个文件所在的
目录也会在这个目录对应的数据块中写入这个文件的文件名和该文件inode的映射关系
- 在删除文件时候是将对应的inode Bitmap和Block Bitmap部分置0,同时移除该目录数据块中这个文件的文件名和该文件inode的映射关系
使用
ls -ali
可以查看文件的inode编号
③软链接
建立一个文件的软链接
ln -s [文件名] [软链接文件名]
注意
:软链接有自己独立的inode编号
,类似于Windows中的快捷方式
两文件彼此独立,软链接文件可以在任何路径下运行,且运行的就是file.txt文件
④硬链接
建立一个文件的硬链接
ln [文件名] [硬链接文件名]
注意
:硬链接没有自己独立的inode
,其本质只是在该目录对应的数据块中写入hard_link和inode(151004430)的对应关系
下面的数字表示的是该文件对应的硬链接文件个数
问1
:一个mkdir一个目录对应的硬链接为什么为 2 ?
答
:每个目录下有个’.
'代表当前目录,就是一个硬链接
问2
:一个mkdir一个目录对应的硬链接为什么为 3 ?
答
:这个目录下还有个目录里面的’..
'代表上级目录是一个硬链接
硬链接的用处:方便我们进行相对路径设置
stat时间
使用
stat [filename]
可以查看该文件的相关属性
注意到有三个时间:
Access
:高频访问此文件才会改变(Linux2.6内核后的策略将访问频次降低)Modify
:更改文件内容的时候会改变Change
:更改文件内容的时候会改变(内容改变,属性也会改变),权限变化的时候会改变
编译过程消耗明显,
makefile会通过比较最近修改时间(Modify)是否改变来决定是否重新编译文件
4)动态库和静态库
①命名和链接
命名方式:
- 静态库:
lib+[名字]+.a
- 动态库:
lib+[名字]+.so
生成可执行程序的方式
静态链接
:链接器在链接静态链接库的时候是以目标文件为单位的,在生成可执行程序之前会将需要使用的函数所在库中包含这个函数的目标文件链接到同一个目标文件中,造成浪费空间,但是运行速度更快动态链接
:在运行一个.o程序时,需要哪个函数就会将库中包含这个函数的目标文件,就将其加载到内存,同时映射到进程地址空间的共享区
,当另一个.o程序也需要用到这个库中的目标文件时就不用再次加载了,解决空间浪费问题,但是速度更慢
gcc编译默认采用的动态链接,需要使用静态链接可以加上
-static
选项如果提示
/usr/bin/ld: cannot find -lc
说明在usr/lib64下没有libc.a静态库
注意
:CentOS8没有在usr/lib64下自带libc.a,且命令yum install glibc-static会提示找不到文件,手动编译复制到usr/lib64下,请参考:glibc-static安装
②打包静态库
我们自己写了四个文件:
mul.h
#pragma once #include <stdio.h> int mul(int x,int y);
mul.c
#include "mul.h" int mul(int x,int y) { return (x*y); }
div.h
#pragma once #include <stdio.h> int div(int x,int y);
div.c
#include "div.h" int div(int x,int y) { return (x/y); }
先对mul.c和div.c进行编译生成.o文件:
gcc -c [文件名]
再使用命令ar -rc [生成静态库名] mul.o div.o
生成静态库
到这时,我们要使用这个打包的静态库就只需要 div.h mul.h libmymath.a三个文件
③使用静态库
c文件名要包含打包的.h文件
需要使用命令:gcc [c文件名] -o [可执行文件名] -I [路径] -L [路径] -l[静态库名省略] (-static)
注意:
-I
:告诉gcc除了默认路径以及当前路径,在指定路径下也找一下头文件-L
:告诉gcc除了默认路径以及当前路径,在指定路径下也找一下库文件-l
:具体链接哪个库(这里是静态库名省略比如libmymath.a,只需要写mymath
)注意-l和库名间无空格
例如:
问
:为什么C语言在编译的时候,从来没有明显的使用过-I
-L
-l
等选项?
答
:库文件和头文件,在默认路径下gcc能找到gcc编译C代码,默认就应该链接libc,如果使用第三方库,一般也要带上-lname
如果我们也不想使用这些选项头文件库文件分别拷贝到默认路径(usr/include, usr/lib64或usr/lib)下,进行库的安装(不建议,会污染库
)
④打包动态库
生成.o目标文件的gcc选项多了
fPIC
选项
fPIC
:位置无关码,即加载到内存的位置和在地址空间的共享区位置无关
(参考上面的动态链接过程)
如图,描述A,B的位置如果用与X位置相关距离,X位置改变,A B位置也随之改变,然而使用与X位置无关距离则不会改变
在动态库的代码里,它无法确定自己被主程序加载到了哪个位置的,所以必须使用位置无关代码,gcc使用相对地址
打包.o文件的gcc选项多了
-shared
,体现了一份代码可以映射到多个地址空间的思想
编写一个makefile
libmymath.so:mul.o div.o gcc -shared -o $@ $^ mul.o:mul.c gcc -fPIC -c $^ div.o:div.c gcc -fPIC -c $^
⑤使用动态库
和静态库一样使用
gcc [c文件名] -o [可执行文件名] -I [路径] -L [路径] -l[动态库名省略]
进行编译
编译成功后我们会发现
ldd查看链接情况
因为不在默认环境变量LD_LIBRARY_PATH下,临时添加
:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/code_test/Date/2-25
ldd test,动态库正常 ./test正常运行
其他方式:
- 拷贝.so文件到系统共享库路径下, 一般指/usr/lib或usr/lib64
- 配置/etc/ld.so.conf.d/,ldconfig更新
在/etc/ld.so.conf.d/下新建一个.conf文件写入自己库的绝对路径,再ldconfig
命令刷新