一、回顾C文件接口
C文件接口是C语言提供的一组函数,用于与文件进行交互、读写文件内容以及进行文件的操作。在C文件接口中,文件指针是一种用于指示文件位置的特殊指针。它用于跟踪文件的读写位置,通过它可以定位文件中的特定位置,并进行读写操作。使用FILE
结构体表示文件指针。我们可以通过fopen
函数来打开一个文件,并获得一个指向该文件的文件指针。
fwrite 是 C 语言中用于将数据块写入文件的函数。它的声明如下:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
参数解释:ptr:指向要写入的数据的指针。 size:每个数据项的字节数。 count:要写入的数据项的数量。stream:指向要写入的文件的指针。fwrite 函数将 ptr 指向的数据块写入 stream 指向的文件。它首先将数据块从内存复制到缓冲区,然后再将缓冲区的数据写入文件。写入成功时,返回实际写入的数据项数量。如果返回值与 count 不相等,则表示写入发生了错误。
下面是一个hello.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;
}
fread
是 C 语言标准库中用于从文件中读取数据的函数。它的声明如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数解释:ptr
:指向要存储读取数据的内存块的指针。size
:每个数据项的字节数。nmemb
:要读取的数据项的数量。stream
:指向要读取的文件的指针。fread
函数从 stream
指向的文件中读取数据,每次读取 size
字节的数据,总共读取 nmemb
个数据项。它将读取的数据存储到 ptr
指向的内存块中,并返回实际成功读取的数据项数量。如果返回值与 nmemb
不相等,则可能表示发生了错误或到达了文件尾部。
下面是一个hello.c读文件示例:
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "r"); // 以读取模式打开文件
if (!fp) { // 检查文件是否成功打开
printf("fopen error!\n"); // 打印错误信息
}
char buf[1024]; // 定义缓冲区
const char *msg = "hello bit!\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;
}
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明 ssize_t s = fread(buf, 1, strlen(msg), fp);
在这里,
fread
函数的返回值s
表示成功读取的数据块数量,而不是字节数。这里使用strlen(msg)
作为要读取的字节数,可能会导致一些问题。因为strlen(msg)
返回的是字符串msg
的长度(不包括结尾的 null 字符),而fread
函数实际上是以字节数为单位读取数据的。因此,如果strlen(msg)
大于文件中剩余的可读数据量,那么fread
函数将尽可能地读取文件中的数据,直到达到msg
的长度为止。此时,fread
返回成功读取的字节数,但并不保证已经读取了完整的msg
长度的数据。这可能导致后续处理数据时出现问题。正确的做法是根据实际需要读取的字节数来调用
fread
函数,而不是依赖于字符串的长度。这样可以确保正确地读取数据,并避免因为数据不完整而出现问题。
在 C 语言中,`stdin`、`stdout` 和 `stderr` 是三个标准的文件流对象,分别代表标准输入、标准输出和标准错误输出。它们定义在 `<stdio.h>` 头文件中。
1. stdin:
'stdin'是标准输入流,通常用于从键盘或其他输入设备读取数据。默认情况下,C 程序从 'stdin'中读取输入。例如,使用 'scanf'函数可以从 'stdin' 中读取用户输入的数据。2. stdout:
'stdout' 是标准输出流,用于向屏幕或其他标准输出设备输出数据。默认情况下,C 程序向 'stdout' 输出内容。例如,使用 'printf' 函数可以将输出内容发送到 'stdout',从而显示在屏幕上。3. stderr:
'stderr' 是标准错误输出流,用于输出程序的错误信息和诊断信息。与 'stderr' 相比, 'stderr' 通常用于输出不是正常输出流的信息。 例如,在程序发生错误时,可以使用 'fprintf(stderr, ...)' 将错误信息输出到 'stderr' 中。这些标准流对象提供了简便的方式来处理输入、输出和错误信息,使得 C 程序的输入输出操作更加方便和标准化。在编写 C 程序时,可以利用这些标准流对象来进行输入、输出和错误处理。
二、系统文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问:
write 函数:在 C 语言中,类似于 write 函数的功能可以通过 fwrite 函数实现。fwrite 函数用于将数据块写入文件流,类似于将数据写入文件描述符中的 write 函数。
read 函数:在 C 语言中,可以使用 fread 函数来读取文件流中的数据块,类似于从文件描述符中读取数据的 read 函数。
close 函数:在 C 语言中,关闭文件流可以使用 fclose 函数来完成,类似于关闭文件描述符的 close 函数。
lseek 函数:在 C 语言中,可以使用 fseek 函数来移动文件流的读写位置,类似于 lseek 函数在文件描述符上进行的偏移操作。
以代码的形式,实现和上面一模一样的代码:
hello.c 写文件:
#include <stdio.h> // 用于使用 perror 函数 #include <sys/types.h> // 用于使用 open 函数的返回类型 #include <sys/stat.h> // 用于使用 open 函数的权限参数 #include <fcntl.h> // 用于使用 open 函数的打开模式参数 #include <unistd.h> // 用于使用 umask、open、write 和 close 函数 #include <string.h> // 用于使用 strlen 函数获取字符串长度 int main() { umask(0); // 设置文件创建的权限掩码为 0,即不进行权限屏蔽 int fd = open("myfile", O_WRONLY|O_CREAT, 0644); // 打开或创建一个文件,只写模式,权限为 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; }
hello.c读文件
#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("myfile", O_RDONLY); // 打开文件,只读模式 if(fd < 0){ perror("open"); // 如果打开文件失败,则输出错误信息 return 1; } const char *msg = "hello bit!\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
3.1 man 2 open
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。 参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 O_APPEND: 追加写
返回值:
成功:新打开的文件描述符(file descriptor)
失败:-1
mode_t mode 用于指定文件的权限模式。
mode 参数通常是一个八进制数,用于表示文件的权限位。常见的权限位包括:
S_IRUSR(用户可读)
S_IWUSR(用户可写)
S_IXUSR(用户可执行)
S_IRGRP(组可读)
S_IWGRP(组可写)
S_IXGRP(组可执行)
S_IROTH(其他人可读)
S_IWOTH(其他人可写)
S_IXOTH(其他人可执行)这些权限位可以按位或组合在一起,以指定完整的文件权限。例如,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH 表示文件所有者可读写,组可读,其他人可读。
3.2 open函数返回值
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而open close read write lseek 都属于系统提供的接口,称之为系统调用接口
看下面一张图
系统调用接口和库函数的关系,一目了然。 所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
3.2.1 文件描述符fd
Linux进程默认情况下会有3个缺省打开的文件描述符,
分别是标准输入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)); // 从标准输入中读取数据到buf中,并返回读取的字节数
if (s > 0) // 如果读取的字节数大于0
{
buf[s] = 0; // 在读取的内容后面添加字符串结束符'\0'
write(1, buf, strlen(buf)); // 将读取的内容写入标准输出
write(2, buf, strlen(buf)); // 将读取的内容写入标准错误输出
}
return 0;
}
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
3.2.2 文件描述符的分配规则
直接看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
// 打开名为"myfile"的文件,以只读方式打开,返回文件描述符fd
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是 fd: 3
关闭0或者2,在看代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0); // 关闭标准输入文件描述符(0表示标准输入)
// close(2);
int fd = open("myfile", O_RDONLY);
// 打开名为"myfile"的文件,以只读方式打开,返回文件描述符fd
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
3.2.3 重定向
那如果关闭1呢?看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1); // 关闭标准输出流(stdout)
int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
// 以只写方式打开或创建文件"myfile",设置文件权限为644,返回文件描述符
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd); // 打印文件描述符
fflush(stdout); // 刷新标准输出流
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, < 那重定向的本质是什么呢?
3.2.4 使用 dup2 系统调用
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2
函数用于复制文件描述符。具体而言,dup2
函数会将oldfd
文件描述符复制到newfd
文件描述符上,如果newfd
已经打开,则会先关闭它。其中:
oldfd:要复制的文件描述符
newfd:新的文件描述符该函数会返回复制后的文件描述符,或者在出现错误时返回 -1。
示例代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("./log", O_CREAT | O_RDWR);
// 创建或读写方式打开文件"./log",返回文件描述符
if (fd < 0) {
perror("open");
return 1;
}
close(1); // 关闭标准输出流(stdout)
dup2(fd, 1); // 复制文件描述符 fd 到标准输出流(stdout)
for (;;) {
char buf[1024] = {0}; // 定义缓冲区数组
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
// 从标准输入流(stdin)读取数据到缓冲区
if (read_size < 0) { // 如果读取出错
perror("read");
break;
}
printf("%s", buf); // 输出缓冲区内容
fflush(stdout); // 刷新标准输出流(stdout)
}
return 0;
}
printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1 下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写 入,进而完成输出重定向。
四、FILE
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。 所以C库当中的FILE结构体内部,必定封装了fd。
来段代码在研究一下:
#include <stdio.h>
#include <string.h>
#include <unistd.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(msg1), 1, stdout);
write(1, msg2, strlen(msg2));
// 使用 write 直接向文件描述符1(stdout)输出消息3
fork(); // 调用 fork 创建子进程
return 0;
}
运行出结果:
但如果对进程实现输出重定向呢? ./file >testfile , 我们发现结果变成了:
可以发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?这和 fork有关!
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据 的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区。 那这里缓冲区谁提供呢? printf ,fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统 调用的“封装”,但是 write 没有缓冲区,而 printf ,fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C库函数,所以由C标准库提供。
感有兴趣,可以看下FILE结构体:
五、理解文件系统
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。
每行包含7列:
模式 硬链接数 文件所有者 组 大小 最后修改时间 文件名
ll读取存储在磁盘上的文件信息,然后显示出来
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
上面的执行结果有几个信息需要解释清楚:
1. 文件名:file.c
- 这是文件的名称,这里文件是一个C语言源代码文件。2. 大小:273 字节
- 文件的大小为273字节,表示文件中包含的数据量。3. 所占数据块数:8
- 文件实际所占用的数据块数目为8,每个数据块大小为4096字节。4. IO 块大小:4096 字节
- 文件系统的I/O块大小为4096字节,这是文件系统用于操作和分配磁盘空间的基本单位。5. 文件类型:普通文件
- 这个文件是一个普通文件,包含文本数据而非目录、链接等特殊类型。6. 设备号:803h/205ld
- 文件所在的设备号信息,用于标识文件所在的设备。7. Inode 号:1543498
- 文件系统中inode号,用于唯一标识文件。8. 链接数:1
- 文件的硬链接数,即有多少个目录项指向该文件。9. 访问权限:0644/-rw-r--r--
- 文件的权限设置,0644表示文件所有者具有读写权限,但仅有读权限给组和其他用户。10. 用户 ID:0/root
- 文件所有者的用户ID和用户名,这里是root用户。11. 组 ID:0/root
- 文件所属组的组ID和组名,这里是root组。12. 上下文:unconfined_u:object_r:admin_home_t:s0
- 文件的安全上下文,提供关于文件如何被访问和操作的额外信息。13. 访问时间:2024-03-12 02:26:25.198573425 -0700
- 文件的最近访问时间。14. 修改时间:2024-03-12 02:26:22.215580152 -0700
- 文件的最近修改时间。15. 更改时间:2024-03-12 02:26:22.216580150 -0700
- 文件的状态更改时间。
inode
为了能解释清楚inode我们先简单了解一下文件系统
Linux的ext2文件系统是一种用于组织和管理数据存储的文件系统,而磁盘则是用于物理存储数据的设备。ext2文件系统可以被格式化并安装在磁盘上,以便在其中存储文件和目录。上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。启动块(Boot Block)的大小是确定的。
引导块(Boot Block)是指存储在计算机硬盘、软盘或其他启动设备的特定区域,用于引导计算机系统。它通常包含引导加载程序(Boot Loader),负责在计算机启动时加载操作系统内核进入内存并启动系统。
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了
GDT(Global Descriptor Table,全局描述符表):用于定义和管理系统中各个内存段的属性和访问权限。“Reserved GDT”指的是未使用的、保留的 GDT 条目,在实际的操作系统开发和使用中,这些保留的描述符可以根据需要进行配置和利用。
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
i节点表(Inode Table):存放文件属性 如 文件大小,所有者,最近修改时间等
数据区(Data Blocks):存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。
为了说明问题,我们将上图简化:
创建一个新文件主要有一下4个操作:
- 存储属性
内核先找到一个空闲的i节点(这里是154371)。内核把文件信息记录到其中。 - 存储数据
假设该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据 复制到300,下一块复制到500,以此类推。 - 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。 - 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(154371,abc)添加到目录文件。文件名和inode'间的对应关系将文件名和文件的内容及属性连接起来。
硬链接
在Linux系统中,每个文件都有一个被称为inode的索引节点,它包含了文件的元数据信息,比如文件类型、拥有者、权限等。硬链接是一种将多个文件名链接到同一个inode的技术。
当你使用'ln'命令创建硬链接时,实际上是在文件系统中创建了另一个指向同一个inode的文件名。这意味着无论使用哪一个文件名来访问文件,其实都是在访问同一个文件的内容。因此,无论你对其中一个文件名做出什么改变,其他所有的文件名都会反映这些改变,因为它们实际上指向同一个inode。在你的例子中,文件abc和def具有相同的inode号码263466,这表明它们是硬链接关系。当你删除一个硬链接时,只是减少了指向inode的链接数目。只有当链接数目降至0时,才会真正释放磁盘空间,因为没有任何文件名指向这个inode了。总之,硬链接允许一个inode有多个文件名引用,而且这些引用的文件名之间没有本质区别。
在Linux系统中,每个目录都拥有两个硬链接:一个指向自身('.'),另一个指向父目录('..')。这两个硬链接是系统创建目录时自动添加的,无论目录中是否包含其他文件或子目录,这两个硬链接总是存在的。即使在目录中并未创建其他文件或子目录,它仍然具有这两个硬链接。这是因为目录本身也是文件系统中的一个特殊文件,它需要这两个硬链接来确保文件系统结构的完整性:
指向自身(
.
):这个硬链接指向目录本身,允许你使用相对路径访问当前目录中的文件和子目录。如果没有这个硬链接,那么无法通过相对路径访问当前目录中的内容,会导致文件系统操作的混乱。指向父目录(
..
):这个硬链接指向当前目录的父目录,允许你通过相对路径访问父目录中的内容。这对于文件系统的导航和路径解析非常重要,确保了文件系统中各个目录之间的正确连接。
通过这两个硬链接,文件系统可以保持一致性,确保目录结构的正确性和可靠性。
因此,即使目录是空的,它的硬链接数目也会是2。
软链接
硬链接是通过inode引用另外一个文件。与硬链接不同,软链接创建的是一个特殊类型的文件,
其中包含指向另一个文件的路径信息。软链接实际上是一个指向目标文件的符号,而不是像硬链接那样直接指向inode。
在创建软链接时,如果原文件被删除或移动,软链接仍然存在,但指向的目标文件会失效,称为“悬空链接”。软链接可以是绝对路径或相对路径,这使得在不同文件系统或目录之间进行链接变得更加灵活。
使用`ln -s`命令可以创建软链接,例如:
软链接的一些特点包括:
1. 软链接可以跨越文件系统,因为它们只是包含了目标文件的路径信息。
2. 可以链接到目录,而硬链接不能。
3. 软链接可以链接到不存在的文件或目录,创建后再填充内容也能够访问。
总的来说,软链接提供了一种更加灵活的方式来创建文件之间的关联,允许用户创建指向其他文件或目录的符号链接。
六、动态库和静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
静态库和动态库的存在有各自的优势和用途:
代码重用:库文件(静态库或动态库)中包含了一组函数或类,可以被多个程序共享使用,实现了代码的重用。
减少代码冗余:静态库会将库文件的代码在编译时完全复制到可执行文件中,每个可执行文件都包含一份库文件的代码,可能造成代码冗余。而动态库则可以在运行时加载,多个程序可以共享同一个动态库,减少了冗余。
灵活性:动态库具有更大的灵活性,因为它们可以在运行时加载和卸载,允许动态更新库版本,而不需要重新编译整个程序。
节省内存:动态库在内存中只需要加载一份,多个程序可以共享,节省了内存空间。相比之下,静态库每个程序都包含一份库文件的代码,可能会占用更多的内存。
版本管理:动态库更容易进行版本管理,可以在系统中同时存在不同版本的动态库,并且程序可以根据需要加载特定版本的库。
准备测试程序
/add.h/
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif // __ADD_H__
/add.c/
#include "add.h"
int add(int a, int b)
{
return a + b;
}
/sub.h/
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif // __SUB_H__
/sub.c/
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
/main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main( void )
{
int a = 10;
int b = 20;
printf("add(10, 20)=%d\n", add(a, b));
a = 100;
b = 20;
printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}
6.1 生成静态库
ar
:是用于创建、修改和提取静态库的命令。-rc
:是ar
命令的选项。其中,r
用于替换或添加文件到静态库,c
用于创建一个新的静态库。libmymath.a
:是静态库的名称。通常以lib
开头,后面是库的名称,以.a
结尾。在这个例子中,静态库的名称为libmymath.a
。add.o sub.o
:是需要包含到静态库中的目标文件列表。在这个例子中,add.o
和sub.o
是两个目标文件。
查看静态库中的目录列表
ar
:是用于创建、修改和提取静态库的命令。-tv
:是ar
命令的选项。其中,t
用于显示静态库中的文件列表,v
用于输出详细信息。libmymath.a
:是要查看的静态库的名称。在这个例子中,我们查看的是名为libmymath.a
的静态库文件。
6.2 使用静态库
默认情况下,gcc会优先链接动态库,如果找不到动态库,才会尝试链接静态库。现在还没生成动态库。
main.c
:是要编译和链接的源代码文件。-L
:用于指定搜索库文件的路径。-L.
表示在当前目录搜索库文件。-lmymath
:需要链接的库文件。
6.3 库搜索路径
编译器在链接过程中按照以下顺序搜索库文件:
-L 指定的目录:编译器首先会按照 -L 选项指定的目录顺序进行搜索。这允许你显式地指定要搜索的特定目录。
环境变量 LIBRARY_PATH 指定的目录:如果没有找到所需的库文件,编译器会检查环境变量 LIBRARY_PATH 中指定的目录。你可以通过设置 LIBRARY_PATH 环境变量来扩展库文件的搜索路径。
系统指定的目录:如果库文件仍未找到,编译器会搜索系统默认的库文件目录。具体的系统默认目录可能因操作系统而异。
/usr/lib:大多数系统都会在此目录下存储常用的库文件。
/usr/local/lib:这也是另一个常见的默认库文件目录,通常用于存放本地安装的库文件。
按照这个顺序搜索库文件,编译器将尝试找到所需的库文件并完成链接过程。
6.4 生成动态库
gcc -fPIC -c sub.c add.c:这个命令编译了 sub.c 和 add.c 两个源代码文件。-fPIC 选项告诉编译器生成位置无关代码(Position Independent Code)以便后续生成共享库使用。编译完成后,会生成对应的目标文件 sub.o 和 add.o。
gcc -shared -o libmymath.so *.o:这个命令将目标文件 sub.o 和 add.o 进行链接,并生成名为 libmymath.so 的共享库文件。-shared 选项告诉链接器生成共享库。最后,*.o 表示链接所有以 .o 结尾的目标文件。链接完成后,会生成共享库文件 libmymath.so。
6.5 使用动态库
编译选项
l:链接动态库,只要库名即可(去掉lib以及版本号) L:链接库所在的路径.
运行动态库
系统报找不到名为libmymath.so的共享库文件,原因是因为系统的动态链接器没有在默认的库搜索路径中找到该文件。
1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib
2、确认/etc/ld.so.conf.d/
目录下的配置文件中包含系统共享库路径:
如果没有的话,可以创建一个新的配置文件,并添加系统共享库路径:
echo "/usr/lib" | sudo tee /etc/ld.so.conf.d/mylibs.conf
该命令的目的是将"/usr/lib"路径写入到/etc/ld.so.conf.d/mylibs.conf配置文件中,以便系统在运行ldconfig命令时能够正确地找到并更新动态链接库缓存。
echo "/usr/lib":该部分使用echo命令将"/usr/lib"字符串输出。
|:管道符号,将前一个命令的输出传递给下一个命令作为输入。
sudo tee /etc/ld.so.conf.d/mylibs.conf:该部分使用tee命令将前一个命令的输出写入到/etc/ld.so.conf.d/mylibs.conf文件中
3、运行ldconfig
命令来更新动态链接库缓存:
4、更改 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=.
:将当前目录(.
)作为共享库文件的搜索路径,这样系统会在当前目录中查找动态(共享)库文件。
gcc main.c -o main -L. -lmymath
:使用gcc编译器编译main.c文件,生成可执行文件main。在编译过程中,通过-L.
选项告诉编译器在当前目录中查找库文件,用-l
选项时只需要指定库的名称,而不需要包含前缀lib
或文件扩展名.so,-lmymath
指定链接名为libmymath.so的共享库文件。
./main
:运行生成的可执行文件main。
如果同时存在同名的静态库和动态库文件,gcc会优先链接动态库文件。如果你想明确链接静态库,可以使用-static
选项:
gcc main.c -o main -L. -lmymath -static
6.6 使用外部库
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来用于处理数学运算和科学计算的函数(math库)
#include <math.h>
#include <stdio.h>
int main(void)
{
double x = pow(2.0, 3.0);
printf("The cubed is %f\n", x);
return 0;
}
-lm表示要链接数学库(libm.so或者libm.a)
库文件名称和引入库的名称
如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a