赞
踩
在学习c语言的时候,fopen如果以写的方式打开文件如果这个文件存在那么文件的内容将会被清空,如果不存在会在当前路径下创建这个文件,那什么是当前路径呢?
我们使用以下代码创建一个文件
我们第一次在程序的所处目录执行该程序,并使用PS命令查看进程,获取进程的pid:
然后我们可以在/proc/中查看进程:
第二次我们在/home/LZH目录下执行该可执行程序
于是我们可以得到如下信息:
操作系统的底层其实给我们提供了文件IO系统调用接口,有的write,read,close和seek登一套系统调用接口,不同的语言会对齐进行封装,封装成对应语言的一套操作文件的库函数,不需要知道底层的调用关系,降低使用者的学习成本。
函数原型如下:
这些选项前三个只可以选择一个,其余的俩个可以与前三个选项通过“|”进行组合。举个例子:我们想要以只写的方式打开一个文件如果文件不存在就创建O_WRONLY|O_CREAT
而这些选项实际上是一个32位大小的数据,这些选项中32位只有1位为1,这一位称为标志位,其余均为0;这样就可以通过“|”来组合选项。(与单片机操作寄存器的方式相同)
我们使用vim打开/usr/include/asm-generic/fcntl.h这个目录下的文件看一看:
系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
系统接口中使用write函数向文件写入相关信息,write函数的函数原型如下:
系统中使用close关闭一个文件。对应函数原型
#inlcude <unistd.h>
int close(int fd);
关闭文件只需要将对应的文件描述符传入即可,如果关闭文件成功则返回0失败返回-1
从用户的角度看,文件可分为普通文件和设备文件两种。
普通文件是指驻留在磁盘或其它外部介质上的一个有序数据集,可以是源文件、目标文件、可执行程序; 也可以是一组待输入处理的原始数据,或者是一组输出的结果。对于源文件、目标文件、 可执行程序可以称作程序文件,对输入输出数据可称作数据文件。
设备文件是指与主机相联的各种外部设备,如显示器、打印机、键盘等。在操作系统中,把外部设备也看作是一个文件来进行管理,把它们的输入、输出等同于对磁盘文件的读和写。 通常把显示器定义为标准输出文件,一般情况下在屏幕上显示有关信息就是向标准输出文件输出。如前面经常使用的printf,putchar 函数就是这类输出。键盘通常被指定标准的输入文件, 从键盘上输入就意味着从标准输入文件上输入数据。scanf,getchar函数就属于这类输入。
当一个c语言程序运行起来时,会默认打开三个流即stdout(标准输出流),stdin(标准输入流)以及stderr(标准错误流)。这3个可以称为终端(Terminal)的标准输入(standard input),标准输出( standard out)和标准错误输出(standard error),当linux开始执行程序的时候,程序默认会打开这3个文件流,这样就可以对终端进行输入输出操作。其对应的设备分别为:显示器,键盘,显示器。下面我们通过man手册查看一下:
#inlcude <stdio.h>
extern FILE* stdin;
extern FILE* stdout;
extern FILE* stderr;
我们来查看一个进程运行起来时它所打开的文件:
我们查看的fd目录下出现的数字被称为文件描述符,每个文件描述都对应一个文件。其中0、1、2文件描述符对应的分别是stdin,stdout,stderr,他们所对应的终端是相同的,对该终端进行输入输出。其中文件描述符3对应的是新打开的文件。
于是我们就可以改变该进程中文件信息(struct FILE结构体中包含的信息)中文件描述符号对应的文件,从而实现重定向。(后面详细解释)
printf()其实就是向stdout中输出,等同于fprintf(stdout,“*”);
perror()其实就是向stderr中输出,相当于fprintf(stderr,“ *”);
那到底stdout,和stderr有什么区别和作用呢?
我们在写程序时用printf()是为了我们能监控我们的程序运行状况,或者是说debug,如果我们的程序是一直运行,不停下来,我们不可能时刻盯着屏幕去看程序输出,这时我们就可以用文件重定向。将输出到一文件中,我们以后就可以看这文件就行。
举例:
#include<stdio.h>
int main()
{
printf("Stdout Helo World!!\n");
fprintf(stdout,"Stdout Hello World!!\n");
perror("Stderr Hello World!!\n");
fprintf(stderr,"Stderr Hello World!!\n");
return 0;
}
编译过后,我们./test > test.txt(默认是将stdout里的内容重定向到文件中),这样就把test程序输出的内容输出到test.txt文件中。还有一种更明晰的写法./test 1>test.txt,这里的1就代表stdout。说到这你应该知道stderr该怎样处理了。
编译过后,./test,屏幕上是四条输出,如果./test > test.txt ,结果是屏幕上输出两条Stderr Hello World!!,Stdout Helo World!!在文件test.txt中,基于上面说的很容易理解现在的结果,于是我们可以随便处理我们想要的输出,例如:
./test 1>stdout.txt 2>stderr.txt,我们将stdout输出到文件stdout.txt中,将stderr输出到stderr.txt文件中;
./test 1>stdout.txt,将stdout输出到文件stdout.txt 中,stderr输出到屏幕上;
./test 2>stderr.txt,将stderr输出到文件stderr.txt中,stdout输出到屏幕上;
./test > test.txt 2>&1,这是将stdout和stderr重定向到同一文件test.txt文件中
并且:stderr,和stdout还有重要一点区别,stderr是没有缓冲的,它立即输出,而stdout默认是行缓冲,也就是它遇到‘\n’,才向外输出内容,如果你想stdout也实时输出内容,那就在输出语句后加上fflush(stdout),这样就能达到实时输出的效果。(缓冲区后面详解)
PS:凡是显示到显示器上的内容都是字符,凡是从键盘读取的内容都是字符,所以键盘和显示器一般被称为字符设备。格式化输入输出就是将字符转换为其它类型或者其它类型转换为字符。
我们所用的一些外设,键盘鼠标显示器等,他们的文件输入输出打开等的操作的实现源码肯定是不同的,比如进程默认打开的三个文件 0 1 2 号文件,他们的open write 等的方法肯定是不同的,而我们打开一个文件,也就是创建一个文件的file结构体,那么不同的设备对文件的操作也该存到这个结构体内部,但是C语言的结构体内只能存变量,不能存函数,所以在结构体file当中有一个file_operations(文件操作)结构体,用来指向硬件提供的一些底层的驱动代码,用来实现对不同外设的文件读写等的操作,这样一来一种结构体的不同的结构体对象就能保存不同外设的文件操作驱动代码。
通过刚刚对stdout&&stderr&&stdin的讨论,我们可以知道存在下面这样的语法:
echo log > /dev/null 2>&1
表示将输出结果重定向到哪里,例如:echo “123” > /home/123.txt
/dev/null :表示空设备文件
所以 echo log > /dev/null 表示把日志输出到空文件设备,也就是将打印信息丢弃掉,屏幕上什么也不显示。
1 :表示stdout标准输出
2 :表示stderr标准错误
& :表示等同于的意思
所以 2>&1 表示2的输出重定向等同于1,也就是标准错误输出重定向到标准输出。因为前面标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。
这个用法平时很常见,重点是为什么这里是用 2 和 1 ,不是3456等等,这要从 Linux 中的文件描述符说起。
文件是由进程打开,而一个进程是可以打开多个文件。系统中也存在着大量的进程那么也就意味着系统中任何时刻都可能存在大量的进程。而我们打开一个文件,需要将文件的相关属性加载到内存当中,操作系统是做管理工作的软件。那么OS系统需要对应这些数据进行管理如何进行管理?为了管理打开的文件,操作系统会给每个打开的文件创建一个结构体struct_file,并以某种数据结构的方式将其组织起来。OS的打开文件的管理也就变成了对数据结构的增删查改等操作。
那么进程怎么知道,那些文件是我打开的了?为了区分文件是那个进程打开的,还需要建立进程和文件之间的对应关系。我们在学习进程的时候,当我们的程序跑起来会将对应的代码和数据加载到内存,并为之创建相关的数据结构(task_struct ,mm_struct,页表)。并通过页表建立虚拟地址和物理地址之间的映射关系。
而为了管理该进程打开的文件,task_struct 有一个指针指向了一下结构体,这个结构体叫做files_struct,结构体里面有一个数组fd_array,而这个数组的下标就是我们说的文件描述符。
我么首先在 linux-5.6.18\include\linux\sched.h 头文件中找到task_struct结构体,在task_struct结构体中可以找到这样一个结构体指针,它指向的是另一个结构体 files_struct 。
里面存放着打开文件的相关属性,文件锁等,都是用来管理文件的,也就是管理file结构体。在末尾有一个数组fd_array[],这个数组是一个结构体指针数组,数组内存放的就是要管理的文件描述结构体也就是file结构体的指针,通过访问这个数组中的地址信息,进程就能对文件进行管理。数组的大小是一个宏定义,转到定义,我们发现这个宏的值是32,也就是通过这个数组,我们可以访问最多32个文件
当进程打开Test.py文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置(0、1、2的位置已经被占用),使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
以上我们讨论的文件描述符表是每一个进程都有的,是进程级的文件描述符表,然后我们还会学习文件系统级别的i-node表。
在"stdio.h"头文件中的搜索"FILE",查看结果如下:
从这里可以看出,文件流指针FILE本质上就是_IO_FILE,即C标准库中的一个结构体;再查找"struct _IO_FILE"结构体的定义:
最终在libio.h头文件中找到_IO_FILE结构体的定义:
这里"_IO_FILE"结构体的成员变量"_fileno"保存的正是文件描述符的数值;通过程序也可以验证这一点
用一句话概括文件流指针与文件描述符的关系就是:文件流指针指向的结构体_IO_FILE内部的成员变量_fileno保存了文件描述符的数值。
知道了FILE结构体和文件描述符的关系后,现在我们来理解一下C的文件接口究竟是如何完成工作的?
以fopen()为例:
(1)给调用者申请struct FILE结构体变量,并返回首地址(FILE*)。
(2)进入系统内核,执行系统调用接口,通过open()打开文件,并返回fd,将fd填充进FILE变量中的fileno。
#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int main() { close(0); close(2); int fd1=open("./log1.txt",O_WRONLY|O_CREAT,0664); int fd2=open("./log2.txt",O_WRONLY|O_CREAT,0664); int fd3=open("./log3.txt",O_WRONLY|O_CREAT,0664); int fd4=open("./log4.txt",O_WRONLY|O_CREAT,0664); printf("%d\n",fd1); printf("%d\n",fd2); printf("%d\n",fd3); printf("%d\n",fd4); close(fd1); close(fd2); close(fd3);、 close(fd4); return 0; }
运行结果如下:
我们发现0和2也被用起来了。现在我们就明白了文件描述符的分配规则是从最小的未被使用的下标开始的。
从文件描述符的分配规则我们可以将文件描述符1分配给新打开的文件,此时使用printf()函数和fput()函数会出现什么现象呢?该如何解释?
运行结果如下:
输入重定向就是,将我们本应该从一个键盘上读取数据,现在重定向为从另一个文件读取数据。
我们的scanf函数是从标准输入读取数据,现在我们让它从log1.txt当中读取数据,我们在scanf读取数据之前close(0)。这样键盘文件就被关闭,这样一样log1.txt的文件描述符就是0。运行结果如下:
举例:
追加重定向和输出重定向的区别是追加重定向不是覆盖数据。
原理其实就只比输出重定向多了一个O_APPEND选项。
1、 dup函数
头文件及函数定义:
#include <unistd.h>
int dup(int oldfd);
dup用来复制参数oldfd所指的文件描述符。当复制成功是,返回最小的尚未被使用过的文件描述符,若有错误则返回-1。
代码示例:
头文件及其定义:
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2与dup区别是dup2可以用参数newfd指定新文件描述符的数值。若参数newfd已经被程序使用,则系统就会将newfd所指的文件关闭,若newfd等于oldfd,则返回newfd,而不关闭newfd所指的文件。
返回值:
若dup2调用成功则返回新的文件描述符,出错则返回-1。
代码示例:
根据应用程序对文件的访问方式,即是否存在缓冲区,对文件的访问可以分为带缓冲区的操作和非缓冲区的文件操作:
两种文件操作的解释和比较:
1、非缓冲的文件操作访问方式,每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
2、ANSI标准C库函数 是建立在底层的系统调用之上,即C函数库文件访问函数的实现中使用了低级文件I/O系统调用,ANSI标准C库中的文件处理函数为了减少使用系统调用的次数,提高效率,采用缓冲机制,这样,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,即需要少量的CPU状态切换,提高了效率。
标准I/O提供了3种类型的缓冲区。
全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。第一次执行I/O操作时,ANSI标准的文件管理函数通过调用malloc函数获得需要使用的缓冲区,默认大小为8192。
//come from /usr/include/stdio.h
/* Default buffer size. */
#ifndef BUFSIZ
#define BUFSIZ _IO_BUFSIZ //BUFSIZ 全局宏定义
#endif
//come from /usr/include/libio.h
#define _IO_BUFSIZ _G_BUFSIZ
//come from /usr/include/_g_config.h
#define _G_BUFSIZ 8192 //真实大小
行缓冲区:在这种情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
注:①标准输入和标准输出设备:当且仅当不涉及交互作用设备时,标准输入流和标准输出流才是全缓冲的。②标准错误输出设备:标准出错绝不会是全缓冲方式的。
对于任何一个给定的流,可以调用setbuf()和setvbuf()函数更改其缓冲区类型。
此函数第一个参数为要操作的流对象,第二个参数buf 必须指向一个长度BUFSIZ 的缓冲区。如果将buf 设置为NULL,则关闭缓冲区。如果执行成功,将返回0,否则返回非0 值。
此函数第一个参数为要操作的流对象;第二个参数buf 必须指向一个长为BUFSIZ 的缓冲区;第三个参数为缓冲区类型,分别定义如下:
//come from /usr/include/stdio.h
/* The possibilities for the third argument to 'setvbuf'. */
#define _IOFBF 0 /* Fully buffered.*/ //全缓冲
#define _IOLBF 1 /* Line buffered. */ //行缓冲
#define _IONBF 2 /* No buffering. */ //无缓冲
第四个参数为该buf的大小。如果指定一个不带缓冲区的流,则忽略buf和size参数。
如果指定全缓冲区或行缓冲区,则buf 和size 可选择地指定一个缓冲区及其长度。如果出现指定该流是带缓冲区的,而buf 是NULL,则标准I/O 库将自动为该流分配适当长度的缓冲,适当长度指的即是由文件属性数据结构(struct stat)的成员st_blksize 所指定的值,如果系统不能为该流决定此值(例如若此流涉及一个设备或一个管道),则分配长度BUFSIZ 的缓冲区。
此函数如果执行成功,将返回0,否则返回非0 值。
要知道Linux下对于向显示器写入采用的是行刷新策略,而写入其它文件时采用的是全刷新策略,为什么要这样设置呢?
因此我们也容易知道:重定向会改变缓冲区的刷新策略。比如说输出重定向,将原来的输出到显示器上策略是行缓冲,现在要将其输出到文件当中采用策略的是全缓冲:下面我们来看一个例子:
我们发现为什么只有系统调用fwrite只打印了一次,而printf和fwrite都打印了两次了?这是为什么?
综上: printf、fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。(刷新数据时,先将数据刷新到内核缓冲区,再由内核刷新到外设)
那这个缓冲区谁提供呢? printf、fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
在FILE结构体中存在:
磁盘设备是一种相当复杂的机电设备。 磁盘设备可以包括一个或多个物理盘片,每个磁盘片分一个或两个存储面(如图(a)所示)。每个磁盘面被组织成若干个同心环,这种环称为磁道track,各磁道之间留有必要的间隙。每条磁道又被逻辑上划分成若干个扇区sectors。在不同扇区之间又保留必要的间隔,图(b)中显示了显示了一个有3个磁道,每个磁道又被分成8 个扇区的磁盘片的一个存储面。
我们所划分出来的这些扇区就是磁盘的最小物理存储单位,同一个同心圆的扇区组合成的园就是磁道(track);由于磁盘里面会有多个碟片,因此在所以碟片上面的同一个磁道可用组合成柱面。
在OS系统中,信息一般以扇区(sectors)的形式存储在硬盘上,而每个扇区包括512个字节的数据和信息(即一个扇区包括两个主要部分:存储数据地点的标识符和存储数据的数据段)。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个块(blocks)。这种由多个扇区组成的”块”,是文件存取的最小单位。”块”的大小,最常见的是4KB,即连续八个 sectors组成一个 blocks。
分区:一块磁盘是比较大的,它有许多柱面,OS系统为了管理它将的每一部分柱面作为一个分区。比如磁盘共有400个柱面,可用划分为4个分区,每个分区100个柱面(通常分区的“最小单位”是柱面);在windows系统下就是将磁盘分为C盘,D盘等等。并且分区会使得数据集中,有助于数据读取的速度与性能。
格式化:将管理信息填入一些管理信息,方便管理,以成为OS系统可以利用的文件系统格式(不同的文件系统写入的管理信息是不同的)
使用ls -l可以得到如下数据:
每行包含7列:(1)模式(2)硬链接数(3)文件所有者
(4)组(5)大小(6)最后修改时间(7)文件名
我们还可以使用stat命令来获取更多的信息
这里便可以看到inode信息主要包括:
(1)文件的字节数,块数 (2)文件拥有者的User ID
(3)文件的Group ID (4)文件的读、写、执行权限
(5)文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
(6)链接数,即有多少文件名指向这个inode
(7)文件数据block的位置
(8)inode编号
文件除了文件的实际内容还有许多的文件属性,在Linux下就是文件的inode号,权限,大小,拥有者/所属组,时间参数,软硬链接等等。文件系统通常会将这俩部分数据分别存放在不同的块,文件属性放到inode中,实际数据则放在数据块中。
为了解释文件属性信息中的inode,我们需要先了解一下文件系统。
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的块组。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,
超级块非常重要,文件系统的基本信息都储存在这里,因此超级块损坏,系统就会奔溃。此外,并不是每一个块组都有超级块,事实上除了第一个块组内含有超级块之外,后续的块组中也可能含有超级块,后续的超级块主要是为第一个块组中的超级块做备份,这样当超级块损坏时可以快速恢复。
数块是用来存放文件数据的地方,在EXT2文件系统下所支持的区块大小有1K,2K和4K共三种。区块的大小在格式化的时候就确定了;并且每个区块都有编号,以方便inode进行记录(inode记录一个区块需要4B)。
注意:每一个区块最多只能放置一个文件的数据,如果文件大于数据块的大小,则一个文件会占用多个数据块,如果文小于数据块,则该块的剩余容量就不能够再次被使用了(磁盘空间会被浪费)。
inode表中有许多inode,inode的数量和大小在格式化的时候就已经固定了,inode大小固定为128B(EXT2系统下);没一个文件都只会占用一个inode,所以文件系统能够建立的文件数量与inode数量有关。系统读取文件的时候需要先找到inode,并分析inode所记录的权限与用户要求的是否符合,符合才可以读取内容。
一个inode的大小为128B,而储存一个数据块好吗需要4B,假设一个文件400MB每个数据块4KB,那么有十万个数据块需要被记录,一个128B大小多空间无法记录这么多数据块;为此inode记录区块号码的区域被定义为12个直接,一个间接,一个双重间接,一个三重间接。
inode有12个直接指向数据块,而一个间接就可以找到一个数据块,而这个数据块被当作记录数据区号码记录区;双重三重间接同理。
这样一个inode就可以指向许多数据块,数据块的大小为16GB(数据块大小为1K时:12K + 256K + 256^2 K+ 256^3K = 16GB)
此时我们知道文件系统将数据块格式化为1K时,能容纳最大文件为16GB。
而删除一个文件时,只需要在俩个位图中的对应位置置为“未使用”即可。
目录也是一种文件,它也有读写可执行三个权限。打开目录,实际上就是打开目录文件。 所以创建一个目录和创建普通文件时,文件系统都是进行相同的操作:文件系统分配一个inode与至少一个数据块给该目录文件。其中inode记录该目录的相关权限与属性,还记录对应的数据块号码;而数据块记录了这个目录下的文件名与该文件名对应的inode号码。使用ls -i可以得到目录内容:
inode本身并不会记录文件名称,文件名是由该文件所在的目录的数据块记录。因此目录下文件的新增,删除,修改文件名都与目录的r和w权限有关。由于目录文件内只有文件名和inode号码,所以如果只有读权限,只能获取文件名,无法获取其他信息,这主要是因为其他信息都储存在文件的inode中,而读取inode内的信息需要目录文件的执行权限(x)。
新增文件,首先申请inode,将文件信息填入inode,然后为文件分配数据块,建立inode和数据块的映射关系,将数据块号码填入inode,然后将文件名和对应的inode号填写懂啊当前的目录的数据块中,在当前目录下添加映射关系,就完成了对文件的添加。
创建命令:
ln 【选项】 原文件 链接文件
-s 创建符号链接(软连接),如果不带这个参数,就是创建硬链接
-f 强制创建文件或目录的链接
-i 覆盖前先询问
-v 显示创建链接的过程
我们来建立三个硬链接,俩个软链接:
我们仔细观察一下发现,test1.txt,test2.txt,test3.txt拥有一样的inode结点(显示结果的第一列),甚至于连权限属性都一模一样。而test4.txt,test5.txt拥有另外一个独立的inode。我们在前面曾经说过,每一个i结点对应一个实际的文件。所以,我们可以发现,建立的硬链接实际上跟我们的源文件是一样的。而软链接则是重新建立了一个独立的文件。
事实上,硬链接的本质就是在该目录下新创建一条新文件名和旧inode号的映射记录而已。
另外,我们观察一下这几个文件的大小,由于我们的源文件是空文件,所以大小是0。那为什么两个硬链接也是0?而软链接却是9呢?
因为硬链接关联着我们的源文件,所以源文件的大小是多大,它们就是多大。至于软链接的大小为什么是9,大家观察一下软链接指向的源文件名的长度,就是9。我们的软链接会写上链接文件的文件名。一个字母一个字节,所以是9个字节,所以软链接的大小是9。
现在我们向test1.txt文件中写入数据,现象如下:
此时我们删除test1.txt观察现象:
当我们删除了源文件之后,发现硬链接还能正常显示原本的内容,而软链接则提示文件不存在。
因为软链接是建立了另一个新的独立的文件,它指向源文件,因为源文件没了,所以它就不能正常指向了;而硬链接实际是一条文件名与i结点的记录。所以,在删除源文件的时候,系统则将链接数减1,当链接数为0的时候,inode就会被系统回收,文件的内容才会被删除。
软链接:
硬链接:
当该目录下再创建一个目录,则该目录的链接数+1
一个目录下的“.”文件就是该目录的一个硬链接,而“…”文件则链接了该目录的上一级目录。
所以我们可以通过目录的硬链接数判断该目录下有几个目录(硬链接数 - 2)。
库文件的本质就是一堆". o"文件的集合(也就是可重定向二进制目标文件),每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块。
例如,C 语言库文件提供有大量的函数(如 scanf()、printf()、strlen() 等),C++ 库文件不仅提供有使用的函数,还有大量事先设计好的类。库文件的产生,极大的提高了程序员的开发效率。因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。
调用库文件为什么还要牵扯到头文件呢?首先,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分。即所有的库文件都提供有相应的头文件作为调用它的接口,库文件是无法直接使用的,只能通过头文件间接调用;所以我们在打包库文件的时候,即打包了库的头文件(库的使用说明书)也打包了一份库文件(库的实现)。
头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。
C或C++程序从源文件到生成可执行文件需经历 4 个阶段
分别为预处理、编译、汇编和链接。
链接阶段所要完成的工作,是将同一项目中各源文件生成的目标文件和程序中用到的库文件整合为一个可执行文件。
虽然库文件明确用于链接,但编译器提供了2种实现链接的方式,分别称为静态链接和动态链接。
采用静态链接方式实现链接操作形成的库文件称为静态链接库;
采用动态链接方式实现链接操作形成的库文件称为动态链接库。
在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示,动态链接库的后缀名通常用 .so 表示;
在 Windows 系统中,静态链接库文件的后缀名为 .lib,动态链接库的后缀名为 .dll。
静态库的扩展名:libxxx.a
动态库的扩展名:libxxx.so
其中的xxx表示库的名称。
下面我们来观察一下这两种方式生成的可执行文件的大小,并使用file命令查看俩种文件的属性:
通过ldd查看可以执行程序依懒的库(注意动态链接生成的可执行程序才有依赖库,静态链接生成的可执行程序是没有依赖库的,静态链接会将库文件的代码拷贝一份到可执行文件中。
制作步骤
首先为了掩饰打包静态库的过程,我创建了几个文件:
ar 工具对形成的.o文件进行打包(打包的时侯带上 -rc 选项)其中 r 和c分别是replace和creat。在这里我们将库名设置为 mylib。
这样就形成了一个静态库:libcal.a
使用静态库,需要包含头文件;如果我们要给别人使用我们写的静态库,就需要将库文件和头文件一起打包:
利用make指令一键打包和make output发布:
将头文件放在mathlib/include中,库文件放在mathlib/lib中
- include文件夹:存放头文件,提供给用户调用的接口API
- lib文件夹:存放库文件,即:生成的静态库、动态库
- src文件夹:存放源文件
执行make和make output:
现在我们以及打包成功了.现在我们就可以交给别人使用了。
下面我们将这个打包好的静态库放到一个目录下进行测试:
对应测试test.c内容:
对应makefile:
执行结果:
我们发现出错了。我们在使用gcc采用静态链接时,我们需要告诉编译器库文件所在路径,头文件所在路径,和你要链接那个库而这三个操作所对应的选项分别为:
参数说明:
修改Makefile:
执行结果:
制作步骤:
注意:静态库生成的.o文件是和位置有关的用gcc生成和位置无关的.o文件,需要使用参数-fPIC(常用) 或 -fpic
在了解什么叫生成和位置无关的.o文件,我们就要联系之前学过的虚拟地址空间:
linux上打开一个运行的程序(进程),操作系统就会为其分配一个(针对32位操作系统)0-4G的地址空间(虚拟地址空间),虚拟地址空间不是在内存中。静态库生成与位置有关的二进制文件(.o文件)虚拟地址空间是从0开始的,生成的二进制文件(.o文件)会被放到代码段即.text代码区。生成的.o代码每次都被放到同一个位置,是因为使用的是绝对地址。动态库生成与位置无关的二进制文件(.o文件)动态库 / 共享库 在程序打包的时候并不会把.o文件打包到可执行文件中,只是做了一个记录,当程序运行之后才去把动态库加载到程序中,也就是加载到共享库空间,但是每次加载到共享库空间的位置可能不同。
参数说明:
把mathlib放在另一个目录下进行测试:
发现在make的时候没有问题,但是在执行可执行程序的时候出现了找不到动态库的问题。这是因为我们使用makefile编译的时候只告知编译器头文件库路径在哪里,当程序编译好的时候,此时已经和编译无关了!加载器在运行的时候,需要进一步告知OS系统,库在哪里。
解决方法:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。