一、前置知识
描述一个文件可以用结构体 struct_file, 该结构体中存储着文件的属性信息, 而进程的 pcb 中通过一个指针指向一个结构体 struct struct_file, 结构体中主要存在一个存储 struct_file* 的数组, 该数组中存储着一个个被打开文件的 struct_file, 其中数组的下标称为文件描述符(fd), 而每个进程的 pcb 中的 struct struct_file 中的 struct_file* 的 0, 1, 2 位置被默认打开, 分别对应为:
- 0 : 标准输入(stdin)
- 1 : 标准输出(stdout)
- 2 : 标准错误(stderr)
所以往后进程打开的文件的 struct_file 会填充在最近最小未被使用的下标位置, 该下标就作为该文件的文件描述符, 通过文件描述符就可以在 struct_file* 数组中找到该文件并对其进行访问.
二、接口介绍
1.open
打开一个文件, 返回其 fd.
头文件:
#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);
- 调用失败返回 -1, 成功返回该文件的文件描述符
- pathname: 被打开文件的名称
- flags: 以什么方式打开该文件
- mode: 被打开文件的权限
通过内置宏设置 flags, 介绍几个常用的:
- O_CREAT: 如果 pathname 不存在则创建, 存在则直接打开
- O_RDWR: 读写打开
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_TRUNC: 覆盖式写入
- O_APPEND: 追加式写入
使用方式通过 按位或( | ) 方式拼接, 将 flags 当位图使用.
示例代码:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
//以追加只写的方式打开文件,权限全为可读可写
int fd = open("./test.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
return 0;
}
运行结果:
可以看到被打开文件是不存在的:
执行程序后:
符合预期, 不存在的文件被创建, 但是文件的权限并不符合预期, 此处的权限为 0664, 原因出在 umask 上, 可以看到默认创建文件时的掩码为:
这就导致了创建时权限对不上预期, 在代码中将该程序的 umask 修改为 0, 如下:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
umask(0);
int fd = open("./test.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
return 0;
}
此时重新编译后再运行:
权限就和预期的一致了.
2.write
向文件描述符为 fd 的文件中写入数据.
头文件:
#include <unistd.h>
函数声明:
ssize_t write(int fd, const void *buf, size_t count);
- 调用成功返回写入的字节数, 0 表示未写入任何内容, 失败返回 -1
- fd: 被写入文件的文件描述符
- buf: 从 buf 中读取数据写入 fd
- count: 要写入的字节数
示例代码:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
umask(0);
int fd = open("./test.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
char buf[1024] = "Hello World\n";
write(fd, buf, sizeof(buf));
return 0;
}
运行结果:
成功写入文件.
3.read
从文件描述符为 fd 的文件中读取数据.
头文件:
#include <unistd.h>
函数声明:
ssize_t read(int fd, void *buf, size_t count);
- 调用成功返回读取到的字节数, 失败返回 -1
- fd: 被读取文件的文件描述符
- buf: 从文件描述符为 fd 的文件中读取数据到 buf
- count: 要读取的字节数
示例代码:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
umask(0);
int fd = open("./test.txt", O_RDONLY | O_CREAT, 0666);
char buf[1024] = {'\0'};
cout << "读取前buf中的内容:" << buf << endl;
read(fd, buf, sizeof(buf));
cout << "读取后buf中的内容:" << buf << endl;
return 0;
}
在运行之前可以先往 test.txt 文件中写入一点信息, 以便于后续的观察, 运行结果:
4.close
关闭 open 打开的文件.
头文件:
#include <unistd.h>
函数声明:
int close(int fd);
- 关闭成功返回 0, 失败返回 -1
- fd: 要关闭的文件的文件描述符
示例代码:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
umask(0);
int fd = open("./test.txt", O_RDONLY | O_CREAT, 0666);
close(fd);
return 0;
}
前面也说过每个进程都默认打开 0, 1, 2 号 fd, 那么试着把 1 号 fd 关闭, 再打开一个文件看看其 fd 为多少, 示例代码:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
umask(0);
close(1);
int fd = open("./test.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
cout << fd << endl;
return 0;
}
运行结果:
编译结束后执行可执行文件, 按照预期应该是通过 cout << fd << endl; 在终端输出文件的 fd, 但是此时无任何显示, 来一下代码, 首先关闭了 1 号 fd, 而 1 号 fd 默认为 标准输出(stdin), 在关闭后打开了一个文件, 那么根据最近最小未被使用的原则, 分配给打开文件的 fd 就是 1, 那么此时处于 1 号 fd 的文件就成为了 标准输出(stdin), 而 cout 就是往 标准输出(stdin) 中输出信息, 此时是不是就成了往打开的文件中输入信息, 看看打开的文件:
可以看到本应该往屏幕输出的信息输入到了打开的文件中, 通过该实例能更好的理解 0, 1, 2 号 fd 的作用, 那么可以想到如果把 0 号 fd 关闭了, 其他文件占据了 0 号 fd, 那么默认从键盘读取数据会变成从打开的文件中读取数据, 也就是说 cin 是从 0 号 fd 所指向的文件中读取数据, cout 是将数据输出到 1 号 fd 所指向的文件中, cerr 是将错误信息输出到 2 号 fd 所指向的文件中, 也能更好的理解在 Linux 下一切皆文件这一概念.
三、软硬链接
在磁盘中, 找到一个文件并不是用文件名, 而是用 inode.
1.软链接
软链接是通过名字引用另外一个文件, 就类似快捷方式.
指令: ln -s 目标文件 自定义软链接名字
示例指令:
ln -s file1.txt file1-soft
执行指令:
通过 -i 选项可以查看文件的 inode, 进行软链接后, 通过 file1-soft 就可以访问到 file1.txt, 而如果 file1.txt 删除后,
file1-soft 也会失效, 如下:
因为目标文件已经不存在了, 所以通过该软链接理所当然的无法访问了.
2.硬链接
硬链接是通过 inode 引用另外一个文件, 在 Linux 中其实可以多个文件名对应一个 inode.
指令: ln 目标文件 自定义硬链接名字
示例指令:
ln file.txt file-hard
执行指令:
首先可以看到两个不同名称的文件的 inode 是相同的, 那么它们肯定指向同一文件, 而在权限列表和拥有者之间存在一列, 该列为硬链接数, 2 就表示目前是有两个文件名在引用该 inode, 删除某一个文件并不会影响另一个文件的访问, 因为其是通过引用计数来管理资源的释放的, 2 就相当于当前 inode 引用计数为 2, 删除一个文件先会减减引用计数, 然后判断当前的引用计数是否为 0, 如果为 0 则真正的删除释放该文件, 否则表示还有其它文件名在引用该 inode, 并不会删除释放文件.
接下来创建一个目录, 看看它的硬链接数为多少:
为 2, 如何得出 2, 进入 Dir 目录, 再查看:
可以看到 Dir 和 点(.) 的 inode 一致, 那么硬链接数为 2 就很明了了, 接下来在 Dir 中创建一个子目录, 再看看其硬链接数:
可以看到此时硬链接数为 3, 因为 Dir 的子目录 SonDir 中的 点点(…) 的 inode 也与其一致, 不难看出规律, 一个空目录的硬链接数为 2, 一个非空目录包含的子目录数等于其硬链接数 - 2.
需要注意的是, 不可以对目录进行硬链接:
但是可以对目录进行软链接:
四、动态库和静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库.
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码.
库命名规则: libxxx.so/libxxx.a
库的真名: 去掉前缀 lib 和去掉后缀 .so/.a, 剩于的 xxx 即为库的真名
我们在编写库后, 想把库分享给别人调用, 可以直接复制 .h 和 .cpp 文件给别人即可, 但是这样会暴露源码, 如果想不暴露源码安全一点的分享, 可以通过编译时加上 -c 选项形成 .o 文件:
再把 .h 和 .o 文件分享给别人即可, 别人在编译时带上 .o 文件并且在代码中包含头文件即可. 但是这种方法也不是很好, 如果存在大量的 .o 文件, 把一堆的 .o 文件传给别人也不太合理.
1.生成静态库
此时就可以把所有 .o 文件打包成一个静态库, 分享时分享该静态库即可, 打包指令如下:
ar -rc 库名 .o文件列表(多个.o文件用空格隔开)
比如:
ar -rc libmylib.a *.o
在执行指令之后会生成一个静态库:
将该静态库分享给别人即可, 别人在拿到静态库后, 在代码中包含头文件即可使用库中提供的函数等, 然后再经过编译即可生成可执行文件, 但不能直接编译, 直接编译会不通过:
需要在编译是加上选项, 指明库的路径和库的名称:
其中 -L 表示寻找库的路径, -l 表示库名, 注意这里的库名并不是 libmylib.a, 而是要去掉前缀和后缀. 此时改动一下代码, 把头文件剪切到别的文件夹里, 再按照刚才的方式进行编译:
此时会报错, 大致就是找不到该头文件, 此时只需在编译时加上 -大i 选项, 表示指明头文件路径, 如下:
ps: -大i, -L, -l 后面可以带空格再接参数, 也可以不带空格直接带参数.
另外也可以把我们编写的头文件添加到 /usr/include 目录下, 该目录为系统搜索头文件时的默认搜索路径, 然后将库文件添加到 /lib64 目录下, 如下:
此时别人在编译时只需要带上 -l库名 即可, 因为不是标准库, 而是第三方库, 所以要带上库名才行, 如下:
其实将下载下来的库, 拷贝到系统的默认路径下, 在 Linux 中就相当于安装库, 而将其在系统默认路径下删除就相当于卸载库.
2.生成动态库
如果存在大量 .o 文件, 直接分享是不合理的, 也可以把它们打成一个动态库, 如下:
1)先生成 .o 文件
在生成 .o 文件时, 不需要在使用 ar 指令, 直接使用 g++/gcc 即可, 只不过要带上选项 -fPIC.
2)将生成的所有 .o 文件打包成动态库
同样是用 g++/gcc 打包, 选项带上 -shared.
在生成了动态库之后, 把头文件和动态库分享给对方后, 对方直接带上选项编译后虽然会生成可执行文件, 但是无法执行, 如下:
会提示找不到动态库, 通过 ldd + 可执行程序文件名(查看可执行程序依赖的库) 也可以观察到:
原因在于运行的时候, 动态库并没有在系统的默认路径下, 所以系统找不到该动态库, 通过以下方法可以解决该问题:
-
环境变量
在 Linux 中存在着一个名为 LD_LIBRARY_PATH 的环境变量, 只需要将动态库路径添加到其中即可.
此时再通过 ldd 指令查看:
发现可以找到我们的第三方库了, 再次运行就没问题了:
-
软链接
可以通过在系统的指定路径下创建动态库的软链接来解决找不到动态库的问题, 如下:
在系统的指定路径下查看软链接:
此时就可以正常运行可执行程序了. -
配置文件
在 /etc/ld.so.conf.d/ 中存储着一些配置文件:
可以在该目录中创建一个配置文件, 名字可以自定义, 然后只需要将动态库的路径写到该文件中即可:
写入保存后再通过 ldd 发现还是没有找到依赖的库:
原因是需要对配置文件进行更新, 通过 ldconfig 进行更新:
再次通过 ldd 查看依赖的库, 已找到依赖的动态库, 此时也可以正常执行程序了:
五、补充
如果同时存在动态库和静态库, 系统默认优先使用动态库, 如下:
如果想要指定使用静态库, 在编译时需加上 -static 选项, 如下:
如果没有安装 glibc-static 就会出此错误, 先进行安装:
此时再进行编译就没问题了, 而且是链接的静态库: