提示:文章容较长,请参考目录阅读
文章目录
一、C语言中的文件接口
在学习Linux的基础IO之前,先回顾一下C语言中的文件操作
1.1 fopen
FILE *fopen(const char *path, const char *mode);
- 功能:打开文件
- 返回值:打开成功返回文件流指针,失败则返回NULL
- 参数:
path,要打开文件的路径,若不指定则默认打开可执行程序所在路径下文件
mode,文件的打开方式
fopen打开文件的方式
- r 只读方式打开文件
- r+ 可读可写方式打开文件
- w 以只写方式打开文件,若文件存在则清空内容,不存在则创建文件
- w+ 可读可写方式打开文件,若文件存在则清空其内容,不存在则创建文件
- a 追加写,若文件不存在则创建文件,文件存在,文件流指针指向文件末尾进行写
- a+ 可以读,可追加写,若文件不存在则创建文件,读的位置初始化到文件头,写的位置从文件末尾开始
在实际应用中,若文件读取或写入不成功,要先查看打开方式是否正确
程序:
运行结果:
1.2 fwrite
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE*stream);
- 功能:向文件写入数据
- 返回值:成功写入的文件快的个数
- 参数
ptr:要写入文件的内容
size:写入时,一个块的大小,单位字节(通常定义1个字节)
nmemb:写入多少块
stream:文件流指针
程序
1.3 fseek
int fseek(FILE *stream, long offset, int whence)
- 函数功能:移动文件流指针位置
- 返回值:成功返回0,失败返回-1
- 参数
stream:文件流指针
offset:偏移量
whence:将文件流指针移动到什么位置
whence常用的几个参数
- SEEK_SET 文件头部
- SEEK_CUR 当前文件流指针位置
- SEEK_END 文件末尾
1.4 fread
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
- 函数功能:从文件中读取数据
- 返回值:成功读入的文件块的个数
- 参数
ptr:要写入的位置
size:读取文件时,一个块的大小
nmeb:读取多少块
stream:文件流指针
程序
所以正确的做法是,先移动文件流指针到文件开头位置,再读取,如图:
1.5 fclose
int fclose(FILE *fp)
关闭文件流指针,在写入或读取结束后,要在程序中关闭文件流指针,否则会造成文件句柄泄漏,持续泄露,最终会导致进程不能打开新的文件。
下面是一个完整程序,包括文件的打开,写入,移动文件流指针,读取,移动文件流指针:
#include<stdio.h>
#include<string.h>
int main()
{
//1.文件打开
FILE*fp=fopen("1.txt","w+");
if(fp==NULL)//文件打开失败
{
perror("fopen");
}
else//文件打开成功
{
printf("Open success...\n");
}
//2.写入
const char*w_word={"Never say never."};
size_t w_size=fwrite(w_word,1,strlen(w_word),fp);
printf("write size:%ld\n",w_size);
//3.移动文件流指针位置
fseek(fp,0,SEEK_SET);
//4.读
char r_word[100]={0};
size_t r_size=fread(r_word,1,sizeof(r_word)-1,fp);
printf("read size:%ld\n",r_size);
printf("%s\n",r_word);
//5.关闭文件流指针
fclose(fp);
return 0;
}
程序运行结果:
1.6 stdin&stdout&stderr
C进程会打开三个输入输出流,分别是stdin,stdout,stderr
这里先写一个程序,让其一直处于运行状态,并通过getpid接口获取到进程号,如图:
根据拿到的进程号查看该进程在proc目录中的fd文件,如图:
fd实际上就是文件描述符,0、1、2三个描述符对应stdin,stdout,stderr。
可以查看"stdio.h"头文件中的定义:
二、系统文件IO
2.1 open
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- 头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
- 函数功能:打开文件
- 返回值:打开成功返回文件描述符,打开失败返回-1
- 参数
pathname:文件的路径
flags:打开方式
mode:创建的文件权限,传一个8进制数字(如:0664)
open的打开方式
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:可读可写
前三个选项只能指定一个 - O_CREAT:文件不存在,则创建。需要使用mode选项,指明新文件的访问权限
- O_APPEND:追加写,即打开时将文件流指针移动到文件末尾为止
这些方式若选取多个,使用“或”运算连接,如:"O_RDWR | O_CREAT"表示以可读可写方式打开,若文件不存在则创建文件。
程序
上面和程序打开失败,因为当前路径没有该文件,应该先创建文件,或者改变打开方式,如:
2.2 write
ssize_t write(int fd, const void *buf, size_t count)
- 函数功能:向文件写入内容
- 返回值:成功写入文件的字节数
- 参数
fd:文件描述符
buf:将buf指向的内容写入文件
count:期望写入多少字节
程序
2.3 lseek
off_t lseek(int fd, off_t offset, int whence)
- 函数功能:重新定位读/写文件偏移量
- 返回值:移动成功,返回偏移的位置,单位字节;调用失败返回-1
- 参数
fd:文件描述符
offset:偏移量,单位字节
whence:要便宜到什么位置
wehence常用参数
- SEEK_SET 文件头部
- SEEK_CUR 当前文件读取或写入位置
- SEEK_END 文件尾部
2.4 read
ssize_t read(int fd, void *buf, size_t count)
- 函数功能:读取文件内容
- 返回值:从文件中读取的字节数
- 参数
fd:文件描述符
buf:读取的内容保存到buf中
count:期望读多少字节
程序
2.5 close
int close(int fd)
关闭文件描述符
包括文件打开、写入、读写位置偏移、读取、关闭文件描述符的程序:
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main()
{
//1.打开文件
int fd =open("1.txt",O_RDWR | O_CREAT,0664);
if(fd<0)
{
perror("open");
return 0;
}
printf("Open success...\n");
//2.写入
const char* w_word="hello linux!";
write(fd,w_word,strlen(w_word));
//3.移动读取位置
lseek(fd,0,SEEK_SET);
//4.读取
char r_word[100]={0};
read(fd,r_word,sizeof(r_word)-1);
printf("%s\n",r_word);
//关闭文件描述符
close(fd);
return 0;
}
三、文件描述符fd
3.1 概念
通过一个程序观察文件描述符
先获取进程的pid
在"/proc"路径下根据进程号查看对应进程的fd文件,可以查看一个进程打开的文件描述符:
可以看到,该进程打开了4个文件描述符,前0、1、2依次是标准输入、标准输出、标准错误, 3号是进程中使用open接口打开的文件。
我们知道,每个进程都有一个task_struct结构体用于保存进程的信息,task_struct结构体中有一个指针*files,这个指针指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针,所以,本质上,文件描述符就是该数组的下标。
其关系如图:
Linux内核源码sched.h中有对files_struct的声明,如图:
该结构体是保存进程打开的文件信息的。
3.2 文件描述符的分配规则
文件描述符不是固定分配的,而是按照最小未使用原则分配,看下面这个程序:
从这个程序我们可以看出,文件描述符并不是固定分配的,即使是0、1、2也是如此,文件描述符按照最小未使用原则进行分配。
3.3 一个进程可以打开多少个文件描述符
- 系统当中一个进程打开文件描述符的数量是由限制的,可以通过"ulimit -a"指令查看,如图:
这里的"open files"的大小就是一个进程可以打开文件描述符的数量 - "open files"的大小可以通过ulimit -n [num]修改,eg:ulimit -n 10000,将一个进程可以打开的文件描述符数量限制为10000
3.4 深入理解文件描述符与文件流指针
查看文件流指针FILE的定义
在"stdio.h"头文件中的搜索"FILE",查看结果如下:
从这里可以看出,文件流指针FILE本质上就是_IO_FILE,即C标准库中的一个结构体
再查找"struct _IO_FILE"结构体的定义:
最终在libio.h头文件中找到_IO_FILE结构体的定义:
这里=="_IO_FILE"结构体的成员变量"_fileno"保存的正是文件描述符的数值==
通过程序也可以验证这一点:
用一句话概括文件流指针与文件描述符的关系就是:文件流指针指向的结构体_IO_FILE内部的成员变量_fileno保存了文件描述符的数值。
3.5 重定向
符号
- >> 追加重定型
- > 清空重定向
使用dup2系统调用进行重定向
int dup2(int oldfd, int newfd);
dup2的两个参数均是文件描述符,含义是将newfd的值重定向为oldfd,即即newfd对应的文件指针指向原来oldfd指向的文件。
程序:
四、动态库和静态库
4.1 概念
静态库与动态库都是二进制的程序代码的集合。将程序编写成库提供给第三方使用,这样做的好处是不会造成源码泄漏,而且调用者不用关心内部实现。
- 静态库(.a):程序在编译链接的时候把库的代码连接到可执行文件当中,运行时不再需要静态库。Linux中:前缀为"lib",后缀".a";Windows中:后缀".lib"。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。Linux中:前缀为"lib",后缀".so";Windows中:后缀".lib。
4.2动态库
生成动态库
- shared:表示生成共享格式
- fPIC:产生位置无关码
- 动态库名称规则:libxxx.so
使用动态库——编译
先写一个程序,调用库中的接口
如果直接编译会有这样的报错
正确的编译指令
L:链接库所在路径
l:链接动态库,后面紧跟库名,没有空格
此时main_test依赖的库中已经有我们自己生成的库。
使用动态库——运行
如果直接运行编译生成的可执行程序,程序运行失败
为什么会这样呢?
通过ldd指令查看可执行程序依赖的库,发现是因为无法找到程序所链接的库的位置。如图:
那么有什么办法能让程序找到动态库呢?
-
将动态库拷贝到可执行程序的目录下(不推荐)
-
更改"LD_LIBRARY_PATH"环境变量,将我们生成的动态库的路径添加到该环境变量中。
这个环境变量可以在"~/.bshrc"文件中修改,完成以后别忘记source一下,让修改生效。 -
将生成的库放到系统库路径下:/lib64(极不推荐)
我们采取修改环境变量的方法,修改之后再通过ldd查看
运行程序,打印成功:
4.3 静态库
生成
静态库的生成有两个阶段
- 用gcc -c 指令把源文件编译成.o的二进制文件
- 通过ar -rc 指令将将.o文件编译成静态库
ar是gnu归档工具,rc表示(replace and creat)
此时,可以通过ar -tv指令查看静态库中的目录列表,即包含了哪些二进制文件
t:列出静态库中的文件
v:verbose 详细信息
使用
在编译可执行程序的时候,如果使用了静态库,静态库会编译到可执行程序中,没有可执行程序依赖动态库的问题。
五、简单文件系统
文件元数据
使用ls -l或ll查看文件详细信息内容如下
每行有7项内容
- 模式
- 硬链接数
- 文件所有者
- 文件所属组
- 文件大小(单位:字节)
- 最后修改时间
- 文件名
还可以使用stat指令查看更详细的文件信息
理解inode
Linux ext2文件系统,按块儿划分,硬盘分区被划分为一个一个的block,如图
文件在磁盘中是离散式存储在Data blocks(数据区)中的,所以用Block Bitmap中的一个比特位来记录Data blocks中哪个数据块已经被占用,那个数据块没有被占用,分别用0、1表示。
即inod节点表中的一个节点就保存了一个文件的存储位置等信息,所以一个inode节点可以代表一个文件。
创建一个文件的4个操作:
- 存储属性
内核先找到一个空闲的i节点,如:10001。内核把文件信息记录到其中。 - 存储数据
假设该文件需要存储在三个磁盘块,内核找到了三个空闲块:100,300,500。将内核缓冲区的数据块分开拷贝到三个数据块 - 记录分配情况
文件内容按顺序100,300,500存放。内核在inode上的磁盘分布区记录了上述块列表。 - 添加文件名到目录
设文件名为xxx。内核将入口(10001,xxx)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
软链接与硬链接
软链接
软链接可以看成源文件的快捷方式,生成方法:
- 修改源文件,软链接文件也会被修改:修改软链接文件,源文件也会被修改
- 删除源文件,软链接文件还在,但其内容已经不存在,若此时再修改软链接文件,又会生成新的源文件,但此时源文件内容已经发生改变。所以一般删除源文件时要同时删除软链接文件。
硬链接
硬链接可以当作源文件的替身
查看文件信息
可以看出,硬链接与源文件的节点号相同,状态信息也完全相同。
事实上,我们在删除一个文件的时候做了两件事
- 在目录中删除文件记录
- 硬链接数-1,如果链接数为0,则释放该文件对应的磁盘
如上图所示,删除test的硬链接文件后,文件的硬链接数-1。