C文件IO相关接口
fopen函数
- pathname: 要打开的文件名字符串
- mode: 访问文件的模式
模式 | 描述 | 含义 |
---|---|---|
“r” | 读 | 文件不存在失败返回null |
“r+” | 读写 | 文件不存在打开失败返回null,文件存在则从头开始覆盖现有的数据(不会清空数据) |
“w” | 写 | 文件不存在创建文件,文件存在则清空文件 |
“w+” | 读写 | 文件不存在创建文件,文件存在清空文件 |
“a” | 写 | 文件不存在创建文件,文件存在数据被附加到文件末尾 |
“a+” | 读写 | 文件不存在创建文件,文件存在数据被附加到文件末尾 |
fclose函数
fwrite函数
情景一:
在没有myfile文件的情况下一”w“方式打开文件。
#include<stdio.h>
#include<string.h>int main()
{FILE* fd=fopen("myfile","w");if(!fd){printf("fopen error!\n");}const char *msg="hello world\n";int cnt=6;while(cnt--){fwrite(msg,strlen(msg),1,fd);}fclose(fd);return 0;
}
以上的代码会出现的情况:在当前目录创建了一个叫myfile的文件,并且会往文件中写入数据。
情景二:
存在myfile 文件的时候使用”r“方式打开文件
#include<stdio.h>
#include<string.h>
#include <unistd.h>
#include <sys/types.h>int main()
{FILE*fd=fopen("myfile","r");if(!fd){printf("fopen error!\n");}char buf[1024];const char*msg="hello world\n";while(1){ssize_t s=fread(buf,1,strlen(msg),fd);if(s>0){buf[s]=0;printf("%s",buf);}if(feof(fd)){break;}}fclose(fd);return 0;
}
以上的代码会出现的情况:会读取当前目录下的myfile文件,并且会往文件中的数据读取到显示器上。
feof是检测流上的文件结束符的函数,如果文件结束,则返回非0值,否则返回0;站在光标所在位置,向后看还有没有字符,如果有,返回0;如果没有,返回非0。它并不会读取相关信息,只是查看光标后是否还有内容。
- C语言会默认打开三个输入输出流,分别是stdin,stdout,stderr
系统接口访问文件
open函数
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
**这三个常量,必须指定一个且只能指定一个 **
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
write函数
write(int fd, const void *buf, size_t count);
第一个参数 文件描述符fd
第二个参数 无类型的指针buf,可以存放要写的内容
第三个参数 写多少字节数
返回值:如果顺利write()会返回实际写入的字节数(len)。错误发生时则返回-1。
close函数
#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 world\n";char buf[1024];while (1){ssize_t s = read(fd, buf, strlen(msg)); // 类比writeif (s > 0){printf("%s", buf);}else{break;}}close(fd);return 0;
}
文件描述符
在Linux操作系统中,文件描述符是一种用于访问文件或输入/输出资源的抽象概念,它是为了更有效地管理和操作文件、设备、套接字等资源而引入的。文件描述符的作用和重要性在操作系统和编程中具有深远意义。
- Liunx x进程默认情况下会打开的文件描述符,分别是标准输入0, 标准输出1,标准错误2.
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.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;
}
文件描述符是一个下标,是用来标识和操作文件或者输入输出设备的整数。每个打开的文件(包括标准输入、标准输出和标准错误输出)都会被分配一个唯一的文件描述符。
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
我们平常的输入是从标准输入(键盘)读取,输出往标准输出(显示器)打印!输入和输出重定向就是不再从标准输入读取,向标准输出打印了,就是从指定的文件读取,或只输入到指定文件。
输出通常包含两种类型:
- 一种是程序运行的结果,即该程序生成的数据;
- 另一种是状态和错误信息,表示当前程序的运行状况。
什么是输入:
默认情况下,标准输入连接到键盘。I/O重定向功能可以改变输出内容发送的目的地,也可以改变输入内容的来源地。
前提知识:
操作系统内部只认识只认识文件标识符fd,并不认识所谓的stdin、stdout。
文件描述符的分配规则是从小到大没有被分配的文件描述符分配给新文件。
总结:
如果我们关闭标准输出,然后打开指定的文件,这时新打开的文件fd的值,就是我们刚刚关闭的标准输出文件的fd的值,所以我们就可以把输出定向到这个文件。这就是我们说的输出重定向,输入重定向也是一样的。
#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, 00644);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}
问题:
- 重定向的本质是什么?
就是修改特定的文件描述符。
dup2系统调用
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2函数是Unix/Linux系统提供的一个系统调用函数其作用是
- 复制文件描述符
- 指定新的文件描述符
参数:
- oldfd表示待复制的文件描述符
- newfd表示新的文件描述符
返回值:
该函数返回新的文件描述符,若出错则返回-1。
工作原理:
- 检查oldfd和newfd是否相等,若相等则不进行任何操作,直接返回newfd;
- 检查newfd的合法性,若已经打开,则先关闭;
- 复制oldfd的文件表项到newfd,使得两者指向同一个文件表项;
- 返回newfd。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{int fd = open("./log", O_CREAT | O_RDWR,0666);if (fd < 0){perror("open");return 1;}close(1);dup2(fd, 1);for (;;){char buf[1024] = {0};ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0){perror("read");break;}printf("%s", buf);fflush(stdout);}return 0;
}
过程:
通过open函数打开文件,如果不存在就创建,存在就打开文件;然后关闭close描述符;在使用dup2函数复制描述符;最后把输入的东西输出到文件中。
FILE
本质就是C语言提供的结构体
struct _iobuf {char *_ptr; // 文件输入的下一个位置int _cnt; // 当前缓冲区位置char *_base; // 文件起始位置int _flag; // 文件标志int _file; // 文件有效性验证int _charbuf; // 当前缓冲区状况int _bufsiz; // 缓冲区大小char *_tmpfname; // 临时文件名};typedef struct _iobuf FILE; //别名FILE,可以直接用
缓存区
缓冲区(Buffer)是内存空间的一部分。也就是说,在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区。
缓存区根据对应的设备分为:
输入缓存区和输出缓存区
缓存区的分类
完全缓存:就是等整个缓存区被填满,才会被刷新缓存。
无缓存:不对输入输出操作进行缓存,对流的读写可以立即操作实际文件。
行缓存:当一行结束的时候便开始刷新缓存,想要刷新行缓存,只要使用换行符’\n’便成功。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.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;
}
运行结果:
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和 fork有关!
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲。
问题:
- 缓存区存在的意义?
缓存主要是为了提高数据的读取速度。因为服务器和应用客户端之间存在着流量的瓶颈,所以读取大容量数据时,使用缓存来直接为客户端服务,可以减少客户端与服务器端的数据交互,从而大大提高程序的性能。
文件系统
文件系统的底层原理
Linux文件系统中的文件是数据的集合,文件系统不仅包含着文件中的数据而且还有文件系统的结构,所有Linux 用户和程序看到的文件、目录、软连接及文件保护信息等都存储在其中。
底层原理图:
每行包括7列:(模式、硬链接数、文件所有者、组、大小、最后修改时间、文件名)
产生inode的前提?
文件是存储在硬盘上的。硬盘的最小单位是扇区,每个扇区的大小为512字节。如果系统在读取硬盘数据的时候按扇区一个一个来读取,那效率就太低了,而是一次连续性读取多个扇区,所以设计者又将多个扇区整合成一个块(block),所以,块就是文件存取的最小单位。一个块的大小为4k。我们现在已经有了块的概念,文件数据就是存放在块中。但光有数据还是不行啊?为了方便管理文件,我们还需要文件的元信息,比如文件的属性,创建时间,权限,所占的块大小,数量等等。
什么是inode?
inode 是一个关键的元数据结构,每个文件或目录都有一个唯一的 inode 号码来标识它。inode 存储了文件的类型、所有者、权限、时间戳、数据块分配情况等信息,而文件的实际内容则存储在数据块中。通过 inode,文件系统可以快速地查找和访问文件的相关信息,而不必遍历整个文件系统。所以硬盘在分区的时候会分为两个区域,一个区域存放数据,一个区域存放inode信息。
还可以通过stat命令看到更多的信息
文件名
inode编号
文件拥有者uid
文件的所属用户组 gid
文件的可读,可写,可执行权限 :Access: (0755/drwxr-xr-x)
文件的时间戳:
- access time : 文件上一次打开的时间
- modify time:文件内容上一次修改的时间
- change time :文件的inode信息上一改变的时间
硬链接数:links
文件数据的所占用的块:blocks
文件所占用的字节数 size
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了
GDT,Group Descriptor Table:块组描述符,描述块组属性信息
块位图(Block Bitmap):Block Bitmap中记录着DataBlock中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
数据块:存放文件内容
文件系统是什么
文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构,即在存储设备上组织文件的方法。操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。
常见的几种文件结构:
文件系统的作用
文件系统的功能包括:管理和调度文件的存储空间,提供文件的逻辑结构、物理结构和存储方法;实现文件从标识到实际地址的映射,实现文件的控制操作和存取操作,实现文件信息的共享并提供可靠的文件保密和保护措施,提供文件的安全措施。
文件系统的分类
FAT(File Allocation Table)
是一种最早的文件系统类型,常用于Windows系统,具有简单、易于实现和兼容性好等特点,但是对于大容量硬盘的支持不太好。
NTFS(New Technology File System)
是Windows NT及其后续版本的默认文件系统类型,支持文件和目录的加密、压缩、权限控制等高级功能,适合大容量硬盘的管理。
EXT(Extended File System)
是Linux系统的默认文件系统类型,具有高效、稳定、可靠等特点,支持大容量硬盘和文件的管理。
HFS(Hierarchical File System)
是Mac OS系统的默认文件系统类型,具有对于苹果设备的兼容性好、支持高级功能等特点。
APFS(Apple File System)
是苹果公司开发的新一代文件系统类型,适用于苹果设备上的存储管理,具有高效、安全、可靠等特点。
文件的操作
- Create,创建不包含任何数据的文件。调用的目的是表示文件即将建立,并对文件设置一些属性。
- Delete,当文件不再需要,必须删除它以释放内存空间。为此总会有一个系统调用来删除文件。
- Open,在使用文件之前,必须先打开文件。这个调用的目的是允许系统将属性和磁盘地址列表保存到主存中,用来以后的快速访问。
- Close,当所有进程完成时,属性和磁盘地址不再需要,因此应关闭文件以释放表空间。很多系统限制进程打开文件的个数,以此达到鼓励用户关闭不再使用的文件。磁盘以块为单位写入,关闭文件时会强制写入最后一块,即使这个块空间内部还不满。
- Read,数据从文件中读取。通常情况下,读取的数据来自文件的当前位置。调用者必须指定需要读取多少数据,并且提供存放这些数据的缓冲区。
- Write,向文件写数据,写操作一般也是从文件的当前位置开始进行。如果当前位置是文件的末尾,则会直接追加进行写入。如果当前位置在文件中,则现有数据被覆盖,并且永远消失。
- append,使用 append 只能向文件末尾添加数据。
- seek,对于随机访问的文件,要指定从何处开始获取数据。通常的方法是用 seek 系统调用把当前位置指针指向文件中的特定位置。seek 调用结束后,就可以从指定位置开始读写数据了。
- get attributes,进程运行时通常需要读取文件属性。
- set attributes,用户可以自己设置一些文件属性,甚至是在文件创建之后,实现该功能的是 set attributes 系统调用。
- rename,用户可以自己更改已有文件的名字,rename 系统调用用于这一目的。
硬链接
什么是硬链接?
由于linux下的文件是通过索引节点(Inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配inode。每添加一个一个硬链接,文件的链接数就加1。
硬连接之间没有主次之分,删除某个硬链接,只是将其从目录的数据块中删除相关信息,并且文件链接数减一。不会从inode表中删除inode,除非只剩下一个链接数。
硬链接的作用?
- 文件引用:硬链接为文件和目录提供了额外的引用。一个文件可以有多个硬链接,每个链接都指向文件系统中的同一个inode(索引节点)。
- 不增加inode:创建硬链接不会为文件增加新的inode。相反,它只是增加了对已有inode的引用计数。
- 跨目录访问:硬链接可以位于不同的目录中,从而允许用户从多个位置访问同一个文件。
- 删除与inode:当删除一个文件的最后一个硬链接时,该文件的inode和相关的数据块才会被释放。如果文件还有其他硬链接,那么删除其中一个链接并不会影响文件的内容。
- 限制:不能跨文件系统创建硬链接(即两个硬链接必须位于同一文件系统中)。此外,不能为目录创建硬链接(尽管技术上可能,但大多数系统出于安全和设计的考虑禁止这样做)。
- 不影响原文件:与软链接(符号链接)不同,硬链接直接指向文件的inode,而不是文件名。因此,即使更改了原始文件名或移动了文件,硬链接仍然有效。
- 权限:硬链接继承了原始文件的权限。因此,用户必须具有适当的权限才能访问或删除硬链接。
- 大小与空间:硬链接本身不占用额外的磁盘空间(除了链接名本身在目录项中所需的少量空间)。它们只是指向文件系统中已存在数据的指针。
- 目录的硬链接:尽管不能直接为目录创建硬链接,但目录本身是其内容的硬链接集合。当在目录中创建一个新文件或子目录时,该目录的链接计数会增加。
怎么使用硬链接?
在Linux系统中,可以使用ln命令来创建硬链接。
ln [options] target_file link_name
- target_file:目标文件,即你希望创建硬链接的文件。
- link_name:链接名,即你希望给目标文件创建的硬链接的名称。
如果你有一个名为original_file.txt的文件,并希望为它创建一个名为alias_file.txt的硬链接,你可以使用以下命令:
ln original_file.txt alias_file.txt
执行此命令后,alias_file.txt将成为original_file.txt的一个硬链接。它们将共享相同的inode和文件数据。
硬链接的应用?
- 多用户共享:当多个用户需要访问同一份文件时,可以通过创建硬链接的方式,使每个用户都能在自己的目录下访问到该文件,而不需要复制多份文件。
- 跨目录访问:硬链接可以位于不同的目录中,方便用户从多个位置访问同一个文件。
软连接
什么是软连接?
- 软连接:一种特殊的文件或目录链接方式,与硬链接(Hard Link)不同,软连接不会导致文件或目录的硬链接,而是一种特殊的符号链接。
特点和用途
- 符号链接:软连接(或软链接)包含了另一个文件的路径名,它指向的是文件或目录的路径,而不是实际的inode或数据块。
- 多个拷贝管理:主要用于表示一个文件的多个拷贝。当一个文件有多个拷贝时,通常会选择一个主拷贝,而将其他拷贝指向主拷贝,这样可以避免因为多个拷贝导致的资源冲突。
- 链接方式:
- 可以链接到任意文件或目录。
- 可以链接不同文件系统的文件。
- 链接文件甚至可以链接不存在的文件,产生“断链”现象。
- 链接文件甚至可以循环链接自己,类似于编程语言中的递归。
- 操作影响:
- 对符号文件进行读或写操作时,系统会自动把该操作转换为对源文件的操作。
- 删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。
- 创建方法:创建过程简单,只需在当前目录下创建一个文本文件,在其中输入软连接的名称和目标文件的路径,即可创建一个软连接。
软连接的作用?
- 方便性:
软连接为文件或目录提供了一个快捷方式或别名。这允许用户或程序在不移动文件或目录的情况下,通过软连接轻松访问它们。这对于经常需要访问某些文件或目录的用户来说非常有用。 - 灵活性:
由于软连接指向的是目标文件或目录的路径,而不是直接指向文件内容或inode,因此它们可以跨文件系统创建。这意味着你可以在一个文件系统中创建一个指向另一个文件系统中文件或目录的软连接。这种灵活性使得软连接在管理和组织分布式文件系统中特别有用。 - 动态性:
与硬链接不同,软连接允许目标文件或目录被移动、重命名或删除,而不会破坏软连接本身。软连接只是一个指向目标文件或目录的路径,如果目标不存在,软连接将变为“死链接”或“断链”,但软连接本身仍然存在。这种动态性使得软连接在某些场景中(如软件部署和配置)特别有用,因为它们可以指向可能随时更改的资源文件或配置文件。 - 节省空间:
虽然软连接本身并不节省磁盘空间(因为它们需要存储目标文件或目录的路径),但它们可以避免在多个位置复制相同的文件或目录,从而节省总体存储空间。通过创建软连接,你可以在不同的位置引用相同的文件或目录,而无需为每个位置复制整个文件或目录。 - 易于管理:
软连接提供了一种简单的方法来组织和管理文件系统。**通过创建软连接,你可以将相关的文件或目录组织在一起,即使它们在物理上位于不同的位置。**这使得文件系统的结构更加清晰和易于理解,从而提高了管理效率。 - 兼容性:
软连接在许多Unix和类Unix系统(如Linux)以及Windows系统中都受到支持。这使得在不同操作系统之间共享和使用文件变得更加容易。通过创建软连接,你可以在不同的系统上访问和引用相同的文件或目录,而无需担心兼容性问题。
怎么使用软连接?
- 创建软连接
使用ln命令和-s选项来创建软连接。基本语法如下:
ln -s [目标文件或目录] [软连接名称]
例如,要在/home/user目录下为/etc/passwd文件创建一个名为passwd_link的软连接,可以执行以下命令:
ln -s /etc/passwd /home/user/passwd_link - 验证软连接
可以使用ls -l命令来查看软连接的详细信息。对于软连接,你会看到类似lrwxrwxrwx的权限字符串,后面跟着->符号和指向的目标文件或目录的路径。
ls -l /home/user/passwd_link
输出
lrwxrwxrwx 1 user user 11 Jun 30 15:00 /home/user/passwd_link -> /etc/passwd - 使用软连接
一旦创建了软连接,你就可以像使用普通文件或目录一样使用它。例如,如果你有一个指向可执行文件的软连接,你可以直接运行它。如果你有一个指向目录的软连接,你可以像访问普通目录一样浏览它。 - 修改软连接指向
如果你想修改软连接指向的目标文件或目录,你需要删除原有的软连接,并创建一个新的指向新目标的软连接。不过,也可以使用ln命令的-snf选项来直接修改软连接的指向:
ln -snf [新的目标文件或目录] [软连接名称] - 注意事项
- 删除软连接:与普通文件相同,使用rm命令删除软连接。删除软连接不会影响目标文件或目录。
- 跨文件系统:软连接可以跨文件系统创建,因为它们只是指向目标文件或目录的路径。
- 权限:软连接的权限与其指向的目标文件或目录的权限无关。但是,你需要有足够的权限来访问目标文件或目录,以便能够使用软连接。
- “断链”或“死链”:如果软连接指向的目标文件或目录被删除或移动,软连接将变为“断链”或“死链”。此时,尝试访问软连接将失败。
软连接的应用?
- 版本管理:在开发过程中,开发人员可能需要使用多个版本的库或工具。软连接可以用于快速切换不同版本的库或工具,而不必更改代码中的引用路径。
- 动态库管理:软连接可用于管理动态库,允许程序在运行时链接到不同的库版本。
动态库和静态库
静态库
什么是静态库?
定义
静态库是指在开发应用程序时,将一些公共的、需要反复使用的代码编译成的一个“库”文件。在链接步骤中,连接器会从静态库文件中取得所需的代码,并将这些代码直接复制到生成的可执行文件中。
特点
- 代码复制:在链接过程中,静态库中的代码会被直接复制到可执行文件中,因此可执行文件会包含一份完整的库代码拷贝。
- 多份冗余:由于每个使用到静态库的可执行文件都会包含一份完整的库代码拷贝,因此当静态库被多个程序使用时,会在内存中产生多份冗余的库代码拷贝。
- 编译依赖:静态库需要在编译时确定所有依赖关系,因此一旦静态库更新,所有使用它的程序都需要重新编译。
- 文件类型:在Windows平台上,静态库的文件扩展名通常为.lib;在Linux平台上,静态库的文件扩展名通常为.a。
优缺点
- 优点:
- 可执行文件不依赖于外部库文件,运行更稳定。
- 编译后的可执行文件体积相对较大,但包含了所有必要的代码和数据,便于分发。
- 缺点:
- 当静态库更新时,所有使用它的程序都需要重新编译。
- 如果多个程序使用同一个静态库,会导致内存中存在多份冗余的库代码拷贝,浪费内存资源。
使用方法
在开发过程中,可以通过配置编译器和链接器来使用静态库。通常需要将静态库文件添加到项目的链接器设置中,以便在编译时正确链接到静态库中的代码。具体步骤可能因开发环境和编程语言的不同而有所差异。
如何生成静态库?
- 编写源代码
- 创建源文件(例如:file1.c、file2.c等),并编写所需的函数实现。
- 编译源代码生成目标文件
- 使用编译器(如gcc)将源文件编译为目标文件(.o文件)。
gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
# 以此类推,为所有源文件生成目标文件
3.创建头文件
- 为库函数创建一个头文件(.h文件),声明库文件中的函数。
- 打包目标文件
- 使用归档工具(如ar)将多个目标文件打包成一个静态库文件(.a文件)。
ar rcs libmylib.a file1.o file2.o
# 其中libmylib.a是生成的静态库文件名,rcs是ar命令的选项,
# r表示替换,c表示创建,s表示创建目标文件索引
# 查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
注意事项:
- 静态库的名字通常以lib开头,以.a结尾(例如:libmylib.a)。
- 静态库在链接时会被完全嵌入到可执行文件中,因此生成的可执行文件会比较大。
- 如果静态库更新,使用它的程序需要重新编译。
怎么使用静态库?
- 引入头文件
- 首先,在你的源代码文件中包含(#include)静态库提供的头文件。这些头文件通常包含库中函数和变量的声明。
#include "library.h" // 假设这是静态库提供的头文件
- 配置编译器和链接器
- 在编译你的程序时,需要告诉编译器在哪里找到静态库的头文件,以及告诉链接器在哪里找到静态库的二进制文件。这通常通过编译器的命令行选项或项目配置设置来完成。
编译器选项(指定头文件搜索路径)
- -I(大写i)选项用于指定编译器搜索头文件的路径。
gcc -I/path/to/library/headers your_program.c -o your_program
链接器选项(指定库文件路径和名称)
- -L(大写L)选项用于指定链接器搜索库文件的路径。
- -l(小写L)选项用于指定要链接的库名(注意,不需要库文件的前缀lib和后缀.a或.lib)。
gcc your_program.c -L/path/to/library -llibrary -o your_program
在这个例子中,-llibrary告诉链接器链接到名为library的库,链接器会在-L指定的路径下查找名为liblibrary.a(在Unix系统上)或library.lib(在Windows系统上)的文件。
注意事项和归纳
- 头文件和库文件的路径:确保你的编译器和链接器能够找到静态库的头文件和二进制文件。这可能需要设置编译器和链接器的搜索路径。
- 库名:在链接器选项中指定库名时,不需要包含lib前缀或文件扩展名(如.a或.lib)。
- 跨平台考虑:不同的操作系统和编译器可能有不同的静态库文件格式和链接选项。例如,Windows上的静态库通常以.lib为扩展名,而Unix系统(如Linux)上的静态库通常以.a为扩展名。
- 项目配置:如果你在使用集成开发环境(IDE),如Visual Studio、Xcode或CLion等,你可能需要在项目的配置设置中指定头文件和库文件的路径,以及要链接的库名。
- 错误处理:如果在编译或链接过程中遇到错误,仔细检查编译器和链接器的输出信息,以确定问题的原因。常见的错误可能包括找不到头文件、找不到库文件、未定义的引用等。
动态库
什么是动态库?
定义
动态库,又称为动态链接库(Dynamic Link Library,简称DLL),在Windows系统中通常以.dll为文件扩展名,而在Linux或Unix系统中则通常以.so为文件扩展名。动态库在程序运行时被加载到内存中,与程序进行动态链接。
特点
- 动态加载:动态库在程序运行时被加载到内存中,而不是在编译时。这意味着程序在运行时可以决定是否需要加载某个动态库,从而实现代码的灵活加载。
- 代码共享:由于动态库是存储在系统的某个特定位置(如Linux下的/usr/lib或/lib目录),因此多个程序可以共享同一个动态库的实例。这有助于减少内存占用,提高系统资源的利用率。
- 更新方便:当动态库更新时,只需要替换旧的动态库文件,而无需重新编译或链接使用它的程序。这使得软件的更新和维护变得更加方便。
- 函数导出和调用:动态库通常包含一个或多个导出的函数,这些函数可以被其他程序调用。通过动态链接,程序可以调用不属于其可执行代码的函数,从而实现代码的复用和模块化。
使用方式
在使用动态库时,需要在编译时指定动态库的接口,而不是将其代码直接复制到可执行文件中。这意味着在编译时,编译器会生成一个对动态库接口的引用,而不是将动态库的代码嵌入到可执行文件中。在程序运行时,操作系统会根据这个引用加载相应的动态库到内存中,并将程序与动态库进行链接。
示例
在Linux系统中,可以使用gcc编译器和ld链接器来创建和使用动态库。首先,通过gcc编译器将源文件编译为目标文件(.o文件),并使用-fPIC选项生成位置无关的代码。然后,使用gcc的-shared选项将目标文件链接为动态库(.so文件)。最后,在编译使用动态库的程序时,需要指定动态库的接口(即头文件)和动态库文件的路径(使用-L选项指定路径,-l选项指定库名)。
注意事项
- 依赖关系:由于动态库是在程序运行时加载的,因此程序需要知道动态库的位置和名称。这通常通过环境变量(如LD_LIBRARY_PATH)或系统配置文件(如/etc/ld.so.conf)来指定。
- 版本兼容性:不同版本的动态库可能具有不同的接口和功能。因此,在更新动态库时,需要确保新的动态库与程序兼容,否则可能会导致程序崩溃或功能异常。
- 安全性:由于动态库可以被多个程序共享,因此需要确保动态库的安全性和稳定性。不当的动态库更新或恶意动态库注入可能会导致系统崩溃或数据泄露等安全问题。
如何生成动态库?
- 准备源文件
首先,你需要有包含你想要在动态库中实现的函数和变量的源文件(.c或.cpp文件)。 - 编译为位置无关代码(Position Independent Code, PIC)
使用GCC编译器,你需要为源文件生成位置无关的代码(PIC)。这通常通过使用-fPIC(或-fpic,对于小模型)选项来完成。
gcc -c -fPIC source_file.c -o source_file.o
在这里,source_file.c是你的源文件,source_file.o是生成的目标文件。
3. 链接目标文件以生成动态库
使用GCC的-shared选项,你可以将目标文件链接成一个动态库。假设你有一个或多个目标文件,你可以将它们全部链接到一个动态库中。
gcc -shared -o libmy_library.so source_file1.o source_file2.o ...
在这里,libmy_library.so是你想要生成的动态库文件的名字。
归纳
生成动态库主要涉及以下几个步骤:
- 编译源文件为PIC:使用-fPIC选项编译源文件为目标文件。
- 链接目标文件:使用-shared选项将目标文件链接为动态库。
怎么使用动态库?
- 链接库文件
- 在编译时:将动态库文件(如.dll在Windows上,.so在Linux上)的路径添加到链接器的搜索路径中。这通常通过编译器的命令行参数完成,例如使用-L选项指定库文件所在的目录。
- 指定库名:在链接器的命令行参数中,使用-l选项(小写L)指定要链接的库名(注意,不需要包含库文件的前缀和后缀)。
- 导入函数
- 在程序中声明:声明需要使用的库函数。这通常通过包含相应的头文件来实现,头文件中包含了函数的声明。
- 链接器替换:在编译时,链接器会使用库文件中的函数实现替换这些声明,以便在程序中调用它们。
- 运行时加载库
- 调用加载函数:在程序运行时,通过调用操作系统提供的加载库函数来加载动态库。在Windows上,这通常使用LoadLibrary函数;在Linux上,使用dlopen函数。
- 指定库文件路径:加载库时,需要指定库文件的路径,并获取一个句柄或指针,以便后续使用。
- 使用库函数
- 通过句柄或指针调用:一旦库文件加载完成,就可以通过之前获取的句柄或指针来调用库函数了。在Windows上,使用GetProcAddress函数获取函数地址;在Linux上,同样使用dlsym函数。
- 传递参数并处理结果:调用库函数时,需要传递所需的参数,并处理函数的执行结果。
- 卸载库
- 释放资源:在程序不再需要使用动态库时,应该调用操作系统提供的卸载库函数来释放库文件所占用的资源。在Windows上,使用FreeLibrary函数;在Linux上,使用dlclose函数。
归纳
使用动态库主要涉及以下几个关键步骤:
- 链接库文件:在编译时指定库文件的路径和名称。
- 导入函数:在程序中声明并使用库函数。
- 运行时加载库:在程序运行时加载动态库文件,并获取句柄或指针。
- 使用库函数:通过句柄或指针调用库函数,并处理结果。
- 卸载库:在程序结束时释放库文件所占用的资源。
怎么安装动态库?
- 直接安装到系统中
既然在系统默认的搜索路径下找不到我们的库文件和头文件,我们就将它们拷贝到系统的默认搜索路径中。
拷贝头文件
sudo cp mylib/include/.h /usr/include/
拷贝库文件
sudo cp mylib/lib/.so /lib64
- 建立软连接
ln -s mylib/lib/libmylib.so libmylib.so
删除当前软链接的指令为 rm -rf libmylib.so;如果使用 rm -rf libmylib.so/ 则会将该软链接指向的目录全部清空!
- 设置环境变量
系统在运行的时候会去帮我们找我们的库,去哪里找呢?除了系统默认库路径下去找,还会去LD_LIBRARY_PATH 加载库的环境变量中去找!
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/library
但是当前这种方法导环境变量是内存级的,当我们退出后重新进入它就没了
- 直接更改系统动态配置文件
在系统中存在一个 /etc/ld.so.conf.d/ 这样的一个配置文件目录,这是系统管理所有系统动态库加载相关的配置文件。
sudo touch /etc/ld.so.conf.d/temp.conf
然后填写动态库的路径,最后在使用sudo ldconfig刷新