目录
前言
一、C语言中文件IO操作
1.文件的打开方式
2.fopen:打开文件
3.fread:读文件
4.fwrite:写文件
二、系统文件I/O
1.系统调用open、read、write
2.文件描述符fd
3.文件描述符的分配规则
4.重定向
5.缓冲区
6.理解文件系统
磁盘
磁盘的逻辑抽象
磁盘分区
磁盘格式化
EXT2文件系统的存储方案
软硬链接
软链接
硬链接
软硬链接的区别
文件的三个时间(ACM时间)
三、动态库和静态库
1.动态库和静态库的概念
2.如何生成静态库
3.如何去使用一个静态库
4.如何生成一个动态库
5.如何去使用一个动态库
前言
一、C语言中文件IO操作
1.文件的打开方式
- r 只读方式打开,文件指针指向文件的首位置
- r+ 读写方式打开,文件指针指向文件的首位置
- w 只写方式打开,清空文件内容,如果文件不存在则创建文件,文件指针指向文件的首位置
- w+ 读写方式打开,文件不存在则创建文件,如果文件存在则清空原来内容,文件指针指向文章的首位置
- a 以追加写的方式打开,写到文件的结尾处,文件不存在则创建文件,文件指针指向文件的末尾位置
- a+ 以读和追加写的方式打开,文件不存在则创建文件,如果文件存在则从文件的末尾位置开始追加
2.fopen:打开文件
3.fread:读文件
4.fwrite:写文件
二、系统文件I/O
1.系统调用open、read、write
int open(const char *pathname, int flags, mode_t mode);
参数 和 返回值
- 成功:返回一个文件描述符
- 失败:返回-1
ssize_t read(int fd, void *buf, size_t count);
参数和返回值
- 文件描述符(fd):表示要读取数据的文件。
- 缓冲区(buf):用于存储读取数据的缓冲区。
- 要读取的字节数(count):表示要读取的字节数。
需要注意的是,在实际编程中,通常会使用循环来读取大文件中的数据,每次读取的字节数为缓冲区的大小。
- 成功:读取到的字节数:表示成功读取到的字节数。
失败:-1
:表示发生错误,错误原因可以通过errno
查看。
ssize_t write(int fildes, const void *buf, size_t nbyte);
参数 和 返回值
fildes
:文件描述符,标识要写入的文件或设备。buf
:指向要写入数据的缓冲区。nbytes
:要写入的字节数。
- 成功:写入的字节数:表示成功写入的字节数。
- 失败:-1:表示出现错误,具体的错误信息可以通过
errno
来获取。
2.文件描述符fd
- 文件是由进程运行时打开的,一个进程可以打开多个文件,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。
- 因此,操作系统务必要对这些已经打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
- 而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。
进程和文件之间的对应关系是如何建立的?
进程 task_struct 当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当进程打开 log.txt 文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
文件描述符 :fd 本质就是数组的下标
因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。
为啥我们打开文件,文件描述符是从 3 开始的,0,1,2 哪去了?
0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器。
而键盘和显示器都属于硬件,属于硬件就意味着操作系统能够识别到,当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流。
操作系统为啥要默认打开0(stdin),1(stdout),2(stderr) ?
就是为了让程序员进行默认输入输出!!!
如何理解一切皆文件?
3.文件描述符的分配规则
我们先做几个小实验
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
char buf[1024];
ssize_t s = read(0, buf, 1024);
if (s > 0)
{
buf[s] = 0;
cout << "echo# " << buf << endl;
}
return 0;
}
我们发现我们输入什么就会给我们打印什么!!! 我们并没有打开0、1号文件,说明0、1号文件是默认打开的。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
可以看到这五个文件获取到的文件描述符都是从3开始连续递增的,这很好理解,因为文件描述符本质就是数组的下标,而当进程创建时就默认打开了标准输入流、标准输出流和标准错误流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配。
若我们在打开这五个文件前,先关闭文件描述符为0的文件,此后文件描述符的分配又会是怎样的呢?
可以看到,第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符还是从3开始依次递增的。
我们可以得出下面两个结论
- 进程默认已经打开了0,1,2
- 文件描述符的分配规则,找到当前没有被使用的最小下标,作为新文件的文件描述符
4.重定向
我们先来做一个小实验
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
// 输出重定向
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if (fd < 0)
{
cerr << "open";
return 1;
}
cout << "fd: " << fd << endl;
// cout << "cout-> fd: " << cout->_fileno << endl;
flush(cout);
return 0;
}
此时我们发现,本来应该输出在显示器上的的内容,输出到了文件myfile中,我们把这种现象叫做输出重定向!!!
那么为啥本来应该输出在显示器上的的内容,输出到了文件myfile中???
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
// 输入重定向
close(0);
int fd = open("myfile", O_RDONLY);
if (fd < 0)
{
cerr << "open" << endl;
return 1;
}
char buf[1024];
fread(buf, 1, sizeof(buf), stdin);
printf("%s\n",buf);
return 0;
}
此时我们发现,本来应该等待我们在键盘输入,结果直接把文件myfile中的内容打印出来了,我们把这种现象叫做输入重定向!!!
原因和上面类似!!!本质就是上层fd不变,底层fd指向的内容发生变化!!!
但是我们这样实现重定向太麻烦了,重定向本质就是上层fd不变,底层fd指向的内容发生变化,那么我们就可以直接将3的内容直接覆盖到1中去,那么有没有文件描述符级别的数组内容的拷贝???有函数dup2!!!
int dup2(int oldfd, int newfd);
参数 和 返回值
- oldfd:要复制的文件描述符。
- newfd:指定新的文件描述符。
- 成功时:返回新的文件描述符。
- 失败时:返回
-1
,并设置errno
来表示错误原因。
我们使用dup2来实现一个输入重定向!!!
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main()
{
// 输入重定向
int fd = open("myfile", O_RDONLY);
dup2(fd, 0);
if (fd < 0)
{
cerr << "open" << endl;
return 1;
}
char buf[1024];
fread(buf, 1, sizeof(buf), stdin);
printf("%s\n",buf);
return 0;
}
我们发现确实可以实现输入重定向!!!
5.缓冲区
缓冲区是什么???
缓冲区是一部分内存!!!
为什么要有缓冲区!!!
我来给大家讲解一个小故事,在广东读大学的小明有一个在北京读大学的好朋友小光,最近小光快要生日了,小明打算送一个生日礼物给小光,现在小明有两个选择一是自己骑一个车送给小光,二是直接下楼送到老鸟驿站,再由老鸟驿站送过去。如果你是小明你会如何选择呢??
我肯定选择直接下楼送到老鸟驿站。因为把礼物送到老鸟驿站后我的任务就完成了。虽然老鸟驿站还是要把礼物送过去。时间没变,但是我不用去送了!!!好处一:提高了我的效率!!!
礼物到了老鸟驿站后,驿站会直接将礼物直接送去北京吗?显然不会,驿站会累计一部分都是北京的包裹在一起送过去。好处二:提高了发送的效率!!!
驿站就相当于缓冲区!!!
- 缓冲区提高了使用者的效率!!!
- 因为有了缓冲区的存在,我们可以积累一部分统一发送,提高了发送的效率!!!
- 总的来说,缓冲区就是提高了效率!!!
因为缓冲区可以暂存数据,所以必定有一定的刷新方式!!!!(就像驿站存储到一定的数量再发货)
- 无缓冲(立即刷新)
- 行缓冲(一行满了就刷新)
- 全缓冲(缓冲区满了,再刷新)
- 显示器:行缓冲
- 磁盘文件:全缓冲
特殊情况
- 强制刷新!!!
- 进程退出的时候,一般要刷新!!!
我们来做一个实验
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
fprintf(stdout, "C: hello fprintf\n");
printf("C: hello printf\n");
fputs("C: hello fputs\n", stdout);
const char *str = "system call : hello write\n";
write(1, str, strlen(str));
fork();
return 0;
}
运行结果
如果我们对进程进行输入重定向,发现除了write外,所有的都打印了两次!!!
这是因为什么呢???
- 我们直接在显示器上打印的时候,行刷新,输出字符串时都有 \n,fork之前的数据都已经刷新了!!!
- 我们重定向到myfile,本质是向磁盘文件写入。全缓冲!!!
- 此时我们使用printf和fputs函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时。
- 由于进程间具有独立性,当进程退出的时候,父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,
- 至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到myfile文件当中printf和puts函数打印的数据就有两份。但由于write函数是系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。
这个缓冲区是谁提供的(用户缓冲区)???
实际上这个缓冲区是C语言自带的,如果说这个缓冲区是操作系统提供的,那么printf、fputs和write函数打印的数据重定向到文件后都应该打印两次。
这个缓冲区在哪里???
我们常说printf是将数据打印到stdout里面,而stdout就是一个FILE*
的指针,在FILE结构体当中还有一大部分成员是用于记录缓冲区相关的信息的。
//缓冲区相关
/* 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. */
也就是说,这里的缓冲区是由C语言提供,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符fd还保存了用户缓冲区的相关信息。
FILE是C语言封装的一个结构体!!!
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。
typedef struct _IO_FILE FILE;
而我们在/usr/include/libio.h
头文件中可以找到struct _IO_FILE
结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno
的成员,这个成员实际上就是封装的文件描述符。
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
};
C语言当中的函数究竟在做什么???
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。
6.理解文件系统
磁盘
磁盘的基本特征
磁盘是一种存储设备,主要用于计算机系统中数据的存储。
它通常由磁性材料制成的盘片组成,通过磁头在盘片上读写数据。磁盘可以分为硬盘和软盘等类型,具有较大的存储容量,可以长期保存数据。
- 一个磁盘有很多盘片!!
- 一个盘片有很多的同心磁道!!
- 一圈磁道有很多的扇形的扇区 !!
- 扇区是磁盘的最小的存储单元 ------- 512字节(0.5kb)
如果我想向一个扇区进行写入,我应该如何去寻址!!!
- 柱面号:确定扇区所在的柱面位置。
- 磁头号(哪一盘片):确定读写头所在的磁头位置。
- 扇区号:确定具体要写入的扇区编号。
通过以上三个步骤,最终确定信息在磁盘的读写位置。柱面号(Cylinder)、磁头号(Head)和扇区号(Sector),简称为CHS定位法。
磁盘的逻辑抽象
首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的。
这样对磁盘的管理就变成了对数组的管理!!!
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可被分为十亿多个扇区。
操作系统可以按照扇区为单位进行存取,也可以基于文件系统,按照文件块为单位进行存取!!!
八个扇区为一个文件块(4kb)!!!我们会以一个文件块来存储文件!!!
所以只要知道文件的起始地址就可以知道整个文件!!!我们把这种寻址的方式方法叫做LBA寻址法!!!
LBA 地址的工作原理如下:
- 连续编号:LBA 对磁盘上的每个扇区进行连续编号,从 0 开始递增。
- 转换:当需要访问某个扇区时,系统会将 LBA 地址转换为对应的CHS地址物理磁盘结构(柱面、磁头、扇区)信息。
- 寻址:通过这种转换,磁盘控制器可以准确地找到要访问的扇区,并进行数据读写操作。
这样的方式使得磁盘访问更加灵活和高效,不受磁盘物理结构的限制。
这样对存储设备的管理,在操作系统层面就转换成为了对数组的增删查改!!!
磁盘分区
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为500G的磁盘为例,该磁盘就可被分为十亿多个扇区。
计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘、E盘、F盘等等。
磁盘格式化
当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化是指对磁盘或磁盘中的分区进行初始化的一种操作。简单来说,磁盘格式化就是对分区后的各个区域写入对应的管理信息。
在格式化过程中,磁盘上原有的数据会被清除,同时会建立文件系统,为存储数据做好准备。不同的文件系统有不同的特点和适用场景。
常见的文件系统
- EXT2/EXT3/EXT4:Linux 系统中常用的文件系统。
- APFS:苹果的新一代文件系统。
- ReFS:微软推出的新一代文件系统。
EXT2文件系统的存储方案
计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
注意: 启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
其次,每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。但是其中超级块(Super Block)并不是每一块组都有。通常,超级块会在多个块组中进行备份,以提高文件系统的可靠性。这样可以在出现问题时,从备份中恢复超级块的信息。
inode表(inode Table)
inode表记录了文件和目录的相关信息,如文件的属性(如文件类型、权限、所有者等)、文件数据所在的数据块位置等。每个文件或目录都对应一个 inode,通过 inode 可以快速定位和管理文件。
- 一般情况下,一个文件一个inode,基本上每一个文件都有!!!
- 在整一个分区中具有唯一性,在Linux内核中,识别文件和文件名无关只和inode有关!!!
数据表(Data Block)
存放文件内容
比如文件内容占用了3,5,7那么,就把3,5,7存入int blocks[ 15 ],这个时候有人就会问了,如果文件太大怎么办,这里就 4*15 kb完全不够用啊!!!
- 下标0 - 12是直接映射!!!
- 下标13是间接映射!!!
- 下标14是三级映射!!!
inode位图(inode Bitmap)
inode位图当中记录着每个inode是否空闲可用。
块位图(Block Bitmap)
块位图当中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
块组描述符表(Group Descriptor Table)
块组描述符表,描述该分区当中块组的属性信息
超级块(Super Block)
注意:
- 其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
- 磁盘分区并格式化后,每个分区的inode个数就确定了。
如何理解创建一个空文件?
- 分配 inode:为新文件分配一个唯一的 inode,用于记录文件的相关属性。
- 初始化 inode 信息:在 inode 中设置一些基本信息,如文件类型(通常是普通文件)、权限等。
- 关联数据块:可能会为文件分配一些初始的数据块,但这些数据块目前是空的。
- 在目录中建立链接:将文件的名称与对应的 inode 关联起来,使其能在文件系统中被找到。
如何理解对文件写入信息?
- 定位:通过文件的 inode 找到相应的数据块位置。
- 写入数据:将新的信息写入到指定的数据块中。
- 更新 inode:修改 inode 中的相关信息,如文件大小、修改时间等。
- 可能的磁盘操作:实际的写入可能涉及磁盘的读写操作,以确保数据的持久保存。
如何理解删除一个文件?
- 将该文件对应的inode在inode位图当中置为无效。
- 将该文件申请过的数据块在块位图当中置为无效。
因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。
- 立即停止使用存储设备:避免新的数据覆盖可能被恢复的文件区域。
- 使用数据恢复软件:有一些专门的数据恢复软件可以扫描存储设备,尝试找回被删除的文件。
为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。
为什么拷贝文件的时候很慢,而删除文件的时候很快?
- 拷贝过程:拷贝文件需要将数据从源位置读取出来,然后写入到目标位置,涉及大量的数据传输和处理,时间相对较长。
- 删除操作:删除文件主要是进行一些标记和资源释放的操作,相对较为简单快捷。
- 文件系统结构:文件系统在删除文件时可以较快地完成一些内部的调整和清理,而拷贝文件需要实际的数据移动和复制。
因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。
这就像建楼一样,我们需要很长时间才能建好一栋楼,而我们若是想拆除一栋楼,只需在这栋楼上写上一个“拆”字即可。
如何理解目录??
- 文件组织方式:目录是一种用于组织文件的结构,它就像文件的“收纳盒”,将相关的文件集合在一起。
- 链接和索引:目录中包含了文件的名称以及与这些文件对应的 inode 的链接或索引,通过这些信息可以找到文件。
- 层次结构:目录可以形成层次结构,构成文件系统的整体架构,方便对文件进行分类和管理。
- 访问入口:用户可以通过目录来浏览和操作文件,它是文件系统中重要的导航和操作界面。
目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。
注意: 每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
软硬链接
软链接
ln -s 源文件 目标链接
其中,-s
选项表示创建软链接,源文件
是要链接的文件,目标链接
是创建的软链接的名称和位置。
通过ls -i -l
命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。
但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。
硬链接
ln 源文件 目标文件
其中,源文件
是已有文件,目标文件
是要创建的硬链接的名称。
通过ls -i -l
命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为924344的文件有myproc和myproc-h两个文件名,因此该文件的硬链接数为2。
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。
总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。就相当于别名!!!
为什么刚刚创建的目录的硬链接数是2???
我们创建一个普通文件,该普通文件的硬链接数是1,因为此时该文件只有一个文件名。那为什么我们创建一个目录后,该目录的硬链接数是2?
因为每个目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2。通过命令我们也可以看到dir和该目录下的.的inode号是一样的,也就可以说明它们代表的实际上是同一个文件。
注意: 一个目录下相邻的子目录数等于该目录的硬链接数减2。
软硬链接的区别
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
文件的三个时间(ACM时间)
在Linux当中,我们可以使用命令stat 文件名
来查看对应文件的信息。
这其中包含了文件的三个时间信息:
- Access: 文件最后被访问的时间。
- Modify: 文件内容最后的修改时间。
- Change: 文件属性最后的修改时间。
当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
我们若是想将文件的这三个时间都更新到最新状态,可以使用命令touch 文件名来进行时间更新。
注意: 当某一文件存在时使用touch命令,此时touch命令的作用变为更新文件信息。
三、动态库和静态库
1.动态库和静态库的概念
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。 一般默认生成的可执行程序都是动态的,动态库体积小,运行时加载,只有一份。
2.如何生成静态库
我们生成库,其实并不要提供源文件,我们其实只需要提供( .o 和 .h)文件,用户要使用库的时候,直接和用户的文件一链接即可。
你有什么证据呢?凭什么说我们其实只需要提供( .o 和 .h)文件呢?
下面我们来写一个小代码
// Add.h
#include <stdio.h>
extern add(int x, int y);
// Sub.h
#include <stdio.h>
extern sub(int x, int y);
// Mul.h
#include <stdio.h>
extern mul(int x, int y);
// Add.c
#include "Add.h"
int add(int x, int y)
{
return x+y;
}
// Sub.c
#include "Sub.h"
int sub(int x, int y)
{
return x-y;
}
// Mul.c
#include "MUl.h"
int mul(int x, int y)
{
return x*y;
}
我们使用 gcc -c ,生成 .o 文件,然后将 .c 文件删除
1、我们先将目标文件 TestMain.c 编译成 TestMain.o。
2、再将 TestMain.o 与其他 .o 文件链接起来。
3、我们发现确实生成了可执行程序 a.out。
4、说明我们其实只需要提供( .o 和 .h)文件没有问题。
这个大概就是库的大概意思,我们还需要打包。
我们创建一个makefile
make一下,生成了一个静态库。
但是这个样子就行了嘛?不行。
我们还有一大堆的 .h 文件,这样无法很好的给用户使用,我们还要把 .h 文件打包起来。
我们再makefile 文件里面加入一个output,创建一个mymath_lib的文件夹
这样生成了一个文件夹(mymath_lib),我们把文件夹压缩一下,放在网上,就可以给其他用户使用了。
假如我们咋网上下载了一个这样的库,我们如何去使用呢 ?
我们需要安装开发环境(其实就是将头文件 和 .a 文件拷贝到操作系统的指定目录下面)
操作系统的指定的头文件一般在 /usr/include 下面
操作系统的指定的 .a 文件一般在 /usr/lib64 下面
拷贝完成后我们就可以使用了。
我们也可以不安装开发环境,直接使用,看下面。
3.如何去使用一个静态库
我们将我们生成的静态库放入用户的目标文件夹,然后编译,发现链接错误
这个时候我们可以知道,我们自己写的静态库(libmymath.a),属于第三方库,编译器(gcc)不认识。
这个时候我们需要去链接。我们需要使用 gcc -lxxx (xxx代指静态库的库名) -L.
我们发现报错,因为 libmymath.a 并不是我们的库名,真正的库名是去掉前面的 lib 以及后面的 .a ,mymath 才是我们的库名。
我们使用正确的库名,再次编译。成功生成可执行程序 a.out 。
我们查询 a.out 所依赖链接的库。发现并没有我们自己写的库(libmymath.a)啊?
1、ldd 查询的是动态库的
2、我们自己写的是静态库直接链接到可执行程序里面去了。
3、编译器(gcc)默认是链接动态库的(动态链接),但是如果你只提供了静态库(.a),那么编译器(gcc)也没有办法,只有单独把用户所指定的静态库(.a)进行静态链接,其他正常动态链接。
4、如果加上 -static ,那么全部就必须是静态链接。
4.如何生成一个动态库
我们创建以个makefile文件,生成动态库主要分为两步
- 先生成目标文件
- 用目标文件生成动态库
我们make验证一下,发现确实没有问题,生成了动态库(libmymath.so)。
但是这个样子就行了嘛?不行。
我们还有一大堆的 .h 文件,这样无法很好的给用户使用,我们还要把 .h 文件打包起来。
我们再makefile 文件里面加入一个output,创建一个mymath_lib的文件夹
这样就生成了一个动态库!!!
5.如何去使用一个动态库
我们还是用刚才使用过的TestMain.c
来演示动态库的使用。
#include "Add.h"
#include "Sub.h"
#include "Mul.h"
int main()
{
int a = 10;
int b = 20;
printf("a + b = %d\n", add(a,b));
printf("a - b = %d\n", sub(a,b));
printf("a * b = %d\n", mul(a,b));
return 0;
}
说明一下,使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I,-L,-l 这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用 -l 选项指明需要链接的库名字来生成可执行程序,下面我们仅以第一种方法为例进行演示。
此时使用gcc编译TestMain.c生成可执行程序时,需要用 -I 选项指定头文件搜索路径,用 -L 选项指定库文件搜索路径,最后用 -l 选项指明需要链接库文件路径下的哪一个库。
gcc TestMain.c -I./mymath_lib/include -L./mymath_lib/lib -lmymath
与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。
需要注意的是,我们使用-I
,-L
,-l
这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库,我们可以使用ldd
命令进行查看。
可以看到,此时可执行程序所依赖的动态库是没有被找到的。
那我们如何区做呢???
方法一:拷贝.so文件到系统共享库路径下
既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。
sudo cp mymath_lib/lib/libmymath.so /lib64
可执行程序也就能够顺利运行了。
方法二:更改
LD_LIBRARY_PATH
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中即可。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/wh/linux_learn/test_24_2_29/work/mymath_lib/lib
可执行程序也就能够顺利运行了。