主页 > 互联网  > 

【Linux】基础IO,软硬链接,动静态库

【Linux】基础IO,软硬链接,动静态库
1. 认识IO 什么是IO

I/O简单来说对应的就是两个单词Input和Output,指的是计算机系统与外部环境(通常是硬件设备或其他计算机系统)之间的数据交换过程

I/O 可以分为两种主要类型:

输入(Input): 输入是指计算机系统接收来自外部环境的数据或信息的过程。例如,当用户在键盘上输入文本时,这些输入字符被视为输入操作。输入可以来自各种外部源,包括键盘、鼠标、传感器、网络连接、磁盘驱动器等。计算机系统需要能够接收和处理这些输入数据,以便执行相应的操作。输出(Output): 输出是指计算机系统将处理后的数据或信息发送到外部环境的过程。例如,当计算机系统将文本显示在屏幕上、打印文档到打印机或将数据写入磁盘驱动器时,这些都是输出操作。输出使计算机系统能够与用户或其他设备进行有效的通信。

在计算机程序中,I/O 操作通常是相对较慢的操作,因为涉及到与外部设备通信或存储介质的读写,而这些操作比内存中的数据处理更为耗时。因此,在编写高效的程序时,需要注意如何最小化 I/O 操作的次数,以提高性能。此外,对于某些应用程序,如数据库管理系统和网络通信应用程序,有效的 I/O 管理对于系统的响应时间和吞吐量至关重要。

2.C文件接口的IO 写文件

#include <stdio.h> #include <string.h> int main() { FILE* fp=fopen("myfile","w"); if(fp==NULL){ perror("open"); return 1; } const char* str="hello world\n"; int cnt=5; while(cnt--) fwrite(str,strlen(str),1,fp); fclose(fp); return 0; }

读文件 #include <stdio.h> #include <string.h> int main() { FILE* fp=fopen("myfile","r"); if(fp==NULL){ perror("open"); return 1; } const char* msg="hello world\n"; char* str[1024]; while (1) { size_t s=fread(str,1,strlen(msg),fp); if(s>0) { str[s]=0; printf("%s",str); } if(feof(fp)) break; } fclose(fp); return 0; }

3. 系统文件接口的I/O

操作文件,除了上述C接口(当然其他语言也有),我们还可以采用系统接口来进行文件访问

接口介绍 #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理解:直接看man 手册,比什么都清楚。 open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

写文件 #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(){ int fd=open("myfile",O_WRONLY); if(fd<0){ perror("open"); } const char* msg="hello world!!\n"; int cnt=5; while (cnt--){ //fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据 write(fd,msg,strlen(msg)); } close(fd); return 0; } 读文件 #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd=open("myfile",O_RDONLY); if(fd<0){ perror("open"); } const char* msg="hello world!!\n"; char* str[1024]; while (read(fd,str,strlen(msg))) { printf("%s",str); } close(fd); return 0; } open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用和 库函数

上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。 而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

文件描述符fd

这个文件描述符fd究竟是个什么数字呢?

我们可以试着把它打印出来看看

#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd=open("myfile",O_RDONLY); if(fd<0){ perror("open"); } printf("%d\n",fd); close(fd); return 0; } //输出结果 //3

文件描述符就是一个小整数,但它为什么是3呢?学过C语言的应该都知道

C默认会打开三个输入输出流,分别是stdin, stdout, stderr仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针,其实这三个流就对应数字0,1,2,进而可以得出,fd本质上就是数组的下标,只不过这个数组是系统帮我们维护的

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2

0,1,2对应的物理设备一般是:键盘,显示器,显示器、

所以输入输出还可以采用如下方式:

#include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { char buffer[1024]; ssize_t s=read(0,buffer,sizeof buffer); if(s>0){ buffer[s]=0; write(1,buffer,strlen(buffer)); write(2,buffer,strlen(buffer)); } return 0; } //运行结果 //输入:hello world!!! //输出: //hello world!!! //hello world!!! 文件描述符的分配规则

上面的例子中,我们新打开一个文件输出后发现是 fd: 3

现在关闭文件描述符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; }

发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

那如果我们关闭标准输出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); close(fd); return 0; }

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

//比如我想让myfile文件写入ls -l 打印的内容 [hdm@centos7 BasicIO]$ ls -l > myfile [hdm@centos7 BasicIO]$ cat myfile total 28 -rw-r--r-- 1 hdm wheel 65 Sep 9 15:56 makefile -rw-r--r-- 1 hdm wheel 0 Sep 10 12:40 myfile -rwxr-xr-x 1 hdm wheel 16576 Sep 10 12:33 test -rw-r--r-- 1 hdm wheel 652 Sep 10 12:33 test.c 重定向

重定向的本质是什么呢?

不过我们要实现重定向,并不会像上面的列子一样,先把一个文件描述符关闭再打开,系统给了我们一套接口

dup2 系统调用

函数原型如下:

#include <unistd.h> int dup2(int oldfd, int newfd); #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd=open("myfile",O_WRONLY | O_CREAT,0644); if(fd<0){ perror("open"); } dup2(fd,1); printf("fd: %d\n", fd); fflush(stdout); const char* str="hello\n"; const char* str1="nihao\n"; fprintf(stdout,"%s",str1); write(1,str,strlen(str)); write(fd,str,strlen(str)); 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 <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { const char* str1="hello printf\n"; const char* str2="hello fprintf\n"; const char* str3="hello write\n"; printf("%s",str1); fprintf(stdout,"%s",str2); write(1,str3,strlen(str3)); fork(); return 0; } //运行出结果: //hello printf //hello fprintf //hello write

但如果对进程实现输出重定向呢? ./test > myfile , 我们发现结果变成了:

[hdm@centos7 BasicIO]$ ./test >myfile [hdm@centos7 BasicIO]$ cat myfile hello write hello printf hello fprintf hello printf hello fprintf

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。

为什么呢?肯定和fork有关!

一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。

printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。

而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后

但是进程退出之后,会统一刷新,写入文件当中。

但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。

write 没有变化,说明没有所谓的缓冲。

综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

4. 理解文件系统

我们使用ls -l的时候看到的除了看到文件名,还看到了文件的其他数据。

[hdm@centos7 BasicIO]$ ls -l total 12 -rw-r--r-- 1 hdm wheel 65 Sep 10 12:57 makefile -rw-r--r-- 1 hdm wheel 66 Sep 10 15:50 myfile -rw-r--r-- 1 hdm wheel 1101 Sep 10 15:48 test.c

每行包含7列:

模式

硬链接数

文件所有者

大小

最后修改时间

文件名

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

[hdm@centos7 BasicIO]$ stat test.c File: ‘test.c’ Size: 1101 Blocks: 8 IO Block: 4096 regular file Device: fd01h/64769d Inode: 1582421 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 1000/ hdm) Gid: ( 10/ wheel) Access: 2023-09-10 15:48:26.249722588 +0800 Modify: 2023-09-10 15:48:26.086715074 +0800 Change: 2023-09-10 15:48:26.086715074 +0800 Birth: -

上面的执行结果有几个信息需要解释清楚

inode

为了能解释清楚inode我们先简单了解一下文件系统

Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个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一个新文件来看看如何工作

[hdm@centos7 BasicIO]$ touch myfile [hdm@centos7 BasicIO]$ ls -i myfile 1574424 myfile

为了说明问题,我们将上图简化

创建一个新文件主要有一下4个操作:

存储属性 内核先找到一个空闲的i节点(这里是1574424)。内核把文件信息记录到其中。

存储数据 该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。

记录分配情况 文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

添加文件名到目录

新的文件名myfile。linux如何在当前的目录中记录这个文件?内核将入口(1574424,myfile)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

理解软硬链接

我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。

//ln 属于硬链接的文件 硬链接文件的名字 ln file file.link [hdm@centos7 BasicIO]$ touch myfile [hdm@centos7 BasicIO]$ ls -li 1574424 -rw-r--r-- 1 hdm wheel 66 Sep 11 13:27 myfile hdm@centos7 BasicIO]$ ln myfile myfile.link [hdm@centos7 BasicIO]$ ls -li 1574424 -rw-r--r-- 2 hdm wheel 66 Sep 11 13:27 myfile 1574424 -rw-r--r-- 2 hdm wheel 66 Sep 11 13:27 myfile.link [hdm@centos7 BasicIO]$ unlink myfile.link [hdm@centos7 BasicIO]$ ls -li 1574424 -rw-r--r-- 1 hdm wheel 66 Sep 11 13:27 myfile [hdm@centos7 BasicIO]$ rm myfile [hdm@centos7 BasicIO]$ ls -li

myfile和myfile.link的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 1574424的硬连接数为2。 我们在删除文件时干了两件事情:

1.在目录中将对应的记录删除,

2.将硬连接数-1,如果为0,则将对应的磁盘释放。

为什么创建目录之后,它的当前目录硬链接数会加一?

因为在当前目录比如test下创建一个目录比如a,那么进入这个目录a之后,里面会存在两个隐藏目录, 一个. ..对应的含义分别是当前目录和上级,而这个 ..就是对上级目录test的硬链接。而且这样对目录的硬链接我们普通用户自己不能进行操作,只能由操作系统分配

为什么我们自己不能对目录进行硬链接?

循环链接问题: 允许目录硬链接可能导致循环链接的问题。当一个目录被硬连接到另一个目录时,可以创建一个链接链,其中一个目录包含硬连接到另一个目录,而另一个目录包含硬连接到第一个目录,从而形成一个无限循环的链接链。这种循环链接对文件系统的稳定性和可用性造成了严重威胁,因为它会导致文件系统陷入无限循环的操作中。文件系统的一致性和安全性: 目录是用于组织文件和子目录的结构,包括文件和子目录的层次结构和元数据(如权限、所有者等)。如果允许硬链接目录,可能会导致混淆和权限问题。例如,不同的用户或进程可以在不同位置创建硬链接,这可能导致混淆和权限冲突。目录硬链接可能会对文件系统的一致性和安全性造成严重影响。性能问题: 如果目录可以硬连接,那么在处理目录结构时,文件系统必须处理硬链接的情况,这可能导致性能问题和更复杂的目录操作实现。

软链接 硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法

软连接其实就跟windows系统上的文件快捷方式一样,只要被链接那个文件删除了,那么软连接也将失效

//ln 属于软链接的文件 软链接文件的名字 ln -s file file.link [hdm@centos7 BasicIO]$ touch myfile [hdm@centos7 BasicIO]$ ls -li 1574424 -rw-r--r-- 1 hdm wheel 0 Sep 11 13:34 myfile [hdm@centos7 BasicIO]$ ln -s myfile myfile.link.s [hdm@centos7 BasicIO]$ ls -li 1574424 -rw-r--r-- 1 hdm wheel 0 Sep 11 13:34 myfile 1574967 lrwxrwxrwx 1 hdm wheel 6 Sep 11 13:34 myfile.link.s -> myfile

我们在用 stat命令进行查看文件属性的时候,会出现三行时间,那它们分别代表上面含义呢?

Access 最后访问时间

访问时间: 访问时间是文件或目录上一次被访问的时间。每当文件或目录被打开、读取或执行时,访问时间都会被更新。访问时间的记录有助于了解文件或目录的访问频率,但在某些情况下,频繁更新访问时间可能会对性能产生影响。可以使用 stat 或 ls -lu 命令查看文件的访问时间。

Modify 文件内容最后修改时间

访问时间: 访问时间是文件或目录上一次被访问的时间。每当文件或目录被打开、读取或执行时,访问时间都会被更新。访问时间的记录有助于了解文件或目录的访问频率,但在某些情况下,频繁更新访问时间可能会对性能产生影响。可以使用 stat 或 ls -lu 命令查看文件的访问时间。

Change 属性最后修改时间

更改时间: 更改时间是文件或目录上一次元数据(metadata)发生更改的时间。元数据包括文件的权限、所有者、链接数等信息。当文件的元数据发生更改时(例如,文件的权限被修改),更改时间将被更新。更改时间还包括文件或目录的创建时间。可以使用 stat 命令查看文件的更改时间。

5. 动态库和静态库

动态库和静态库是在编程中常用的两种库文件类型,它们有不同的特点和用途。

静态库(Static Library): 静态库是一组已编译的对象文件的集合,通常以.a(在Unix/Linux)或.lib(在Windows)为文件扩展名。当你将静态库链接到一个可执行文件中时,编译器会将库中的代码和数据复制到最终的可执行文件中。静态库使得可执行文件具有所有必需的代码,因此它们通常较大。静态库的优点是独立性,不需要依赖外部库文件,但可能导致可执行文件变得较大。 动态库(Dynamic Link Library, DLL,或Shared Library): 动态库也是一组已编译的对象文件的集合,通常以.dll(在Windows)或.so(在Unix/Linux)为文件扩展名。与静态库不同,动态库的代码和数据不会被复制到可执行文件中。相反,可执行文件包含指向库的引用,库在运行时加载到内存中。动态库使得多个程序可以共享同一份库,节省了磁盘和内存空间。动态库的缺点是,如果库文件不存在或版本不匹配,程序可能无法运行。

选择使用动态库还是静态库取决于项目的需求和设计。通常情况下:

使用动态库可以减小可执行文件的大小,因为多个程序可以共享同一个库。使用静态库可以确保可执行文件的独立性,不受外部库的影响,但可能导致可执行文件变得较大。 生成静态库

我们可以自己制定一个动静态库来测试,例如

/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__ /add.c/ #include "add.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(%d,%d)=%d\n", a, b, add(a, b)); a = 100; b = 200; printf("sub(%d,%d)=%d\n", a, b, sub(a, b)); } [hdm@centos7 myfile]$ ls add.c add.h main.c sub.c sub.h [hdm@centos7 myfile]$ gcc -c add.c -o add.o [hdm@centos7 myfile]$ gcc -c sub.c -o sub.o 生成静态库 [hdm@centos7 myfile]$ ar -rc libmymath.a add.o sub.o ar是gnu归档工具,rc表示(replace and create) 查看静态库中的目录列表 [hdm@centos7 myfile]$ ar -tv libmymath.a rw-r--r-- 1000/10 1240 Sep 11 15:49 2023 add.o rw-r--r-- 1000/10 1232 Sep 11 15:50 2023 sub.o t:列出静态库中的文件 v:verbose 详细信息 [hdm@centos7 myfile]$ gcc main.c -L. -lmymath -L 指定库路径 -l 指定库名 测试目标文件生成后,静态库删掉,程序照样可以运行。

库搜索路径

从左到右搜索-L指定的目录。

由环境变量指定的目录 (LIBRARY_PATH)

由系统指定的目录 /usr/lib /usr/local/lib

生成动态库

shared: 表示生成共享库格式 fPIC:产生位置无关码(position independent code) 库名规则:libxxx.so

[hdm@centos7 myfile]$ ls add.c add.h add.o libmymath.a main.c sub.c sub.h sub.o [hdm@centos7 myfile]$ gcc -fPIC -c sub.c add.c [hdm@centos7 myfile]$ gcc -shared -o libmymath.so *.o [hdm@centos7 myfile]$ ls -l total 44 -rw-r--r-- 1 hdm wheel 57 Sep 11 15:42 add.c -rw-r--r-- 1 hdm wheel 0 Sep 11 15:36 add.h -rw-r--r-- 1 hdm wheel 1240 Sep 11 15:57 add.o -rw-r--r-- 1 hdm wheel 2680 Sep 11 15:50 libmymath.a -rwxr-xr-x 1 hdm wheel 15824 Sep 11 15:58 libmymath.so -rw-r--r-- 1 hdm wheel 236 Sep 11 15:52 main.c -rw-r--r-- 1 hdm wheel 57 Sep 11 15:43 sub.c -rw-r--r-- 1 hdm wheel 66 Sep 11 15:42 sub.h -rw-r--r-- 1 hdm wheel 1232 Sep 11 15:57 sub.o

使用动态库

编译选项 l:链接动态库,只要库名即可(去掉lib以及版本号) L:链接库所在的路径.

示例: gcc main.c -lmymath -L.

运行动态库 1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib(需要管理员权限) 2、更改 LD_LIBRARY_PATH

标签:

【Linux】基础IO,软硬链接,动静态库由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【Linux】基础IO,软硬链接,动静态库