1. 回顾C语言文件接口
1.1 写文件
#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;
}
1.2 读文件
#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)
{
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
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;
}
1.3 将信息输出到显示器
#include <stdio.h>
#include <string.h>
int main()
{
const char* msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
1.4 stdin & stdout & stderr
- C默认会打开三个输入输出流,分别是stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
1.5 总结
- 打开文件的方式见:C语言文件操作
2. 系统文件I/O
2.1 接口使用
操作文件,除了上述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;
}
读文件:
#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));//类比write
if (s > 0)
{
printf("%s", buf);
}
else {
break;
}
}
close(fd);
return 0;
}
2.2 接口介绍
2.2.1 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
-
mode_t:创建文件的默认权限。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
2.2.2 write
#include <unistd.h>
size_t write(int fildes,const void *buf,size_t nbytes);
- fildes:要写入文件的文件描述符。
- buf:要写入的内容。
- nbytes:要写入多少个字节。
- 返回值:返回实际写入多少个字节。
2.2.3 read
#include <unistd.h>
size_t read(int fildes,void *buf,size_t nbytes);
- fildes:要读取文件的文件描述符。
- buf:把读到的内容存放在buf里面。
- nbytes:一共要读多少个字节。
- 返回值:它返回实际读入的字节数,可能会小于请求的字节数。如果 read 调用返回 0,就表示没有读入任何数据,已到达了文件尾;如果返回 -1,则表示 read 调用出现了错误。
2.2.4 close
#include <unistd.h>
int close(int fildes);
- fildes:需要关闭文件的文件描述符。
- 返回值:当 close 系统调用成功时,返回 0,文件描述符被释放并能够重新使用;调用出错,则返回 -1。
2.2.4 lseek
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
- fd:文件的文件描述符。
- offset:是偏移量。
- whence:是偏移的起始位置。
- 返回值:文件指针的新位置。
2.3 open函数返回值
在认识返回值之前,先来认识一下两个概念: 系统调用和库函数
- 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数。
- open close read write lseek 都属于系统提供的接口,称之为系统调用接口
- 如下是学习操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f系列的函数,都是对系统调用的封装,方便二次开发。
2.3.1 文件描述符fd
- 通过对open函数的学习,我们知道了文件描述符是一个整数
2.3.2 返回值0 & 1 & 2
- 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));
if (s > 0)
{
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
我们现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
2.4 文件描述符的分配规则
代码演示:
#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;
}
运行结果:
关闭0或者2,如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
运行结果(关闭0):
运行结果(关闭2):
发现是结果是: fd: 0 或者 fd: 2 可见,文件描述符的分配规则是:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
3. 重定向
3.1 重定向的概念
如果关闭1呢结果会是什么?如下代码:
#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);
}
运行结果:
myfile文件内容:
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢?如下图:
3.2 dup2 系统调用
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
调用dup2后oldfd下标的指针内容将会覆盖newfd下标指针内容,即最后只剩下oldfd所指向的内容
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("./log", O_CREAT | O_RDWR);
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;
}
在My_shell中添加重定向功能:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <fcntl.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char *rdirfilename = NULL;
int rdir = NONE;
// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表
const char *getusername()
{
return getenv("USER");
}
const char *gethostname1()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd, sizeof(pwd));
}
void check_redir(char *cmd)
{
// ls -al -n
// ls -al -n >/</>> filename.txt
char *pos = cmd;
while(*pos)
{
if(*pos == '>')
{
if(*(pos+1) == '>'){
*pos++ = '\0';
*pos++ = '\0';
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=APPEND_RDIR;
break;
}
else{
*pos = '\0';
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=OUT_RDIR;
break;
}
}
else if(*pos == '<')
{
*pos = '\0'; // ls -a -l -n < filename.txt
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=IN_RDIR;
break;
}
else{
//do nothing
}
pos++;
}
}
void interact(char *cline, int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd);
char *s = fgets(cline, size, stdin);
assert(s);
(void)s;
// "abcd\n\0"
cline[strlen(cline)-1] = '\0';
//ls -a -l > myfile.txt
check_redir(cline);
}
int splitstring(char cline[], char *_argv[])
{
int i = 0;
argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
return i - 1;
}
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0){
perror("fork");
return;
}
else if(id == 0){
int fd = 0;
// 后面我们做了重定向的工作,后面我们在进行程序替换的时候,难道不影响吗???
if(rdir == IN_RDIR)
{
fd = open(rdirfilename, O_RDONLY);
dup2(fd, 0);
}
else if(rdir == OUT_RDIR)
{
fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
dup2(fd, 1);
}
else if(rdir == APPEND_RDIR)
{
fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666);
dup2(fd, 1);
}
//让子进程执行命令
//execvpe(_argv[0], _argv, environ);
execvp(_argv[0], _argv);
exit(EXIT_CODE);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
int buildCommand(char *_argv[], int _argc)
{
if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"), "%s", pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "export") == 0){
strcpy(myenv, _argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
if(strcmp(_argv[1], "$?") == 0)
{
printf("%d\n", lastcode);
lastcode=0;
}
else if(*_argv[1] == '$'){
char *val = getenv(_argv[1]+1);
if(val) printf("%s\n", val);
}
else{
printf("%s\n", _argv[1]);
}
return 1;
}
// 特殊处理一下ls
if(strcmp(_argv[0], "ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit){
// 1.
rdirfilename = NULL;
rdir = NONE;
// 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
interact(commandline, sizeof(commandline));
// commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
// 3. 子串分割的问题,解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
// 4. 指令的判断
// debug
//for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
//内键命令,本质就是一个shell内部的一个函数
int n = buildCommand(argv, argc);
// 5. 普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。追加和输入重定向和输出重定向原理一样。
3.3 如何理解一切皆文件
拿硬件来说,硬件在Linux系统下也会有自己独立的一个文件来表示该硬件,当需要使用硬件使,操作系统会根据文件数组来间接调用该文件,具体情况如下图所示:
4. FILE文件和用户级缓冲区
- 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd。
如下代码:
#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;
}
运行结果:
如果对进程实现输出重定向呢? ./test> file , 我们发现结果变成了:
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf和fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲。
综上可知: printf和fwrite库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区。
C/C++语言为什么要提供用户级缓冲区以及缓冲区何时将数据刷新出来?
那这个缓冲区谁提供呢? printf和fwrite 是库函数,write是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf和fwrite有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
FILE结构体:
typedef struct _IO_FILE FILE; //在/usr/include/stdio.h
结构体在/usr/include/libio.h当中:
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char* _IO_save_base; /* Pointer to start of non-current get area. */
char* _IO_backup_base;/* Pointer to first valid character of backup area */
char* _IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker* _markers;
struct _IO_FILE* _chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t* _lock;
#ifdef _IO_USE_OLD_IO_FILE
};
5. 文件系统
5.1 认识硬件 —— 磁盘
磁盘(disk)是指利用磁记录技术存储数据的存储器。那么磁盘的工作原理是什么:
想使用硬盘,必须要将硬盘格式化。格式化的意思是按照操作系统的理解方式将硬盘划分为几个扇区(sector),并将这些扇区记录到分区表(Master Boot Record,MBR)中,方便操作系统查找文件。硬盘的存储容量由磁道(Track)密度决定,为方便管理,又将磁道分为若干等份的扇区。
用户想要将数据保存,需要点保存按钮,之后,被操作系统接收到这个动作,并询问要将这个文件保存到什么地方,选好保存路径后,操作系统就告诉南桥芯片(上北下南,在主板CPU插槽的下面,协助CPU管理外设的部门),你把这个文件保存到硬盘的第X磁道第N扇区,如果不够,按照顺序继续写。如果磁盘空间够,就会将文件保存下来,并返回给操作系统一个完成指令。如果空间不够,会返回给操作系统一个磁盘空间不足的指令,操作系统会让用户继续选择文件的保存路径,直到剩余空间可以保存这个文件。
磁盘也可以把它想象成线性结构,磁盘分成若干个基于扇区的数组,当需要访问或者插入数据的时候就会利用每个扇区的编号来寻找相对应的分区
5.2 理解文件系统
5.2.1 文件属性
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。
每行包含7列:
- 模式
- 硬链接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
ls -l 读取存储在磁盘上的文件信息,然后显示出来
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息:
上面的执行结果有几个信息需要深入了解
5.2.2 inode
为了能更好了解inode先简单了解一下文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表:存放文件属性。如:文件大小,所有者,最近修改时间等
- 数据区:存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。
将上图简化后如下图所示:
-
新建一个文件主要有以下4个操作:
- 存储属性
内核先找到一个空闲的i节点(这里是794147)。内核把文件信息记录到其中。 - 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。 - 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。 - 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(794147,test.cpp)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
- 存储属性
-
删除一个文件(删除=允许被覆盖):
- 当文件被删除时,操作系统会将inode Bitmap和Block Bitmap索引号设置为 0,这样就把一个文件给删除了。
- i 节点中的引用数减1,若i节点号的引用数降为 0,则i节点在分配表中标记为未使用。
-
查找一个文件:
- 找到文件相对应的inode后,利用inode找到i节点表里面的数据就可以找到存放该文件的扇区
-
修改一个文件:
- 修改文件前肯定需要找到相应的文件,找到文件后直接修改即可
5.2.3 目录的理解
目录也是文件,也有自己的inode,目录也要有自己的属性,目录也有数据块,目录里面存放的是对应文件的inode的映射关系。具体形式如下图:
5.3 硬链接
我们可以知道,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。
- abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。
- 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。
硬链接原理及应用场景:
目录是不允许硬链接的。目录可以硬链接的话就会无限的循环,Linux系统里面的 . 和 . . 虽然是硬链接,但这是为了方便对目录的进出提供方便,即拥有了相对地址找文件更加方便。
5.4 软链接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法是:
软连接的原理和应用场景:
文件的三个时间:
- Access 最后访问时间
- Modify 文件内容最后修改时间
- Change 属性最后修改时间
5.5 知识点补充
-
硬件级别的磁盘和内存之间进行数据交互的基本大小是多少
-
操作系统是如何管理内存的
-
了解内核级缓冲区
6. 动态库和静态库
6.1 什么是库
库可以简单地看成一种代码仓库,他提供给使用者一些可以直接拿来使用的变量,函数和类;它也是一种特殊的程序,但是库不可以单独运行;
库可分为两种,一种是静态库,一种是动态库,区别是静态库在程序的链接阶段会被复制到程序中,而动态库顾名思义,在运行时才调入内存。
库的好处:代码保密,方面部署和分发。
什么是动态库和静态库:
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
1. 静态库的优缺点:
优点:
- 可以在编译时就地将静态库链接到目标代码中,减少了程序运行时的资源占用。
- 适用于多次编译和长期运行的系统,因为静态库不会被动态加载和卸载,因此不会带来额外的开销。
缺点:
- 静态库的代码和数据会被链接到目标代码中,因此会增加目标代码的大小和复杂度,影响程序的运行速度。
如果需要修改静态库的代码或数据,则需要重新编译整个程序,不利于版本管理和更新。
2. 动态库的优缺点:
优点:
- 可以在程序运行时动态加载和卸载,因此可以更灵活地管理库文件的版本和更新。
- 可以减少程序运行时的资源占用,因为动态库只有在需要时才会被加载。
缺点:
- 需要在编译时就将动态库链接到目标代码中,因此会增加程序编译时的复杂度和时间。
- 如果需要修改动态库的代码或数据,则需要重新编译目标程序,不利于版本管理和更新。
综上所述,静态库适用于需要长期运行和稳定的系统,动态库适用于需要更灵活和高效运行的程序。在选择使用静态库或动态库时,需要根据具体的需求和场景进行权衡和选择。
站在库的制作者角度理解库
6.2 静态库
命名规则:
Linux中:libxxx.a(lib是固定前缀,xxx是自己起的库名称,.a是固定后缀名)
Windows中:libxxx.lib
生成静态库的过程:
(1) ar 命令
通过 ar 命令来创建静态库, ar命令的选项解析如下:
选项 | 含义 |
---|---|
-c | 禁止在创建库时产生正常消息 |
-r | 如果指定的文件已经存在于库中,则替换它 |
-s | 无论 ar 命令是否修改了库文件内容,都强制重新生成库符号表 |
-v | 将建立新库的逐个文件的描述输出到标准输出(将生成新库过程中每个文件做了啥打印到终端) |
-q | 将指定文件添加到库文件的末尾 |
-t | 将库的目录写至标准输出 |
(2) 使用 ar 命令生成静态库
创建静态库时,使用上面的 -rc 三个选项即可,创建静态库的命令如下:
# 静态库必须以 lib 为前缀,文件拓展名为 .a
ar -rc libmymath.a mymath.o
6.3 制作和使用静态库
利用如下代码实现静态库的封装和使用:
#include "mymath.h"
int myerrno = 0;
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
double div(int x, int y)
{
if(y == 0)
{
myerrno = 1;
return -1;
}
return x / y;
}
封装流程如下:
- 首先将.c文件编译汇编为.o文件:
gcc -c mymath.c
- 得到如下文件:
mymath.c mymath.o mymath.h
- 输入如下命令:
ar -rc libmymath.a mymath.o
- 将得到的libmymath.a移动到lib/mymathlib目录下:
mv libmymath.a lib/mymathlib
此时,项目的静态库已经准备好了,试试来编译main.c文件
#include <stdio.h>
#include "mymath.h"
int main()
{
int a = 1;
int b = 3;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", sub(a, b));
printf("a * b = %d\n", mul(a, b));
printf("a / b = %f\n", div(a, b));
return 0;
}
- 在当前目录下输入命令:
gcc main.c -o test
报错:
因为在main.c的当前目录下,找不到include文件下的头文件
- 在当前目录下输入命令:
gcc main.c -o test -I lib/include/
报错:
在main文件中,具体的静态库的定义是在lib文件下的库文件中,所以除了加载头文件,还需要链接库文件。
- 所以在当前目录下输入命令:
gcc main.c -o test -I lib/include/ -l mymath -L lib/mymathlib
执行结果:
-l 指示静态库的名称mymath,不是库文件的名字libmymath,-L表示库的地址
运行结果:
静态库的制作和使用就算完成了。
将整个静态库制作过程编写成Makefile如下:
lib=libmymath.a
$(lib):mymath.o
ar -rc $@ $^
mymath.o:mymath.c
gcc -c $^
.PHONY:clean
clean:
rm -rf *.o *.a lib
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mymathlib
cp *.h lib/include
cp *.a lib/mymathlib
6.3 动态库
命名规则:
Linux中:libxxx.so(lib是固定前缀,xxx是自己起的库名称,.so是固定后缀名)
Windows中:libxxx.dll
生成动态库的过程:
gcc获得.o文件,将.o文件打包,-fpic得到和位置无关的代码,命令如下:
gcc -c -fpic a.c b.c
gcc获得动态库:
gcc -shared a.o b.o -o libmymath.so
6.4 制作和使用动态库
将以下代码封装成一个动态库:
#include "mymath.h"
int myerrno = 0;
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
double div(int x, int y)
{
if(y == 0)
{
myerrno = 1;
return -1;
}
return x / y;
}
封装流程如下:
- 首先将.c文件编译汇编为.o文件:
gcc -c -fpic mymath.c
- 得到如下文件:
mymath.c mymath.o mymath.h
- 输入命令:
gcc -shared mymath.o -o libmymath.so
- 将得到的libmymath.so移动到lib/mymathlib目录下:
mv libmymath.so lib/mymathlib/
- 将mymath.h移动到lib/include目录下:
mv mymath.h lib/include
此时,动态库已经准备好了,试试来编译main.c文件
#include <stdio.h>
#include "mymath.h"
int main()
{
int a = 1;
int b = 3;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", sub(a, b));
printf("a * b = %d\n", mul(a, b));
printf("a / b = %f\n", div(a, b));
return 0;
}
- 在当前目录下输入命令:
gcc main.c -o test
报错:
因为在main.c的当前目录下,找不到include文件下的头文件
- 在当前目录下输入命令:
gcc main.c -o test -I lib/include/
报错:
在main文件中,具体的动态库的定义是在lib文件下的库文件中,所以除了加载头文件,还需要链接库文件。
- 在当前目录下输入命令:
gcc main.c -o test -I lib/include/ -L lib/libmymath -l mymath
但是在运行时依旧报错:
这个错误表示动态库加载失败
原因如下:
ldd查找动态库依赖关系:
由于动态库是运行时加载入内存的,所以我们需要在运行时,让系统找到动态库的位置,报错就是系统无法找到动态库的具体位置,导致加载失败。
如何找到动态库的位置呢?
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。对于elf格式的可执行程序,是由ld-linux.so来完成的,它先后搜索elf文件的 DT_RPATH段—>环境变量LD_LIBRARY_PATH—> /etc/ld.so.cache文件列表—>/lib/,/usr/lib目录找到库文件后将其载入内存。
6.5 解决动态库加载失败
- DT_PATH段我们是无法修改的,所以第一种方法是在环境变量LD_LIBRARY_PATH中加入所需要的动态库绝对路径。
(1)在当前目录下输入命令:
[xiaomaker@VM-28-13-centos mymathlib]$ pwd
/home/xiaomaker/code_cpp/test_Linux/test_11_12/lib/mymathlib
(2)拿到了动态库绝对路径后保存到环境变量中:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xiaomaker/code_cpp/test_Linux/test_11_12/lib/mymathlib
(3)确认路径保存成功:
echo $LD_LIBRARY_PATH
:/home/xiaomaker/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/xiaomaker/code_cpp/test_Linux/test_11_12/lib/mymathlib
(4)现在再查看动态库依赖度关系
(5)在当前目录下执行test文件:
注意:
-
这种配置只在当前终端中有用,该终端页面关闭后环境变量便消失了
-
要永久配置该环境变量,有两种方式,一种是用户级别,一种是系统级别
- 第二中方法是在系统默认路径下/lib64或者usr/lib64/下建立软链接
(1)建立软连接命令如下:
sudo ln -s /home/xiaomaker/code_cpp/test_Linux/test_11_12/lib/mymathlib/ libmymath.so /lib64/libmymath.so
建立该软链接需要root身份来进行,所以前面需要加sudo。
最开始的路径是动态库的绝对路径,后面的是系统文件的路径。
(2)建立软链接的结果:
- 第三中方法是拷贝到系统默认的库路径/lib64或者usr/lib64/下,这中方法是最常用的,但是一般都是拷贝第三方库。
- 第四种方法是配置/etc/ld.so.conf.d/,ldconfig更新
(1)打开/etc/ld.so.conf.d/就是如下文件
每个文件里面都是存放了动态库的绝对路径,所以我们可以创建一个xxx.conf文件,将自己的动态库的路径存放在该文件下即可。
将整个动态库制作过程编写成Makefile如下:
dy-lib=libmymethod.so
.PHONY:all
all: $(dy-lib)
$(dy-lib):mylog.o myprint.o
gcc -shared -o $@ $^
mylog.o:mylog.c
gcc -fPIC -c $^
myprint.o:myprint.c
gcc -fPIC -c $^
.PHONY:clean
clean:
rm -rf *.o *.so mylib
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp *.h mylib/include
cp *.so mylib/lib