【Linux】基础:基础IO
摘要:本文基础IO的内容将从过往熟悉的C语言文件操作出发,引申指系统调用的文件操作,再进一步深化为对于进程管理的文件进行介绍,从而了解文件描述符的概念和管理方式,其中还会介绍其运用下的重定向和缓冲区的概念与基本原理。再继续深入探讨,了解文件系统的相关内容掌握inode这一核心概念,并由此拓展出软硬连接内容。最后还会对于动静态库进行介绍。
文章目录
- 【Linux】基础:基础IO
- 一、文件相关的系统调用
- 1.1 open
- 1.2 close
- 1.3 write
- 1.4 read
- 二、文件描述符
- 2.1 文件描述符的管理方式
- 2.2 理解一切皆文件
- 2.3 文件描述符的分配规则
- 三、重定向
- 3.1 重定向的本质
- 3.2 重定向示例
- 3.3 重定向的系统调用
- 四、缓冲区
- 4.1 概述
- 4.2 缓冲区刷新策略
- 五、文件系统
- 5.1 概述
- 5.2 inode
- 六、软硬连接
- 6.1 软连接
- 6.2 硬链接
- 七、动静态库
- 7.1 概述
- 7.2 头文件源文件汇编
- 7.3 制作静态库
- 7.4 制作动态库
- 7.5 小节
一、文件相关的系统调用
在以往学习C语言或其他语言的过程中,学习了在语言层面对于文件的各种调用接口,其中包括了在对于文件的打开关闭、读写等相关接口,可能还涉略了对于相关语言层面缓冲区的概念。在此不过赘述,如需复习,可以参考博客http://t.csdn.cn/2JF2O。需要提醒的是,后续将会提到默认打开的三个标准输入输出分别为stdin
、stdout
、stderror
,其中对应的硬件分别为键盘、显示器、显示器,三个流的类型都是FILE, fopen返回值类型,文件指针*。
在本文中,首先将会介绍在系统层面上的文件操作的系统调用,这些系统调用与以往库函数的关系在于,库函数是对于这些系统调用的封装,是用户层部分的内容,而系统调用是在操作系统的用户层与内核直接,提供系统调用层,毕竟操作系统不允许用户直接访问或操作操作系统。更具体而言,对于文件的访问,最终都是需要访问硬件的,而硬件的管理者是操作系统。因此在语言层面上对文件的操作必须要贯穿操作系统,操作系统是不相信任何角色的,因此访问操作系统,需要通过系统调用接口。如下图所示:
关于文件相关的系统调用接口,本文将会介绍4个内容,包括open
、read
、write
、read
。对于该四个系统调用函数,需要掌握其作用,参数以及返回值的内容,具体内容如下:
1.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:打开方式,传递一个int参数,每个int类型正数有32个bit,一个bit代表了一个标志位,可以通过位运算来传递宏定义后的打开方式
- mode:文件权限信息,可以通过一个八进制数来设立对应的文件权利信息
flags:常用传递标志位包括:
O_WRONLY
、O_RDONLY
、O_CREAT
,这些表示的都是一个比特位的数据,而且不重复,可以通过位运算来传递标志位,设定打开方式,具体定义如下(/usr/include/bits/fcntl-linux.h):#define O_RDONLY 00 #define O_WRONLY 01 #define O_RDWR 02 #ifndef O_CREAT # define O_CREAT 0100 /* Not fcntl. */ #endif
可以简单理解为:
#define O_WRONLY 0x1 ------> 0000 0001 #define O_RDONLY 0x2 ------> 0000 0010 #define O_CREAT 0x4 ------> 0000 0100
返回值:返回所打开文件的文件描述符,
实例:
该实例展现了对于open
系统调用的使用,具体需要掌握打开方式的书写与文件权限信息的设置以及文件描述符返回值打印。
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main(){
int fd = open("./test.txt",O_RDONLY | O_CREAT,0644);
int fd2 = open("./test2.txt",O_RDONLY | O_CREAT,0604);
int fd3 = open("./test3.txt",O_RDONLY | O_CREAT,0643);
if(fd < 0){
printf("open error\n");
}
// ......
printf("fd: %d\n",fd);
printf("fd2: %d\n",fd2);
printf("fd3: %d\n",fd3);
close(fd);
return 0;
}
[root@VM-12-7-centos Blog_File]# ./Blog_file
fd: 3
fd2: 4
fd3: 5
[root@VM-12-7-centos Blog_File]# ll
total 28
-rwxr-xr-x 1 root root 17640 Jan 4 17:15 Blog_file
-rw-r--r-- 1 root root 459 Jan 4 17:15 Blog_file.c
-rw-r--r-- 1 root root 73 Jan 4 17:12 makefile
-rw----r-- 1 root root 0 Jan 4 17:15 test2.txt
-rw-r----x 1 root root 0 Jan 4 17:15 test3.txt
--wsr-s--T 1 root root 0 Jan 4 17:12 test.txt
1.2 close
头文件:
#include <unistd.h>
定义:
int close(int fd);
作用:通过文件描述符关闭文件
参数:文件描述符
1.3 write
头文件:
#include <unistd.h>
定义:
ssize_t write(int fd, const void *buf, size_t count);
作用:通过文件描述符写文件
参数:
- fd:文件描述符
- buf:缓冲区,写入文件的内容
- count:写入文件的大小,单位为字节
返回值:实际写出的字节数,如果返回值是0,就表示没有写入任何数据;如果返回是-1,就表示在write调用中出现了错误,对应的错误代码保存在全局变量error里面。
实例:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(){
int fd = open("./test.txt",O_WRONLY | O_CREAT);
if(fd < 0){
printf("open error\n");
}
const char *msg = "hello world\n";
int cnt = 5;
while(cnt){
write(fd, msg, strlen(msg));
cnt--;
}
close(fd);
return 0;
}
[root@VM-12-7-centos Blog_File]# cat test.txt
hello world
hello world
hello world
hello world
hello world
补充:在写入文件过程中,是不需要写入\0的,因为\0为结尾是C语言的规定
1.4 read
头文件:
#include <unistd.h>
定义:
ssize_t read(int fd, void *buf, size_t count);
作用:通过文件标识符来读取文件
参数:
- fd:文件描述符
- buf:缓冲区,读取文件的内容
- count:读取文件的大小,单位为字节
返回值:实际读取的字节数,如果返回值是0,就表示没有读取任何数据;如果返回是-1,就表示在read调用中出现了错误,对应的错误代码保存在全局变量error里面。
示例:
int main(){
int fd = open("./test.txt",O_RDONLY);
if(fd < 0){
printf("open error\n");
}
char buffer[1024];
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if(s > 0){
buffer[s] = 0;
printf("%s\n",buffer);
}
close(fd);
return 0;
}
[root@VM-12-7-centos Blog_File]# ./Blog_file
hello world
hello world
hello world
hello world
hello world
二、文件描述符
2.1 文件描述符的管理方式
实际上,对于上述内容介绍的相关文件操作来说,从表现上都是进程来执行对应的函数,可是进程需要操作这些文件,便需要先打开这些文件,将这些文件的相关属性信息,加载到对应的内存中。为此操作系统将会存在大量的文件。而操作系统就需要将这些打开的文件完成管理操作,而对这些文件的管理方式就是先对文件的属性内容进行描述,再通过相应的数据结构完成组织。在此引入了文件描述符的概念。
对于打开的文件来说,其中需要管理的数据则是包括两部分,分别为对于文件内容的数据以及对于文件属性的数据,在Linux中,采用了结构体file
进行描述,其中包括的信息为文件内容信息,以及文件属性信息。而对于这些文件的组织形式,则是通过不同的数据结构将其组织起来,文章对此不进行介绍,
而对于每个进程而言,则是通过相应的文件描述符来管理组织相应的文件的。当进程创建时,会同时创建对应的进程PCB
,在Linux系统中,通过结构体task_struct
来表示,其中存在结构体指针struct files_struct *fs
,指向进程的结构体struct files_struct
,在该结构体中存在结构体指针数组struct file* fd_array[]
,在该数组中指向该进程打开文件的结构体file
内容,其中就包括了文件的属性与文件的内容。而文件标识符fd
就是在内核中进程和关联文件的数组的下标。示意图如下:
2.2 理解一切皆文件
对于操作系统而言,最后需要交互的底层一定是硬件外设,完成基础IO的工作,对于每个外设而言,需要进行不同的读写工作,因为不同的硬件设备,为此读写过程也是不同的,为此增加了一个驱动层,给各自的外设设置驱动程序,达成读写的目的。而针对于一切皆文件的说法,则是操作系统通过设置一个软件虚拟层(vfs:virtual file system),其中通过数据结构的方式组织起各个对应文件结构体,在结构体中存在函数指针数组,对应着驱动程序中的读写函数。从上层来看,struct file
中的读写函数指针,指向各个文件,根本不关心到底是什么文件,直接对应相应的驱动程序,从而达成一切皆文件的目的。示意图如下:
2.3 文件描述符的分配规则
在学习C语言的过程中,会提及默认打开的三个标准输入输出流,而对应的就是用文件描述符表示的对应0:stdin
、1:stdout
、2:stderr
,因此在进程创建后,这个三个是自动分配文件描述符的,当然在后续的操作过程中,仍然是可以对其进行关闭。
而对于其他文件描述符的分配规则,对于新文件分配的fd
,是从fd_array
中找到一个最小的,未被使用的作为新的fd
。
示例如下:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(){
printf("三个标准输出输出的文件标识符:\n");
printf("stdin -> %d\n",stdin->_fileno);
printf("stdout -> %d\n",stdout->_fileno);
printf("stderr -> %d\n",stderr->_fileno);
printf("打开性文件的文件标识符:\n");
int fd = open("./text.txt",O_RDONLY | O_CREAT, 0644);
printf("fd -> %d\n",fd);
printf("关闭stdin后,创建新文件的文件标识符\n");
close(0);
int fd2 = open("./text2.txt",O_RDONLY | O_CREAT, 0644);
printf("fd2-> %d\n",fd2);
return 0;
}
[root@VM-12-7-centos Blog_File]# ./Blog_file
三个标准输出输出的文件标识符:
stdin -> 0
stdout -> 1
stderr -> 2
打开性文件的文件标识符:
fd -> 3
关闭stdin后,创建新文件的文件标识符
fd2-> 0
三、重定向
3.1 重定向的本质
了解文件描述符的管理方式后,便可以对重定向进行探讨,重定向可以简单分为三类,分别为输出重定向、追加重定向、输入重定向。其本质就是对于文件描述符的运用,将原本输入或输出到某个文件描述符内容的重新设定,输入或输出到另外一个文件描述符描述的文件中。以输出重定向为例:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(){
close(1);
int fd = open("./test.txt",O_CREAT | O_WRONLY , 0644);
printf("fd = %d\n",fd);
return 0;
}
对于上述代码,关闭了标准输出的输出重定向,对于新的打开文件的分配的文件描述符为,最小的一个未被使用的值,因此fd = 1
。当进行库函数调用,对标准输出中打印fd
的值,此时对应的文件描述符因为fd = 1
,对应打开的文件为test.txt
,因此在屏幕中将不会出现打印信息,而对于文件中,则会存在打印信息,结果如下:
[root@VM-12-7-centos Blog_File]# ./Blog_file
[root@VM-12-7-centos Blog_File]# cat test.txt
fd = 1
图示如下:
3.2 重定向示例
输出重定向:将本应显示到显示器中的内容,输出重定向到文件内部中
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(){
close(1);
int fd = open("./test.txt",O_CREAT | O_WRONLY , 0644);
printf("fd = %d\n",fd);
return 0;
}
[root@VM-12-7-centos Blog_File]# ./Blog_file
[root@VM-12-7-centos Blog_File]# cat test.txt
fd = 1
追加重定向:修改打开方式,将追加到显示器的内容,重定向到文件中
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(){
close(1);
int fd = open("./test.txt",O_CREAT | O_WRONLY | O_APPEND, 0644);
printf("append fd :%d\n",fd);
fprintf(stdout,"append hello world\n");
return 0;
}
[root@VM-12-7-centos Blog_File]# cat test.txt
fd = 1
append fd :1
append hello world
输入重定向:将原本从键盘中读取内容在显示器打印,重定向为在文件中获取内容
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main(){
close(0);
int fd = open("./test.txt",O_RDONLY);
// fd == 0
char line[128];
while(fgets(line,sizeof(line) - 1, stdin)){
printf("%s",line);
}
}
[root@VM-12-7-centos Blog_File]# cat test.txt
fd = 1
append fd :1
append hello world
[root@VM-12-7-centos Blog_File]# ./Blog_file
fd = 1
append fd :1
append hello world
3.3 重定向的系统调用
在Linux中,可以使用的重定向的系统调用有dup
、dup2
、dup3
,在本文中主要介绍dup2
的内容,具体内容如下:
dup2
头文件:
#include <unistd.h>
定义:
int dup2(int oldfd, int newfd);
作用:通过新旧文件描述符来完成重定向工作
参数:
- oldfd:原来的文件描述符
- newfd:重定向后的新的文件描述符
返回值:
- 成功:将oldfd复制给newfd, 两个文件描述符指向同一个文件
- 失败:返回-1, 设置errno值
实例:
输出重定向
#include<stdio.h> #include<unistd.h> #include<sys/stat.h> #include<sys/types.h> #include<fcntl.h> int main(){ int fd = open("./test.txt",O_WRONLY | O_TRUNC); dup2(fd,1); // stdout -> 1 -> struct file* -> test.txt printf("hello print\n"); fprintf(stdout,"hello fprintf\n"); fputs("hello fupts\n",stdout); return 0; }
[root@VM-12-7-centos Blog_File]# cat test.txt hello print hello fprintf hello fupts
输入重定向
#include<stdio.h> #include<unistd.h> #include<sys/stat.h> #include<sys/types.h> #include<fcntl.h> int main(){ int fd = open("./test.txt",O_RDONLY); dup2(fd,0); char buffer[1024]; scanf("%s",buffer); printf("%s\n",buffer); return 0; }
[root@VM-12-7-centos Blog_File]# cat test.txt helloworld [root@VM-12-7-centos Blog_File]# ./Blog_file helloworld
四、缓冲区
4.1 概述
在Linux操作系统中,用户端与内核端都存在缓冲区。对于内核端的缓冲区,则是为了减缓磁盘或者某种外设与内存间的速度差异而设置,在此不做赘述。对于用户层的缓冲区内容,该缓冲区用于写入文件的内核缓冲区,在写入过程中,是需要文件描述符来作为指示的。以C语言为例,用户层的缓冲区通过结构体FILE
来存储,该结构体封装了文件描述符与缓冲区相关内容,在达到某种情况时,可以通过文件描述符将缓冲区内容写入文件的内核缓冲区中。对于C语言的源码可以发现对应相关的内容:
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
};
4.2 缓冲区刷新策略
用户端缓冲区刷新至内核态缓冲区的刷新策略:
- 不缓冲:立刻刷新
- 行缓冲:比如显示器打印
- 全缓冲:缓冲区满才进行刷新,比如磁盘文件中写入
- 进程退出时,回啥信用户层缓冲区的数据到内核缓冲区中
为了进行验证,通过一个实验进行说明,代码如下:
对于以下代码,如果不关闭fd,则会在test.txt中完成输出重定向,可是如果关闭fd,不会完成输出重定向。原因是由于进行了输出重定向,对于用户层缓冲区来说,刷新策略发生了改变。当进行重定向后,刷新策略从行缓冲改变为全缓冲,此时如果不关闭文件标识符,在进程退出时,会刷新缓冲区,可是当文件没有关闭时,为全缓冲策略,此时缓冲区未满不进行缓冲,当进程结束时,同样不存在打开的文件描述符,不会进行缓冲区刷新。如果需要进行缓冲,则可以通过fflush()
完成刷新功能。
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main(){
close(1);
int fd = open("./test.txt",O_WRONLY | O_TRUNC);
// stdout -> 1 -> struct file* -> test.txt
printf("hello print\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fupts\n",stdout);
// close(fd);
return 0;
}
[root@VM-12-7-centos Blog_File]# cat test.txt
hello print
hello fprintf
hello fupts
int main(){
close(1);
int fd = open("./test.txt",O_WRONLY | O_TRUNC);
// stdout -> 1 -> struct file* -> test.txt
printf("hello print\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fupts\n",stdout);
close(fd);
return 0;
}
[root@VM-12-7-centos Blog_File]# cat test.txt
[root@VM-12-7-centos Blog_File]#
int main(){
close(1);
int fd = open("./test.txt",O_WRONLY | O_TRUNC);
// stdout -> 1 -> struct file* -> test.txt
printf("hello print\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fupts\n",stdout);
fflush(stdout);
close(fd);
return 0;
}
[root@VM-12-7-centos Blog_File]# ./Blog_file
[root@VM-12-7-centos Blog_File]# cat test.txt
hello print
hello fprintf
hello fupts
五、文件系统
5.1 概述
在上述内容中,讨论了单个文件的系统调用操作以及进程管理打开后的文件管理方式,不过对于另外未打开的文件,将会存在于磁盘之中,在此对Linux的EXT系列文件系统进行简单的介绍。
从最底层的磁盘出发,磁盘是计算机中的一个机械设备,主要组成可分为:磁盘臂、读写头、转轴、磁道、盘片、柱面、扇区等,其中最为主要的概念是盘面、磁道和扇区**。对于磁盘来说,写入的最基本单位是扇区,通过盘面、磁道和扇区,可以确定存储的具体位置,这种定位方式,可以将盘片抽象为一种线性结构**。而对于操作系统而言,就是运用了这种思想,将磁盘认为是一种线性结构。
通过转换为线性结构思想,操作系统管理磁盘的方式还需对其进行分区与格式化的操作,分区是为了细分磁盘区域,便于管理,而格式化,是写入对应的文件系统。对于分区后的磁盘空间,操作系统会对其划分为一个个的块,将其分为了Boot Block与Block Group。而对于Block Group来说,再次进行了分块操作,其中可以分为五个部分,分别为:Super Block、Group Descriptor Table、Block Bitmap、inode Bitmap 、inode Table 、Data blocks。示意图如下:
5.2 inode
对于文件系统而言,每个分块都有各自的作用,其中重要关键需要掌握的内容是关于inode的理解。对于Block Bitmap与inode Bitmap可以合称为位图结构,inode Table 与 Data blocks可以合称为数据结构,以下将逐一介绍:
- Boot Block:启动块,大小为1kb,由pc标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能操作该块。
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
- Super Block:超级块,存放文件系统本身的结构信息。每1个Block group中都有SuperBlock,group 0中为主,其他group为辅,主要是将SuperBlock进行备份,避免因为一点损害导致全局不可用。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- Group Descriptor Table:块组描述符,描述块组属性信息,Block Bitmap的起始位置和length等。
- inode Table:inode的数据节点的存储,存放文件属性,如文件大小、所有者、最近修改时间等
- Data Blocks:存放文件内容,具体的数据节点存储。
- Block Bitmap:Block Bitmap中记录着Data Block中数据块占用情况与未占用情况。
- inode Bitmap:描述inode Table中的inode的占用与空闲情况。
而对于文件的工作原理,则是通过每一个块的协同作用完成的,要了解对应的工作原理,除了掌握每种块的作用外,还要了解一下各自的使用原理和一些补充概念,其中主要补充概念有位图、inode、以及目录。
- 位图结构(inode Bitmap与Block Bitmap):对于位图而言,它是一串二进制数,对于该二进制数,其比特位位置含义是inode Table的inode编号或者Data Block的数据块编号,其比特位的内容含义则是表示其特定的inode或者数据块是否被占用,位图的方式是二进制数从右向左。
- inode:inode可以将其理解为一个结构体,储存了所有关于文件的属性信息,当不仅如此,还有许多关于其他的信息。对于每个文件来说,包括文件属性和文件信息的部分,因此inode不仅记录属性信息,还记录了对应的文件内容数据信息,需要通过数据结构来映射inode所指向的文件内容信息块,比如通过记录相应的位图完成。对于每个inode而言,还需要对自身进行说明,因此需要通过用一个inode号码作为唯一指向。故inode的主要内容就包括文件的所有属性,文件的内容块映射和唯一inode号码标识。
- 目录:目录也是文件,也是存在inode来记录文件属性的,而在目录的数据内容块中,则是储存了关于文件名与inode编号对应的信息。在学习过程中可以发现,每次创建一个文件,都是需要在特定目录下完成的,而在该目录下的数据内容块中,则是完成了文件名和inode编号数据的对应。
了解相应概念后,可以对文件的工作原理进行探讨,以创建文件、写入文件和删除文件为例进行说明,同时通过示意图进行展示
- 创建文件:首先选定特定分区,在相应目录下完成文件名与inode编号的映射,其中inode的选取需要查看inode Bitmap中的位图信息,从右向左查找关于空闲的inode块并将其状态空闲转为占用。
- 写入文件:通过文件名,查询相应目录,在目录内容数据块中查询inode编号,在目录的数据块中查询相应inode进行书写,当inode指向的数据块不存在或已满时,需要增添数据块,则通过Block Bitmap位图查询空数据块以及修改位图内容,并将指定数据块的位图信息写入inode,以及在特定数据块中储存写入信息。
- 删除文件:删除文件更为简单,只需修改位图信息即可。
六、软硬连接
6.1 软连接
软连接:类似于常用的快捷方式,有直接独立inode的,说明软连接是一个独立文件,有自己独立的属性集也有独立的数据块,而数据块的内容是保存文件所在路径和文件名。
shell
建立方式:ln -s [源文件或目录] [目标文件或目录]
shell
解除方式:unlnink [目标文件或目录]
实例使用:通过C语言完成进度条展示的可执行程序书写,通过软连接连接该可自行程序,命名为prochar,并将其执行。最后对连接进行解除。
[root@VM-12-7-centos Blog_File]# ./Blog_file
[====================================================================================================][100%]
[root@VM-12-7-centos Blog_File]# ln -s ./Blog_file prochar
[root@VM-12-7-centos Blog_File]# ll -li ./prochar
1054393 lrwxrwxrwx 1 root root 11 Jan 6 18:04 ./prochar -> ./Blog_file
[root@VM-12-7-centos Blog_File]# ./prochar
[====================================================================================================][100%]
[root@VM-12-7-centos Blog_File]# unlink ./prochar
6.2 硬链接
硬链接:本质不是一个独立的文件,而是在目录下的一个文件名和inode编号的映射关系的设定,没有自己独立的inode,为此在inode中应有关于硬链接数的属性
shell
建立方式:ln [源文件或目录] [目标文件或目录]
shell
解除方式:unlnink [目标文件或目录]
实例使用:通过C语言完成进度条展示的可执行程序书写,通过硬连接连接该可自行程序,命名为prochar.hard,并将其执行。最后对连接进行解除。
[root@VM-12-7-centos Blog_File]# ln Blog_file prochar.hard
[root@VM-12-7-centos Blog_File]# ll -li ./prochar.hard
1054352 -rwxr-xr-x 2 root root 17688 Jan 6 17:50 ./prochar.hard
[root@VM-12-7-centos Blog_File]# ./prochar.hard
[====================================================================================================][100%] [root@VM-12-7-centos Blog_File]# unlink prochar.hard
七、动静态库
7.1 概述
在Linux中,库都是文件,且库一般分为两种:动态库与静态库,有各自的命名规则,如果是动态库,库文件是以.so
作为后缀的,如果是静态库,库文件是以.a
作为作为后缀的。而库的真实名字是去掉lib前缀,去掉.a-
、.so-
(包括后续内容)后缀的余下内容。
在Linux中的C程序中,一般情况下,正常编译是链接了动态库的,可以通过ldd 文件名
进行查询,比如如下是链接了C语言的动态库。
[root@VM-12-7-centos Blog_File]# ldd Blog_file
linux-vdso.so.1 (0x00007ffc685bd000)
libc.so.6 => /lib64/libc.so.6 (0x00007f9df7231000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9df75f6000)
[root@VM-12-7-centos Blog_File]# ls /lib64/libc.so.6 -l
lrwxrwxrwx 1 root root 12 Mar 12 2021 /lib64/libc.so.6 -> libc-2.28.so
当然也可以通过在Makefile文件中,添加编译选项-static
使用静态库,但在Linux服务器中,一般是未安装的,需要自行安装。编译后可以通过file 文件名
来查看使用库的情况。
动态库与静态库的主要区别在于:
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
库,实际就是二进制文件,一套完整的库是包含库文件本身、头文件和说明文档的,头文件和说明文档是起到对库中的方法进行说明,这样的设计既方便使用,同时也是保证了代码的私密性。
7.2 头文件源文件汇编
对于库的制作,是将源代码转换为可重定向目标文件,通过头文件和库的内容完成制作。在此首先介绍不需要将其打包为库的过程,为此设定了以下的文件结构,并在其中写入相应代码.
[root@VM-12-7-centos Blog_File]# tree test_lib/
test_lib/
├── lib
│ ├── add.c
│ ├── add.h
│ ├── sub.c
│ └── sub.h
└── test.c
1 directory, 5 files
[root@VM-12-7-centos Blog_File]# cat test_lib/lib/add.h
#pragma once
#include <stdio.h>
int _add(int x,int y);
[root@VM-12-7-centos Blog_File]# cat test_lib/lib/add.c
#include "add.h"
int _add(int x,int y){
return x + y;
}
[root@VM-12-7-centos Blog_File]# cat test_lib/lib/sub.h
#pragma once
#include <stdio.h>
int _sub(int x,int y);
[root@VM-12-7-centos Blog_File]# cat test_lib/lib/sub.c
#include "sub.h"
int _sub(int x,int y){
return x - y;
}
[root@VM-12-7-centos Blog_File]# cat test_lib/test.c
#include "./lib/add.h"
#include "./lib/sub.h"
int main(){
int x = 10;
int y = 20;
printf("x + y = %d\n", _add(x , y));
printf("x - y = %d\n", _sub(x , y));
return 0;
}
对该目录下的Makefile进行书写,其中obj
表示把对应文件的变量名作为obj
变量,而可执行文件的test
依赖于obj
变量中的文件名,要依赖这些文件名,就需要在当前目录和lib目录下的.c
文件生成.o
文件,从而完成依赖。从而达到汇编头文件与源文件的目的。
obj = test.o add.o sub.o
test:$(obj)
gcc -o $@ $^
%.o:%.c
gcc -c $<
%.o:./lib/%.c
gcc -c $<
.PHONY:clean
clean:
rm -f *.o test
7.3 制作静态库
在同样的目录结构下制作静态库libmath.a
,其中包含内容为各个可重定向目标文件与对应的头文件,其中在Makefile文件中进行对应的编译设置。
首先,要对静态库完成生成,在此使用了指令’ar -rc 静态库名称 需要带包文件列表(.o)
,其中ar
表示gnu归档工具,而rc
表示replace and create。但是由于依赖的各个.o
文件是不存在的,因此要通过编译产生,故完整的makefile文件如下:
libmath.a:add.o sub.o
ar -rc $@ $^
%.o:%.c
gcc -c $<
.PHONY:clean
clean:
rm -f *.o output libmath.a
生成静态库后,还可以在makefile文件中补充打包和安装过程,打包过程为将头文件和库带包在一起,安装过程则是将头文件和库安装到对应的系统存放头文件和静态库的位置,内容如下:
.PHONY:output
output:
mkdir output
cp -rf *.h output
cp libmath.a output
.PHONY:install
install:
cp *.h /usr/include
cp libmath.a /lib64
生成静态库后,可以通过指令ar -tv 库名.a
查看静态库中的目录列表,其中-t
表示列出静态库的文件,-v
表示verbose即详细信息,示例如下:
[root@VM-12-7-centos lib]# ar -tv libmath.a
rw-r--r-- 0/0 1240 Jan 6 19:37 2023 add.o
rw-r--r-- 0/0 1232 Jan 6 19:37 2023 sub.o
完成对于静态库的制作后,便可以对静态库进行使用,同样在makefile文件中进行,此时的makefile为test.c的目录下的makefile文件。
其中需要通过gcc
指令进行编译,指令为gcc -o $@ $^ -I指定头文件搜索路径 -L指定库文件搜索路径 -l库名
,而库名为静态库的真实命名,即去掉lib
前缀与.a
后缀。
示例如下:
test:test.c
gcc -o $@ $^ -I./lib -L./lib -lmath
.PHONY:clean
clean:
rm -f test
完成编译后,既可运行对应的可执行程序,由于静态库的特性测试目标文件生成后,静态库删掉,程序照样可以运行。
7.4 制作动态库
动态库的制作与静态库的制作是极为相似的,是根据指令的不同而区分,同样在makefile文件中进行说明。对于动态库的生成制作,不再是使用ar
指令,而是使用gcc
编译器,选项为-shared
,其含义为生成共享库格式,具体为gcc -shared
。同样的需要生成动态库,也需要依赖于可重定向目标文件,但是由于生成策略不同,需要添加选项-fPIC
,其含义为产生位置无关吗(position independence code),具体makefile示例如下:
libmath.so:add.o sub.o
gcc -shared -o $@ $^
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
rm -rf *.o output libmath.so
.PHONY:output
output:
mkdir output
cp -rf *.h output
cp libmath.so output
.PHONY:install
install:
cp *.h /usr/include
cp libmath.so /lib64
完成动态库的制作后,可以使用动态库,为此通过makefile进行编译,其内容是与静态库相同的,makefile示例如下:
test:test.c
gcc -o $@ $^ -I./lib -L./lib -lmath
.PHONY:clean
clean:
rm -f test
在编译成功后,运行可执行程序,会出现报错问题,其中报错原因是没有找到相应的动态库,可是在之前编译的过程中,已经是设定了动态库的搜索路径与动态库名称,但是那只是编译过程,与运行可执行程序无关。为了解决这一报错,需要设定加载器,在运行运行的时候,进一步告知系统,动态库的位置。其中主要有三种方法,分别为:
-
拷贝
.so
文件到系统共享库路径下, 一般指/usr/lib,该方法不推荐使用 -
更改环境变量
LD_LIBRARY_PATH
,将该环境变量设置为库路径,示例如下:[root@VM-12-7-centos output]# pwd /home/lht/code/Blog_File/test_lib/output [root@VM-12-7-centos output]# export LD_LIBRARY_PATH=/home/lht/code/Blog_File/test_lib/output [root@VM-12-7-centos output]# echo $LD_LIBRARY_PATH /home/lht/code/Blog_File/test_lib/output [root@VM-12-7-centos output]# cd .. [root@VM-12-7-centos test_lib]# ldd test linux-vdso.so.1 (0x00007ffdc6dd0000) libmath.so => /home/lht/code/Blog_File/test_lib/output/libmath.so (0x00007f3e1e61d000) libc.so.6 => /lib64/libc.so.6 (0x00007f3e1e258000) /lib64/ld-linux-x86-64.so.2 (0x00007f3e1e81f000)
-
ldconfig
配置:在/etc/ld.so.conf.d/路径下创建配置文件,写入动态库的路径,并通过ldconfig
指令完成更新,示例如下:[root@VM-12-7-centos ~]# cd /etc/ld.so.conf.d/ [root@VM-12-7-centos ld.so.conf.d]# ll total 8 -rw-r--r--. 1 root root 26 May 28 2021 bind-export-x86_64.conf -r--r--r-- 1 root root 67 Dec 22 2021 kernel-4.18.0-348.7.1.el8_5.x86_64.conf [root@VM-12-7-centos ld.so.conf.d]# touch myconf.conf [root@VM-12-7-centos ld.so.conf.d]# ll total 8 -rw-r--r--. 1 root root 26 May 28 2021 bind-export-x86_64.conf -r--r--r-- 1 root root 67 Dec 22 2021 kernel-4.18.0-348.7.1.el8_5.x86_64.conf -rw-r--r-- 1 root root 0 Jan 6 20:57 myconf.conf [root@VM-12-7-centos ld.so.conf.d]# vim myconf.conf [root@VM-12-7-centos ld.so.conf.d]# ldconfig [root@VM-12-7-centos ld.so.conf.d]# cat myconf.conf /home/lht/code/Blog_File/test_lib/output [root@VM-12-7-centos ld.so.conf.d]# ldconfig
[root@VM-12-7-centos test_lib]# ldd test linux-vdso.so.1 (0x00007ffdb1f12000) libmath.so => /home/lht/code/Blog_File/test_lib/output/libmath.so (0x00007f06c3292000) libc.so.6 => /lib64/libc.so.6 (0x00007f06c2ecd000) /lib64/ld-linux-x86-64.so.2 (0x00007f06c3494000)
7.5 小节
在该小节中主要介绍了三种汇编过程
- 头文件源文件汇编:注意依赖关系的设定,先生成对应的可重定向目标文件,在完成可执行文件的生成
- 制作静态库:本质是打包所有
.o
文件,使用ar
来完成打包,在通过gcc
完成编译生成可执行文件 - 制作动态库:本质是打包所有
.o
文件,生成.o
文件时需要添加选项-fPIC
,再使用gcc -shared
进行打包动态库,完成打包后需要对加载器进行设置,可以通过配置文件设置或者通过修改环境变量设置
对于库的提供,本质就是打包所有的.o
文件以及头文件,如果只提供静态库,只能静态链接到程序之中,如果只提供动态库,只能动态链接程序之中,如果都存在时优先使用动态链接,而需要使用静态链接时可以通过选项-static
完成编译。
补充:
- 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
- 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!