目录
C文件IO
系统文件IO
接口介绍
系统调用和库函数
文件描述符
open返回值
文件描述符的分配规则
重定向
代码演示
使用dup2系统调用
缓冲区
FILE
理解文件系统
文件系统
inode
软硬链接
静态库和动态库
概念
生成静态库
生成动态库
C文件IO
- 写文件
#include <string.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { FILE *fp = fopen("log.txt","w"); if(!fp) { perror("fopen"); } char buf[1024]; const char *msg = "hello world\n"; int count = 5; while(count--) { fwrite(msg,strlen(msg),1,fp); } fclose(fp); return 0; }
打开log.txt查看:
- 读文件
#include <string.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { FILE *fp = fopen("log.txt","r"); if(!fp) { perror("fopen"); } char buf[1024]; const char *msg = "hello world\n"; while(1) { ssize_t s = fread(buf,1,strlen(msg) ,fp); if(s > 0) { buf[s] = 0; printf("%s",buf); } if(feof(fp)) { break; } } fclose(fp); return 0; }
运行结果:
输出
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
const char *msg = "hello world\n";
fwrite(msg,strlen(msg),1,stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
- stdin & stdout & stderr
- C默认会打开三个输入输出流,分别是stdin,stdout,stderr
- 这三个流的类型都是FILE*,fopen返回值类型,文件指针
- 总结--打开文件的方式
文件使用方式 | 含义 |
"r"--只读 | 打开一个已经存在的文件,可读 |
"r+"--读写 | 打开一个文件,可读可写 |
"w"--只写 | 打开一个文件,不存在可创建新文件,可写 |
"w+"--读写 | 打开一个文件,不存在可创建新文件,可读写 |
"a"--追加 | 向文件末尾追加数据,不可读 |
"a+"--读写 | 向文件末尾追加数据,可读写 |
系统文件IO
操作文件,除了上述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("log.txt",O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello world\n";
int len = strlen(msg);
while(count--)
{
write(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("log.txt",O_RDONLY); if(fd < 0) { perror("open"); return 1; } const char *msg = "hello world\n"; char buf[1024]; while(1) { ssize_t s = read(fd,buf,strlen(msg)); if(s > 0) { printf("%s",buf); } else { break; } } close(fd); return 0; }
接口介绍
- 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_EDWR:读写打开 (1,2,3必须指定一个且只能指定一个)
- O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限。
- O_APPEND:追加写
- mode_t:需要进行创建文件时,就需要指定创建文件的八进制访问权限值。
- 返回值:成功返回新打开的文件描述符,失败返回-1。
open函数具体使用哪个,和具体的应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
系统调用和库函数
fopen、fclose、fread、fwrite都是C标准库当中的函数,这些为库函数
open、close、read、write都是系统提供的接口,称为系统调用接口。
系统调用接口和库函数的关系,如下图所示:
文件描述符
open返回值
- 文件描述符fd
文件描述符就是一个小整数
- 0&1&2
- Linux进程默认情况下会有三个缺省打开的文件描述符,分别是标准输入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系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*file,指向一张表file_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针,所以,本质上,文件描述符就是该数组的下标。所以只要拿着文件描述符,就可以找到对应的文件。
描述如图:
文件描述符的分配规则
规则:从头遍历数组fd_array[],找到一个最小的并且没有被使用的下标,分配给新的文件。
代码演示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd1 = open("log.txt",O_RDONLY);
int fd2 = open("log.txt",O_RDONLY);
printf("fd1: %d\n",fd1);
printf("fd2: %d\n",fd2);
close(fd1);
close(fd2);
return 0;
}
从结果可以看出输出是从3开始的进行分配的。当关闭0和2,代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(0);
close(2);
int fd1 = open("log.txt",O_RDONLY);
int fd2 = open("log.txt",O_RDONLY);
printf("fd1: %d\n",fd1);
printf("fd2: %d\n",fd2);
close(fd1);
close(fd2);
return 0;
}
我们可以看到结果是,fd1: 0,fd2 : 1。可见,文件描述符的分配规则是:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
代码演示
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY|O_CREAT,00644);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
return 0;
}
此时,我们可以发现,本来应该输出到显示器上的内容,输出到了文件myfile当中,其中,fd=1。这种现象就叫做输出重定向。我们常见的重定向有:
- >:命令的结果输出到文件上
- >>:命令的结果追加的方式输出到文件上
- < 1 > 2:讲文件1作为命令的输入设备,讲执行结果输出到文件2中
- 重定向的本质
如果我们要进行重定向,上层只认0,1,2,3这样的fd。我们可以在OS内部通过一定的方式调整数组的特定下标的内容(指向),我们就可以完成重定向操作
示图:
使用dup2系统调用
- 函数原型
#include <unistd.h>
int dup2(int oldfd, int newfd);
- 代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt",O_CREAT | O_RDWR);
if(fd < 0)
{
perror("open");
return 1;
}
close(1);
dup2(fd,1);
char buf[1024] = {0};
ssize_t read_size = read(0,buf,sizeof(buf)-1);
if(read_size < 0)
{
perror("read");
}
printf("%s",buf);
fflush(stdout);
return 0;
}
dup2是让newfd赋值oldfd对应的描述信息,成功则关闭newfd原来的指向,也可以简单理解:dup2接口就是让两个描述符都指向oldfd所指向的文件,相当于是对newfd进行了重定向。
缓冲区
- 什么是缓冲区
缓冲区的本质就是一段内存
- 为什么要有缓冲区
- 解放使用缓冲区的进程时间
- 缓冲区的存在可以集中处理数据刷新,减少IO的次数从而达到提高整体的效率的目的
- 缓冲区在哪里
先来感受一段代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
printf("hello printf");
const char *msg = "hello write";
write(1,msg,strlen(msg));
sleep(5);
return 0;
}
我们可以从结果中看出,printf并没有立即刷新,这就是因为有缓冲区的存在,printf内部封装了write。但是write是立即刷新的,所以通过上边的代码演示,我们虽然还不知道缓冲区在哪里,但是我们可以肯定缓冲区是不在write内部。而write是系统接口,所以我们讨论的缓冲区,并不是内核级别的。所以可以看出这个缓冲区只能是C语言提供的,是语言级别的缓冲区。
printf是C库当中的IO函数,一般往stdout中输出,stdout是FILE*类型,FILE*是一个struct结构体,会封装很多的属性,其中有fd还有该FILE对应的语言级别的缓冲区。
图示:
我们写的代码不是直接写在操作系统里边的,而是写到语言级别的缓冲区中(cache),等到数据量积累到一定程度时,会定期通过fd调用write(),把数据刷新到对应的内存当中。但是存在一个问题,什么时候去刷新,这里有一个刷新策略如下介绍:
- 刷新策略
- 无缓冲:立即刷新,系统调用接口就是无缓冲
- 行缓冲:逐行进行刷新
- 全缓冲: 缓冲区数据满时进行刷新
FILE
- 因为IO相关函数与系统调用接口对应,并且库函数封装了系统调用,所以本质上,访问文件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd
研究一下如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.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;
}
运行结果:
对进程实现重定向:
我们发现printf和fwrite(库函数)都输出了2次,而write只输出了一次(系统调用)。这是为什么呢?
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf、fwrite库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 我们被放在缓冲区的数据不会被立即刷新,在进程退出的时候统一进行刷新,写入到文件中
- 但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
- write没有变化,说明没有所谓的缓冲。
综上:printf、fwrite库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级别的缓冲区。其实为了提高整机性能,OS也会提供相关的内核级缓冲区。
理解文件系统
文件系统
- 使用ls-l查看文件信息
显示的信息中,每行有7列信息,分别是:模式、硬链接数、文件所有者、组、大小、最后修改时间、文件名。
- ls-l读取存储在磁盘上的文件信息,然后显示出来,如图所示:
- 除了使用ls-l方式读取信息,还有一个stat命令可以看到更多的信息
inode
为了解释清楚inode先介绍文件系统
上图是磁盘文件系统图,磁盘分区被划分为一个个block,block的大小是由格式化的时候确定的,并且不可以更改。
- 对上图解释
- Block group:文件系统会根据分区的大小划分为数个Block group,每个都有着相同的结构组成
- 超级块(Super Block):存放文件系统本身的结构信息
- GDT(Group Descriptor Table):块组描述符(有多少inode,起始inode编号,有多少inode被使用,有多少block被使用)
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
- inode位图(inode Bitmap):每个bit表示一个inode是否空间可用
- i节点表(inode Table):存放文件属性。如文件大小,所有者,最近修改时间等
- 数据区(Data blocks):存放文件内容
inode就是包含文件的所有属性,直接保存的就是该文件对应的blocks编号
- 当我们创建一个文件,操作系统做了什么呢?
操作系统修改inode Bitmap,在inode Table里找到对应inode结点,向对应inode Table里写文件的属性并且分配数据块,把数据写在数据块里同时修改Block Bitmap并且建立inode和block的映射关系,最终返回该文件的inode。
软硬链接
- 硬链接
- 硬链接就是单纯的在Linux指定的目录下,给指定的文件新增文件名和inode编号的映射关系。
- 硬链接数本质就是该文件inode属性中的一个计数器,count,标识有几个文件名和我的inode建立映射关系,简言之就是有多少个文件名指向我的inod。
- 当我们删除文件时:1、在目录中将对应的记录删除。2、将硬链接数-1,如果为0,则将对应的磁盘释放。
- 使用ln创建硬链接
- 软链接
- 软链接是通过inode引用另一个文件,软链接是通过名字引用另一个文件,其实本质就是linux下的一个快捷方式。
- 使用ln -s创建软链接
- 软硬链接的区别
软链接是一个独立文件,有自己独立的inode和inode编号,硬链接不是一个独立文件,他和目标文件使用的是同一个inode。
- 为什么创建普通文件,硬链接数默认是1?
因为普通文件的文件名,本身就和自己的inode具有映射关系,只有一个。
静态库和动态库
概念
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
生成静态库
- 代码示例
- Makefile文件
- 运行结果
- 运行测试文件
生成动态库
- 代码与静态库相同修改其Makefile
- Makefile文件
- 运行结果
- 运行测试文件
运行动态库有三种方法:
- 拷贝.so文件到系统共享库路径下,一般指/usr/lib
- 更改LD_LIBRARY_PATH(我上边用的就是这种方法)
- ldconfig配置/etc/ld.so.conf.d/(内部其实就是一个路径),使用ldconfig命令更新配置