前言
先来回忆一下C语言的文件操作:
#include <stdio.h>
#include <string.h>
int main()
{
//打开文件,以写的方式打开
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
}
//相应操作
const char *msg = "hello bit!\n";
int count = 5;
while(count--){
fwrite(msg, strlen(msg), 1, fp);
}
//关闭文件
fclose(fp);
return 0;
}
C默认会打开三个输入输出流,分别是stdin, stdout, stderr,这三个流的类型都是FILE, fopen返回值类型,文件指针*
一、系统文件I/O
操作文件,除了上述C接口(当然,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("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello bit!\n";
int len = strlen(msg);
while(count--){
write(fd, msg, len);//fd: 文件描述符, msg:缓冲区首地址, //len:本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
}
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_RDWR : 读,写打开
以上这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
系统调用和库函数
C语言的fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc),而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口。而系统调用为库函数的实现提供了底层支持。可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
文件描述符
文件描述符就是系统调用接口open的返回值,它是一个整型。而Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入, 标准输出, 标准错误,分别用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;
}
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
文件描述符的分配规则
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//以只读的方式打开文件
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
//打印文件描述符后关闭文件
printf("fd: %d\n", fd);
close(fd);
return 0;
}
文件描述符的结果是3,我们知道0,1,2分别代表标准输入,标准输出和标准错误。如果我们打开文件前,关掉标准输入0,会发生什么呢:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
我们会发现其结果变为了0,所以,文件描述符的分配规则就是:在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("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
//刷新缓冲区
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。
因为我们知道,文件描述符就是指向对应打开文件的指针数组的下标,同时根据文件描述符的分配原则,关闭1以后,再打开文件分配的文件描述符就是1,所以本该输出到屏幕的内容,输出到了文件中去。
为了实现重定向,Linux系统可以使用dup2系统调用来实现重定向:
#include <unistd.h>
//oldfd为要定向到的那个文件描述符
//newfd表示要被重定向的那个文件描述符
int dup2(int oldfd, int newfd);
有这样一段代码:
#include <stdio.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;
}
运行结果如下:
但如果对进程实现输出重定向呢? ./hello > file , 我们发现文件结果变成了这样:
怎么变成五行了呢?虽然很迷,但是盲猜和fork有关!
这是因为printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新
- 但是进程退出之后,会统一刷新,写入文件当中。
- fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲。
printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,由C标准库提供。
二、文件系统
在我们使用ls -l命令时,看到的除了看到文件名,还看到了文件的属性信息。
- 第一行是total,代表文件当前目录下文件占用的大小
- 第一列代表文件类型
- | 常规文件,即file |
---|---|
d | 目录文件 |
b | block device 即块设备文件,如硬盘;支持以block为单位进行随机访问 |
c | character device 即字符设备文件,如键盘支持以character为单位进行线性访问 |
l | symbolic link 即符号链接文件,又称软链接文件 |
p | pipe 即命名管道文件 |
s | socket 即套接字文件,用于实现两个进程进行通信 |
- 之后三列代表当前用户的读,写和可执行权限,接着三列代表当前用户所属组的对应权限,再往后三列代表,其他人的权限。
- 接下来一个数字代表对应文件的硬链接数。
- 之后的两段字符分别代表当前用户和所属组。
- 再往后是文件大小,日期,以及文件名。
通过stat可以查看文件时间记录的信息:
stat log.txt
- Access 指最后一次读取的时间
- Modify 指最后一次修改数据的时间
- Change 指最后一次修改元数据的时间(文件的属性和状态信息发生改变,该时间就会更新)
inode
通过 ls -i就可以查看文件的inode
Linux下每个文件都有自己的inode,Linux系统内部不使用文件名,而是使用inode号码识别文件。
文件系统结构
Linux ext2文件系统,磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。
- 档案系统描述符:描述块组属性信息
- 区块对应表,也就是块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- Inode Table:存放x相应Inode文件属性 如 文件大小,所有者,最近修改时间等。
- 数据区:存放文件内容.
根据不同区块功能的划分,创建文件需要四个步骤:
- 存储属性:内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
- 存储数据:根据存储数据需要在区块对应表修改标记,同时分配对应区块用以存储数据。
- 记录分配情况:内核在inode上的磁盘分布区记录了区块的分配情况。
- 添加文件名到目录:通过文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
目录文件的文件内容存储的是文件名和inode指针的对应关系。同时对应文件的文件名信息并没有放在inode结构中,而是放在了文件所在的目录下。
软硬连接
硬链接通俗的将,就是给文件取了一个别名,创建一个文件的硬链接时,文件名对应的Inode依然是相应文件的Inode,同时删除文件时,对应的硬链接数量会减少。
而软连接是创建一个新的文件,Inode也是一个新的Inode,但是运行软连接时会引用对应的链接文件。
静态库与动态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
生成静态库代码如下:
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
[root@localhost linux]# gcc main.c -L. -lmymath
-I 指定头文件路径
-L 指定库路径
-l 指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行。
库搜索路径:
- 从左到右搜索-L指定的目录。
- 由环境变量指定的目录 (LIBRARY_PATH)。
- 由系统指定的目录:/usr/lib、/usr/local/lib
生成动态库:
[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
shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
编译选项:
l:链接动态库,只要库名即可(去掉lib以及版本号)
L:链接库所在的路径.
当我们打包完成静态库时,是可以直接运行程序的。但是动态库则不然,需要做如下操作(任意一种都可以):
- 拷贝.so文件到系统共享库路径下, 一般指/usr/lib
- 更改 LD_LIBRARY_PATH
- ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
如果不执行以上操作,动态库就无法找到对应位置:
我们执行第二个方法:
再来看动态库,结果如下: