当前位置: 首页 > news >正文

免费学平面设计的网站一站式网站手机端怎么做

免费学平面设计的网站,一站式网站手机端怎么做,企业公示信息查询系统陕西,陕西有色建设有限公司网站文章目录 前言一、基础IO1、文件预备知识1.1 文件类的系统调用接口1.2 复习c语言接口 2、文件类的系统调用接口2.1 open系统调用2.2 close系统调用2.3 write系统调用2.4 read系统调用 3、文件描述符3.1 文件描述符fd介绍3.2 文件描述符fd分配规则与重定向3.3 重定向原理3.4输入… 文章目录 前言一、基础IO1、文件预备知识1.1 文件类的系统调用接口1.2 复习c语言接口 2、文件类的系统调用接口2.1 open系统调用2.2 close系统调用2.3 write系统调用2.4 read系统调用 3、文件描述符3.1 文件描述符fd介绍3.2 文件描述符fd分配规则与重定向3.3 重定向原理3.4输入重定向和追加重定向 4、重定向系统调用dup25、一切皆文件6、缓冲区6.1 缓冲区6.2 问题解决 7、练习7.1 自己设计用户层缓冲区7.2 myshell中加入重定向功能 8、stdout和stderr8.1 错误重定向8.2 模拟实现perror 9、磁盘文件9.1 磁盘介绍9.2 inode和文件名 10、软硬链接10.1 软链接10.2 硬链接 11、动静态库11.1 制作静态库11.2 制作动态库 前言 一、基础IO 1、文件预备知识 经过前面的学习我们知道了文件 文件内容 属性(也是数据)所以当我们创建了一个文件并没有给里面写内容时此时该文件也占有了空间因为文件的属性也需要空间来存储。所以我们对文件的所有操作就是(1). 对内容。 (2). 对属性。 1.1 文件类的系统调用接口 我们知道文件在磁盘(硬件)上放着当我们想要通过代码来访问文件时的步骤为写代码 - 编译 - 生成可执行程序.exe - 运行 - 访问文件并且我们的程序访问文件的本质其实是该进程在访问文件。 磁盘是一个硬件只有操作系统可以对其进行写入所以如果我们想直接使用代码来对文件进行写入是不可能的此时我们就需要使用操作系统提供的接口操作系统提供的这些关于文件读写的接口就是文件类的系统调用接口。 但是操作系统提供的文件类的系统调用接口使用起来比较难所以每种语言都对这些操作系统提供的接口做了封装以便这些接口可以被用户更好的使用但是这也导致了不同的语言有不同的语言级别的文件访问接口(都不一样)但是这些语言封装的其实是同一套操作系统提供的文件类的系统调用接口只不过每个语言都规范了自己所写的封装函数这使得每个语言的封装函数都不相同。例如Linux系统中提供的文件类的系统调用接口就只有一套而这些语言都对这一套操作系统层面的文件接口进行了封装。 如果语言不提供对文件的系统接口的封装那么所有的访问文件操作都必须直接使用操作系统提供的文件类的系统调用接口而使用这些语言的用户需要访问文件时也使用这些操作系统提供的文件类的系统调用接口。但是如果用户使用了Linux操作系统提供的系统接口来编写访问文件的代码那么这个程序就无法在windows等其它平台中直接运行了因为Linux操作系统和windows操作系统提供的文件类系统调用接口不一样所以这个程序就不具备跨平台性了。而当语言把所有的平台的代码都实现一遍然后使用条件编译当该程序在Linux系统中就使用Linux操作系统提供的系统调用接口当该程序在windows系统中就使用windows操作系统提供的系统调用接口。 我们知道磁盘是硬件而文件都存在磁盘中所以向文件中写数据其实就是向磁盘中写数据那么显示器也是硬件printf向显示器打印其实也是向硬件中写入数据。 在Linux中一切皆文件。 1.2 复习c语言接口 我们在前面学过c语言中使用fopen函数来打开文件并进行访问。可以看到fopen函数的第一个参数为要打开的文件的路径第二个参数为打开该文件的模式其中r为只读模式w为只写模式r为读写模式w为读写模式r和w的区别就是使用r模式时当打开的文件不存在时会报错而使用w模式时当打开的文件不存在时则会自动创建。使用w模式为覆盖式写入而使用a模式为追加写入a模式为读写模式并且写入为追加写入。 当前路径的概念 当我们在调用fopen函数时第一个参数没有写绝对路径时那么这个文件会被创建在该程序所在的目录下。 我们执行该程序后然后在 /proc/目录下查看该进程的相关文件我们可以看到该进程当前的目录。其实当一个进程运行起来的时候每个进程都会记录自己当前所处的工作路径这就是当前路径。 我们在使用fwrite存数据时需要传入写入的字符串的长度我们知道c语言中字符串后面都有一个\0字符串结束符但是我们将字符串存入文件中时是不会存这个\0字符串结束符的因为\0是c语言的规定而文件只保存有效数据所以文件中不会存\0字符。 我们看到上面的代码中使用w模式打开文件然后向log.txt文件中写入了一些数据。我们将代码中文件写入的操作都注释掉然后重新编译并执行该程序该程序就只是将log.txt文件打开再关闭但是log.txt文件中原来的内容被清空。由此可以推断出w模式在向文件中写内容之前会先将文件中的内容清空即在打开文件后马上先将文件内容清空。 并且我们看到输出重定向和w模式的写入类似打开log.txt文件时会先将log.txt中的内容清除。 我们修改代码将log.txt以a追加模式打开此时我们看到当向log.txt文件中写入内容时不会将原来的内容删除了而是在文件的最后追加新的内容。 下面我们再来复习读取文件的接口。可以看到log.txt文件中的内容被读取出来并打印到屏幕上了。 然后我们稍微将代码修改一下将读取的文件路径改为从命令行获取这时这个程序就实现了类似于cat命令的功能了。 我们知道c语言程序中会默认打开三个标准输入输出流 (1). stdin。 (2). stdout。 (3). stderr。 即相当于c语言程序会默认执行下面的语句。 2、文件类的系统调用接口 c语言中操作文件的库函数在底层其实都是调用了文件类的系统调用接口。 2.1 open系统调用 可以看到open系统调用的第一个参数是要打开的文件的路径第二个参数flags是一个标志位用来表示open的一些选项这些大写的选项都是宏定义 那么为什么open系统调用的第二个参数要设置为这样呢 下面我们来看一个案例如何给函数传递标志位。当我们调用一个函数需要传入多种选项时如果我们将每一个选项都定义为形参当调用函数时将这些形参都一一传值那么这样调用函数就会很麻烦所以我们一般使用一个int类型的形参因为一个int类型数据有32位每一位都可以标识一种状态。 可以看到下面的代码中当我们调用show函数时传入ONE则就会执行ONE相关的语句当传入ONE | TWO则就会执行ONE相关的语句和TWO相关的语句。 而open系统调用的第二个参数其实就是采用的这种方法我们可以在下面的文件中查看到这些宏定义。当我们调用open系统调用时需要open执行什么样的功能就可以将对应的宏加进去然后open函数中就会执行这些功能。 下面我们使用open系统调用。 我们使用open系统调用打开log.txt文件然后我们给第二个参数传入O_WRONLY即只写的选项然后我们当前目录下是没有log.txt文件的我们运行程序后发现打开文件失败因为没有这个文件。这是因为当只有O_WRONLY选项时只会打开文件进行写入当没有这个文件时并不会自动创建文件。只有再加上O_CREAT才会检测如果没有该文件就先创建文件。 当加上了O_CREAT选项后我们发现log.txt文件被创建了出来。并且我们看到此时打印出来了open的返回值fd为3。 我们发现上面的log.txt文件被创建出来后该文件的权限是随机的。如果我们想要设置新建文件的权限此时我们就需要用到open系统调用的第三个参数来进行设置。我们向open的第三个参数中传入新建文件的权限。 但是我们发现新建的log.txt文件的权限并不是我们设置的权限这是因为有umask码而导致的。 如果我们不想要被系统的umask码改变创建文件的权限可以使用下面的系统调用将该进程的umask码设置为0。下面的open创建文件时采用就近原则使用umask所以会将umask当作为0。此时我们看到新建的log.txt文件的权限就和我们设置的权限一致了。 当我们已经知道了文件存在后就不需要传入第三个参数了因为此时传入第三个参数文件的权限后已经存在的log.txt文件的权限也不会改变。 2.2 close系统调用 close系统调用很简单只需要向close中传入调用open打开文件时的返回值fd即可关闭该文件。 2.3 write系统调用 可以看到write系统调用中也需要传入调用open打开文件时的返回值fd然后需要传入要写入的数据的地址然后传入要写入的数据的个数而write的返回值ssize_t是成功并且完整的写入到文件中的数据的个数。如果发生错误则write就返回-1。 下面我们调用write系统调用向log.txt文件中写入数据。我们看到数据成功写入到了log.txt文件中。 当我们再次向log.txt文件中写入新的数据时我们发现log.txt文件中原来的数据并没有被清除而新数据也没有追加到原数据后面而是新数据替换了原数据的一部分内容。 如果我们在调用open时再加上O_TRUNC选项后此时就相当于fopen的w模式了。可以看到此时test02程序向log.txt文件中写入数据时都会先将原来的数据先进行清空。 当想要实现追加添加数据时即fopen的a模式就需要将O_TRUNC选项换为O_APPEND选项。此时可以看到test02程序向log.txt文件中写入数据时是追加写入。 2.4 read系统调用 我们看到read系统调用的参数和write的相似read系统调用中也需要传入调用open打开文件时的返回值fd然后需要传入读取的数据要存入的地址然后传入要读取的数据个数而read的返回值ssize_t是成功并且完整的从文件中读取的数据的个数。如果发生错误则read就返回-1。 下面我们来使用read读取数据。 因为需要读取数据所以open的第二个参数需要传入O_RDONLY选项。然后我们创建一个buffer数组用来存读取到的数据因为read只会读取数据并不会自动添加\0所以我们需要手动添加\0即先将buffer中都初始化为\0。我们看到read成功读取到log.txt文件中的数据了并且将这些数据打印了出来。 3、文件描述符 3.1 文件描述符fd介绍 通过上面对文件类系统调用接口的使用我们发现了这些接口都离不开一个变量即open系统调用的返回值fd那么这个fd是什么呢通过上面的使用我们可以知道close、write、read接口就是靠这个fd来找到要操作的文件其实这个fd就是文件描述符。 下面的代码中我们打开了4个文件然后分别打印open的返回值然后发现这些文件的文件描述符从3开始那么012.去哪里了呢还记得我们前面提到的c语言程序中会默认打开三个文件流吗其实这三个文件流的fd就是012。 下面我们证明文件描述符1就是标准输出流。 我们调用fprintf函数向stdout标准输出流中写入数据然后数据会显示到屏幕上。然后我们调用write向文件描述符为1的文件中写入数据发现数据也会显示到屏幕上所以文件描述符1对应的就是标准输出流。 然后我们证明文件描述符0就是标准输入流。 我们调用read系统调用从文件描述符为0的文件流中读取数据然后再打印出来。当我们执行test05可执行程序后我们在键盘上输入内容然后按回车后发现输入的内容显示到了屏幕上这说明文件描述符0对应的就是标准输入流。 我们知道stdin、stdout、stderr都是c语言默认创建的三个FILE*类型的变量那么FILE是一个什么类型呢 FILE其实是一个c标准库提供的结构体即在c语言中每一个打开的文件都会对应一个FILE结构体这个结构体里面存储了关于打开的文件的相关信息。我们知道c语言中提供的fopen、fwrite、fread、fclose等函数的底层其实都调用了open、write、read、close系统调用接口而调用这些系统调用都需要传入文件描述符所以我们可以推测出FILE结构体里面必定封装了fd因为文件类系统调用接口只认fd而不认c语言的FILE结构体。 下面我们证明FILE结构体里面封装了fd。 我们知道stdin、stdout、stderr就是三个FILE类型的指针所以我们可以通过stdin-变量的方式来访问stdin指向的FILE结构体里面的变量。 我们使用下面的代码打印stdin、stdout、stderr指针指向的FILE结构体里面的_fileno变量的值可以看到stdin、stdout、stderr指针指向的FILE结构体里面的_fileno变量的值就是stdin、stdout、stderr这三个文件流的文件描述符。所以在调用系统调用时其实是将这三个文件流的fd传入到了系统调用中。 经过上面的验证我们知道了c语言的FILE结构体中封装了文件描述符fd那么这个文件描述符fd到底是什么呢 我们知道进程要访问文件必须先打开文件并且一个进程可能需要打开多个文件所以一般而言 进程 : 打开的文件 1 : n。文件可以被进程访问的前提是该文件已经加载到了内存中然后才能被进程直接访问。那么如果有多个进程都需要访问文件时此时内存中就会加载了大量被打开的文件而这些文件都需要通过操作系统提供的系统调用接口来进行操作所以操作系统需要管理这些被打开的文件。我们知道操作系统对进程的管理通过task_struct操作系统对进程地址空间的管理通过mm_struct操作系统对资源的管理都是采用先描述再组织的方式管理资源所以操作系统对文件的管理也构建了一个struct file结构体用来管理内存中被打开的文件即内存中每一个被打开的文件操作系统都会创建一个struct file结构体对象来记录这个文件的所有内容(不仅仅包含属性内容)。然后操作系统将每个被打开文件对应的struct file结构体对象使用双链表组织起来然后又使用一个strcut file array[32]的指针数组来存储这些被打开文件对应的struct file结构体对象的地址。而fd就是每个被打开文件对应的strcut file结构体对象的地址在strcut file* array[32]数组中的下标。所以fd在内核中本质就是一个数组下标。一个进程的task_struct中有一个指针指向了strcut file* array[32]这个指针数组这就使这些被打开的文件与进程建立了对应的关系。 我们知道文件 内容 属性。 通常我们称 (1). 被进程打开的文件(内存文件)。 (2). 没有被进程打开的文件即在磁盘上的文件(磁盘文件)。 下面我们看Linux内核的源码来看源码中的struct file和task_struct是什么关系。 我们查看内核源码中描述进程的task_struct结构体中有一个struct files_struct files类型的指针变量。 然后我们查看struct files_struct结构体中可以看到struct files_struct结构体中有一个struct file * fd_array[NR_OPEN_DEFAULT]指针数组这个指针数组里面存储的都是struct file * 类型的指针。 然后我们查看struct file结构体可以看到struct file结构体中存的就是内存文件相关的信息。当进程打开文件时即当一个文件被加载到内存中时操作系统就会为这个内存文件创建一个struct file结构体对象。 通过上面查看源码我们可以得出这些结构体之间的关系如下图所示。 每当创建一个进程都会创建相应的task_struct在task_struct中会有一个mm_struct类型的指针指向该进程的地址空间也会有一个 files_struct * 类型的指针指向该进程的文件结构体在该文件结构体中会有一个指针数组。当从磁盘中打开一个文件时此时该文件就变为内存文件操作系统就会为该文件创建一个struct file来记录该文件的属性。(类似于一个进程加载到内存时操作系统创建一个对应的task_struct来管理该进程一样)操作系统为每个内存文件创建一个struct file以便来管理打开的内存文件。然后操作系统将这个打开的文件的file_struct的地址填入到文件结构体的指针数组中这个文件的文件描述符就是该内存文件的struct file在数组中的下标。当该进程中进行read、write文件操作时需要传入文件描述符通过这个文件描述符在该进程的文件结构体中的数组找到中找到要修改的内存文件的 struct file然后修改该内存文件的内容以此来进行文件的修改。所以本质上文件描述符就是数组下标。 文件描述符就是从0开始的小整数。当我们打开文件时操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组每个元素都是一个指向打开文件的指针所以本质上文件描述符就是该数组的下标。所以只要拿着文件描述符就可以找到对应的文件 下面为在c语言中调用文件类的函数时操作系统底层所做的处理。 3.2 文件描述符fd分配规则与重定向 我们看到每次新打开的文件的文件描述符fd的值都为3这是因为c语言中默认会打开stdin、stdout、stderr三个文件流这三个内存文件的文件描述符分别为0、1、2。所以每次新打开的文件描述符的值都为3。 下面的代码中我们在刚开始就使用close将文件描述符为0的内存文件关闭此时可以看到新打开的内存文件的文件描述符就为0了。 下面的代码中我们在刚开始使用close将文件描述符为2的内存文件关闭此时可以看到新打开的内存文件的文件描述符的为2了。综上我们可以得出fd的分配规则为找到最小的没有被占用的文件描述符。 下面的代码中我们在刚开始使用close将文件描述符为1的内存文件关闭此时发现该程序就不会打印内容到显示器上了并且在log.txt文件中我们也没有看到打印的内容。 我们将clost(fd)注释掉然后我们发现虽然程序同样没有在屏幕上打印内容但是log.txt文件中有了要打印的内容了。 然后我们将close(fd)不注释掉了但是在close(fd)前面加一个fflush(stdout)语句然后我们会发现log.txt文件中也有了要打印的内容。 下面我们多调用几个接口向stdout中写入数据。然后我们同样发现显示器上没有内容打印而log.txt文件中被写入了本该打印到显示器上的数据。这几个案例中本来数据都是向stdout(标准输出)即显示器中写入的但是我们看到这些数据都被写入到了log.txt文件中其实这就是输出重定向即本该输出到显示器中的数据重定向输出到了log.txt文件中。 3.3 重定向原理 我们知道在c语言程序中默认会打开三个文件即创建三个FILT* 类型的变量分别为stdin、stdout、stderr这三个FILE* 类型的指针变量指向的的FILE结构体中有一个_fileno变量该变量记录了c语言中内存文件的文件描述符这三个内存文件的文件描述符分别为0、1、2。 例如stdout - FILE* - FILE - _fileno(1)所以当调用fprintf(stdout,…)函数传入stdout时其实fprintf函数在底层调用write系统调用并且传入的文件描述符为stdout指向的FILE结构体里面的_fileno变量的值即向write系统调用中传入的文件描述符为1。此时write系统调用操作的就是文件描述符为1的内存文件即修改的就是fd_array指针数组中下标为1的元素指向struct file结构体对象中的数据。而因为我们在代码的起始时就将文件描述符为1的内存文件关闭了所以当我们调用open(“log.txt”, O_WRONLY | O_CREAT | O_TRUNC, 0666);打开log.txt文件时此时操作系统为log.txt内存文件分配的文件描述符为1所以下面的代码中我们调用fprintf(stdout,…)函数其底层都是对文件描述符为1的内存文件进行了操作而此时文件描述符为1的内存文件是打开的log.txt文件所以使用fprintf(stdout,…)就将数据写入到了log.txt文件中。 我们看下面的图知道正常的代码中stdout的文件描述符为1。然后fprintf(stdout,…)函数会将数据写入到显示器上。 但是当我们刚开始就将文件描述符为1的内存文件关闭了。然后使用open系统调用打开log.txt文件此时log.txt文件的文件描述符fd就是1所以fprintf(stdout,…)函数将数据写到文件描述符为1的文件中其实就是将数据写入到了log.txt文件中了。 3.4输入重定向和追加重定向 下面的图片中代码都忘了在最后关闭被打开的文件log.txt了都应该加上一句close(fd);。 输入重定向 下面的代码中我们可以看到使用fgets函数从stdin文件中读取数据即从键盘中读取数据然后打印到显示器上。 然后我们调用open系统调用将log.txt文件打开此时可以看到打开的log.txt文件的文件描述符fd为3。然后我们在键盘中输入的数据也打印在了显示器上。 下面我们在代码的开始使用close将文件描述符为0的内存文件关闭此时可以看到打开的log.txt文件的文件描述符fd就是0了然后我们使用fgets函数从键盘中读取数据变成了从log.txt文件中读取数据了这是因为发生了输入重定向。 追加重定向 下面的代码为输出重定向将本该输出到显示器的数据输出到了log.txt文件中我们发现每次输出重定向向log.txt文件中写入数据时都会先将log.txt文件中的数据清空。 我们将open的O_TRUNC选项换为O_APPEND选项此时每次向log.txt文件中写入数据时就不会将原来的数据先清空了这就是追加重定向。 4、重定向系统调用dup2 上面的代码实现重定向之前需要先使用close关闭文件然后再使用open打开一个文件使该文件使用刚才关闭文件的文件描述符这样的操作感觉很麻烦。其实操作系统还提供了关于重定向的系统调用接口dup、dup2、dup3接口。下面我们来学习dup2系统调用接口。 我们可以看到dup2有两个参数一个为oldfd一个为newfd。如果我们想让本来应该输出到文件描述符为1的显示器文件的数据输出重定向到文件描述符为3的log.txt文件中。我们应该这样使用dup2(3,1)。即表示将fd_array数组中下标为3的元素的内容拷贝到下标为1的元素中那么此时文件描述符为1的文件也是log.txt文件了。 下面为没有调用dup2系统调用时每个内存文件和对应的文件描述符的情况。 当调用dup2系统调用后将fd_array数组中下标为3的元素的内容拷贝到下标为1的元素中那么此时文件描述符为1的文件也是log.txt文件了。 下面我们来使用dup2实现输出重定向。 下面的代码中我们将每次执行该程序的命令行参数给打印了出来此时我们没有调用dup2可以看到数据都输出到了显示器上。 下面我们调用dup2即将文件描述符为1的内容拷贝到文件描述符为3的内容中。此时我们可以看到本来应该输出到显示器的数据发生了输出重定向输出到了log.txt文件中。并且输出重定向在写数据之前会将log.txt文件中的旧数据先清空。 使用dup2实现追加重定向。 我们将代码中调用open时选项O_TRUNC改为O_APPEND时。此时每次向log.txt文件中写入数据时就不会将原来的数据清空了这就是追加重定向。 5、一切皆文件 我们知道在c语言中使用struct来实现面向对象的过程即struct就类似于class但是class中有成员属性和成员方法而struct中不能定义函数但是struct中可以使用函数指针来指向定义好的函数。Linux就是使用c语言写的在Linux的内核中struct file结构体的定义就类似于下面这样定义的struct file结构体中不仅记录了每个内存文件的相关信息还使用函数指针指向了这些内存文件对应的方法。 经过了上面对文件的学习我们知道了像磁盘、显示器、键盘、网卡等硬件设备的读写操作其实就类似于对文件进行读写操作只不过这些底层不同的硬件一定对应不同的操作方法即不同的读写方法。这些外设的核心访问函数其实都是read、write等IO函数并且因为这些硬件的功能不同所以这些设备都有自己的read和write方法并且每个设备之间的read和write方法的代码实现一定不同因为这些外设都有着不一样的功能。 那么这些硬件的读写方法都不一样Linux操作系统是怎么为它们创建struct file结构体对象的呢 Linux内核中的struct file结构体中有对应的函数指针当给不同的硬件创建struct file结构体对象时就将这些函数指针指向这个硬件自己的对应函数。 这些硬件的核心函数就是read和write而Linux中对普通文件进行的操作也是read和write操作。这样看来普通文件和硬件的操作都是read和write操作不同的是每个硬件的read和write函数的实现都不相同而普通文件的read和write函数的实现相同Linux中想要将管理硬件也和管理普通文件一样但是硬件的read和write函数和普通文件的read和write函数不一样。所以Linux在struct file结构体中创建了readp和writep函数指针。当创建的为操作硬件的struct file结构体对象时该结构体对象的readp和writep函数指针就指向该硬件自己的read和write函数而当创建的为操作普通文件的struct file结构体对象时该结构体对象的readp和writep函数指针就指向普通文件的read和write函数。这样对于上层来说操作硬件和操作普通文件都是对struct file结构体对象里面的内容进行修改即在上层看来操作硬件就好像也是和操作普通文件一样了这就实现了Linux中一切皆文件。 6、缓冲区 6.1 缓冲区 缓冲区其实就是一段内存空间这个空间由c标准库提供并维护。 我们知道当一个进程等待IO设备时会进入阻塞状态。在我们写的c语言程序中如果要将数据打印到显示器上时其实就是一次IO操作而如果我们每打印一个字符就调用一次write系统调用将这个数据写到显示器中然后等待显示器的回应这样就需要频繁的调用write系统调用来向显示器中写入数据因为显示器为外设所以每一次向显示器中写入数据都是很慢的而且等待显示器的回应也是很慢的会影响用户交互这种为写透模式(WT即数据写到目标文件后才向下执行命令)。所以c标准库中提供了缓冲区的概念即进程中要放到显示器中打印的数据先放到缓冲区中然后缓冲区马上给进程回应告诉进程这个数据已经写到显示器中了其实这个数据现在还在缓冲区中存放但是对进程来说这个数据已经到显示器中了所以进程就可以继续执行下面的命令了这种为写回模式(WB即不管数据有没有写到目标文件中只要数据写到缓冲区中就认为已经写到了目标文件中)所以缓冲区的存在可以提高整机效率并且提高用户的响应速度。缓冲区中会有刷新策略例如立即刷新(缓冲区有数据就马上调用write接口将数据写到目标文件中)、行刷新(遇到\n才会调用write接口将缓冲区的数据写到目标文件中)、满刷新(当缓冲区的数据存满后才调用write接口将数据写到目标文件中)。除了这三个常规的刷新策略还有特殊情况时的刷新策略例如用户使用fflush强制刷新缓冲区数据、进程退出时会刷新缓冲区数据。所以缓冲策略 一般 特殊。 我们看下面的代码中先使用了c语言提供的关于文件操作的函数向显示器中写入数据然后又使用操作系统提供的系统调用向显示器中写入数据。并且我们在最后调用了fork系统调用创建了一个子进程。 当我们执行test05程序时可以看到显示器中打印了这些数据。然后我们test05程序输出的数据输出重定向到log.txt文件中我们cat查看log.txt文件的内容时发现使用c语言函数向显示器写入的数据被写到log.txt文件中两次而使用write系统调用向显示器中写入的数据只在log.txt文件中写入一次。 当我们将代码最后的使用fork创建子进程的语句注释掉时再次运行test05程序我们看到显示器中只打印了一遍数据当我们将test05输出的数据输出重定向到log.txt文件中后我们看到此时log.txt文件中这些数据也只被写入了一次。 我们将上面的两个情况进行对比为什么会发生这种现象呢 我们需要先知道所有的外部设备永远都倾向于全缓冲刷新(满刷新)因为进程和外部设备进行IO操作时数据量的大小不是主要矛盾进程和外部设备预备IO的过程是最耗时间的所以全缓冲可以更少次的外设的访问以此来提高效率所以磁盘文件都会使用全缓冲刷新策略。 虽然所有的外部设备永远都倾向于全缓冲刷新但是刷新策略还需要结合具体的情况而进行更改。例如显示器这个外部设备它是直接给用户看的所以它不止要照顾效率还要考虑用户的响应时间所以显示器的刷新策略是行刷新。 c语言程序中使用c语言提供的关于文件操作的函数时这些数据都会存放在c标准库维护的缓冲区中。 下面我们就来解释上面的现象我们知道当代码执行到fork时上面的printf、write等函数都已经执行完了但是并不代表这些数据现在已经都被写入到了目标文件中。 c语言中如果是将这些数据写到显示器中因为显示器的刷新策略是行刷新所以当这些数据在缓冲区中时如果遇到了\n就会将\n之前的数据写入到显示器文件中。所以此时当代码执行到fork的时候函数执行完了并且数据已经被刷新了此时缓冲区中是没有数据的此时执行fork创建子进程子进程在写时拷贝时拷贝父进程的缓冲区数据时并没有拷贝到数据因为此时缓冲区数据已经刷新出去了所以test05程序在向显示器中写入数据时只写入了一次。 而当程序重定向向磁盘中的文件中写入数据时此时刷新策略变为了全缓冲所以在fork的时候函数执行完了但是缓冲区中的数据还没有被刷新此时这些数据就在缓冲区中而fork创建的子进程在写时拷贝时也会将缓冲区的数据进行拷贝。所以就可以看到调用c语言函数写的数据被写入了两次因为这些数据都存在缓冲区中而调用系统调用写入的数据只写入了一次因为这些数据直接写到了内核缓冲区中。所以log.txt文件中父子进程都将缓冲区的数据写入到了log.txt文件中。 如果我们在fork之前调用fflush函数将缓冲区中的数据都刷新后那么当fork创建子进程后子进程写时拷贝时拷贝父进程的缓冲区数据就为空此时再向log.txt文件中输出重定向数据时就只有父进程的数据写入到了log.txt文件中而子进程的缓冲区为空所以没有数据写入到log.txt文件中。 在前面我们说过c语言程序中默认打开三个文件流即stdin、stdout、stderr我们可以在/usr/include/stdio.h头文件中看到这三个文件流的定义。 我们在前面说过c语言中的FILE结构体中封装了文件描述符fd其实FILE结构体中不只是封装了文件描述符fd还包含了该文件fd对应的c标准库中的缓冲区结构。我们可以在/usr/include/libio.h文件中看到FILE结构体中包含了缓冲区的开始地址和结束地址。 6.2 问题解决 下面我们来看一下以前提出的一个问题。 我们在代码刚开始将文件描述符为1的文件关闭然后打开log.txt时此时log.txt为1printf和fprintf和write不再是向显示器中写数据而是向普通文件log.txt中写数据。我们在代码最后使用fork创建一个子进程并且将close(fd)注释掉然后我们发现在将test01的数据输出重定向到log.txt文件中时发现c语言函数输出的数据被写入了两次系统调用函数的输出被写入了一次这是因为c语言函数的输出数据都先存放在c标准库的缓冲区中而系统调用的输出数据存放在内核缓冲区中因为此时是向普通文件写数据所以遇到\n时不再会刷新而是当缓冲区满时才刷新。fork创建子进程后子进程的缓冲区中也有父进程的c标准库的缓冲区的数据。然后在进程结束后将强制将缓冲区的数据进行刷新所以log.txt中的数据才是这样。 所以我们需要使用fflush来手动刷新。而当不使用ffluse手动刷新缓冲区的数据时 如果我们将close(fd)不进行注释此时将test01输出的数据重定向到log.txt文件中后我们发现log.txt文件中只有系统调用write的数据这是因为当创建子进程后虽然父子进程的缓冲区中都有数据但是当执行close(fd)后此时数据还在缓冲区中但是对应的文件已经关闭了所以当进程关闭时进行缓冲区数据强制刷新时没有刷新出来数据因为文件已经关闭了缓冲区的数据已经清空了。 如果我们在关闭文件之前使用fflush函数刷新缓冲区数据此时看到数据又正常写入到log.txt文件中了。 7、练习 7.1 自己设计用户层缓冲区 通过上面的学习我们知道了c标准库中提供了一个缓冲区并且缓冲区的一些信息就包含在FILE结构体中下面我们自己也模拟实现一下缓冲区。 我们创建一个MyFILE结构体里面封装了文件描述符fd然后创建了一个长度为1024的字符串数组buffer这个buffer就是我们模拟实现的缓冲区然后我们定义一个end来表示这个数组的有效数据个数。这样当我们想要打开文件写数据或者关闭文件时就调用我们自己写的接口对我们自己定义的MyFILE结构体进行操作。 下面我们就来一一实现这些文件操作函数首先我们实现fopen_函数该函数的第一个形参为要打开文件的路径第二个形参为要以什么模式打开这个文件。我们先创建一个MyFILE* 类型的结构体指针并且置为NULL然后判断该函数的第二个参数为以什么模式打开这个文件然后我们调用open系统调用传入不同的选项来实现不同模式打开文件的效果如果open系统调用打开文件成功那么就使用malloc申请一片空间创建一个struct MyFILE结构体对象然后将fp指向这片空间并且将这个struct MyFILE结构体对象的数据都进行修改。最后将结构体指针fp返回。 然后我们实现fputs_函数该函数第一个参数为要写入的数据第二个参数为要操作的MyFILE结构体对象的指针。我们将fputs_并不会直接将fputs_要写入的数据写到目标文件中而是将数据存到我们的buffer数组中即缓冲区中。 然后我们再实现fflush_函数该函数可以刷新缓冲区数据。当我们调用write系统调用时其实数据并没有被直接写到磁盘中而是写到了内核缓冲区中而syncfs系统调用会直接将数据写到磁盘中。 然后我们使用下面的代码进行测试我们在fputs_函数中打印buffer数组中的内容即缓冲区中的内容。然后我们可以看到buffer数组里面的内容是调用fputs_函数每次写入的内容这说明我们向缓冲区中写入数据成功。并且因为在fclose_函数中我们调用了fflush_函数所以最后buffer数组中的内容会被写到log.txt文件中。 下面我们就可以继续写fputs_函数并且在该函数内实现一些刷新策略例如当遇到’\n’时就调用write系统调用写数据或者当buffer数组全满时才调用write系统调用写数据。下面实现的是行刷新策略即当向文件描述符为1的文件中写数据时采用行刷新策略。我们还可以在文件描述符为其它值时写不同的刷新策略。 然后我们在测试代码开始将文件标识符为1的文件关闭然后再打开log.txt文件此时log.txt的文件标识符就为1然后我们在前两个要写入的数据中加入\n则在fputs_函数中就会先将第一个数据写入到log.txt文件中然后将第二个数据写入到log.txt文件中最后将第三个和第四个数据一起写到log.txt文件中。因为此时stdout已经被我们关闭所以我们将测试的内容显示到stderr中来验证。我们看到buffer数组里面的数据遇到\n时就会刷新了。 下面我们再演示使用fork创建子进程后log.txt文件中会有两份数据写入的情况。 当重定向写入到lot.txt文件中时没有带\n时在fork时虽然代码执行完了但是缓冲区buffer里面的数据还没有写入到文件中所以子进程写时拷贝父进程数据时会将buffer也拷贝一份。然后我们的fclose函数中都会调用fflush函数将数据刷新即将数据写入到文件中。所以父子进程都会将数据写入到文件中故log.txt中有两个数据。 7.2 myshell中加入重定向功能 我们之前写的myshell脚本并没有重定向功能下面我们将myshell脚本加上重定向功能。 首先我们需要得到命令行命令cmd_line然后分析这个命令中是否有重定向。 我们使用CheckRedir函数来检测cmd_line中是否有重定向命令如果有重定向命令CheckRedir函数中就将重定向的文件名返回如果没有重定向命令则CheckRedir命名就返回NULL。 因为CheckRedir函数中检查是否有重定向命令有四种结果即输出重定向、输入重定向、追加重定向、无重定向。所以我们设置四个宏来表示这四个重定向然后设置一个全局变量来记录该重定向的状态。然后我们实现CheckRedir函数在该函数中我们将end指针指向cmd_line命令行字符串的最后一个字符然后向前遍历cmd_line如果遇到了重定向就接着判断并且将重定向命令的位置变为字符串结束符’\0’这样重定向命令前面的命令和后面的命令就分开了然后将重定向命令后面的命令字符串返回。如果没有检测到重定向命令就返回NULL。 然后我们在子进程中判断命令中是否有重定向命令如果有重定向命令就先执行重定向命令并且我们根据redir_status的不同状态来决定open系统调用中以什么选项打开文件。 我们可以看到输出重定向中每一次写入数据前都会先将原来的数据清空。 我们看到追加重定向不会将文件中原来的数据清空而是在文件末尾追加写入数据。 我们看到输入重定向功能也可以正常使用。 8、stdout和stderr 8.1 错误重定向 我们知道stdout和stderr打开的都是显示器即都是向显示器中打印数据。下面我们分别向stdout和stderr中写入数据然后我们看到向文件描述符为1的stdout文件和文件描述符为2的stderr文件中写入的数据都在显示器中打印了出来但是当进行输出重定向时只有文件描述符为2的stderr的数据被重定向输出到了log.txt文件中。 文件描述符1 和 2 对应的都是显示器文件但是它们两个是不同的可以认为是同一个显示器文件被打开了两次。 使用下面的命令可以将错误信息和输出信息分别写入到不同的文件中。其中./test02 log.txt 2 err.txt表示在将test02的数据输出重定向到log.txt文件之前先将test02的数据中本该写入到文件描述符为2的文件中的数据输出重定向到err.txt文件中然后再进行test02 lot.txt所以此时会将本该写入到文件描述符为1的文件中的数据输出重定向到log.txt文件中。 ./test02 log.txt 2 err.txt如果我们想让test02的错误信息和输出信息都写入到log.txt文件中我们可以使用下面的命令。 ./test02 log.txt 21命令表示先将文件描述符为1的文件输出重定向到log.txt文件中然后将文件描述符为1的内容拷贝给文件描述符为2的内容此时文件描述符为1和2的文件都输出到log.txt文件中内容。 ./test02 log.txt 21当我们想要将一个文件的内容拷贝到另一个文件时可以使用下面的命令。 cat log.txt back.txt 8.2 模拟实现perror 我们在上面的程序中看到perror打印的信息后面多出来了一个Success。我们在c语言中学过一个全局变量errno该变量记录了程序的错误码下面我们修改errno的值。可以看到当errno为不同的值时perror所打印出来的错误信息也不一样。 下面我们自己模拟实现perror函数。我们在学习c语言时学到了一个strerror函数只要向该函数中传入错误码该函数就会返回对应的错误信息我们模拟实现perror函数底层就调用这个函数来实现打印错误信息。 下面的代码中我们打开log.txt文件当没有log.txt文件时就会报错而我们自己写的myperror正确将错误信息打印了出来。 9、磁盘文件 通过前面的学习我们知道了当磁盘中的文件加载到内存中并且被打开时此时该文件变为内存文件。而那些在磁盘中没有被打开的文件叫磁盘文件。那么这些磁盘文件当我们使用时该如何找到呢并且我们怎样查看这个磁盘文件的大小、文件属性等信息呢而且磁盘中有很多磁盘文件操作系统又是怎样存储并且管理这些磁盘文件的当查找一个文件时是如何快速找到指定的文件的。这就需要我们先来了解一个存储数据的硬件——磁盘。 9.1 磁盘介绍 我们的电脑中内存是掉电易失存储介质所以当关闭电脑时内存中的数据都会丢失。 而磁盘是永久性存储介质当关闭电脑时磁盘中的数据也不会丢失并且我们常见的 SSD、U盘、flash卡、光盘、磁带也都是永久性存储介质。 磁盘是一个外设它是计算机中唯一的一个机械设备所以当从磁盘中存取数据时是很慢的远远比不上电脑CPU的速度所以操作系统就需要通过一些方式来将访问磁盘数据的过程加快。 下面就是磁盘的物理结构。 我们可以看到磁盘中后很多盘面每一个盘面被划分为一个个磁道而每一个磁道又被划分成一个个扇区每个扇区就是一个磁盘块各个扇区存放的数据量是相同的一般都是512字节。操作系统从磁盘中取数据时一次取4KB大小的数据即8个扇区的数据。 看到下面磁盘的物理结构后我们会想计算机是如何将数据写到指定的扇区中的呢 我们想要将数据写到指定扇区之前要先找到这个扇区通过上面的介绍我们知道了要确定一个扇区需要先知道它所在的盘面然后知道它所在的磁道然后才能确定这个扇区的具体位置。这就需要用到CHS寻址模式CHS寻址模式将硬盘划分为磁头Heads、柱面(Cylinder)、扇区(Sector)。通过CHS我们就可以找到任意一个扇区那么所有的扇区我们就都可以找到了。 知道了磁盘的物理结构后那么磁盘的逻辑结构是什么样的呢。 我们可以通过另一个永久性存储介质磁带来理解磁盘的逻辑结构。 磁带中也是圆形结构但是当我们将磁带抽出来时看到磁带变为了线性结构由此我们可以分析得出磁盘其实也是一个线性结构的数组。 我们看到磁盘的虚拟结构就变为了一个数组操作系统从磁盘中找指定的扇区就变为了从数组中找扇区的下标。而操作系统管理磁盘就变为了管理数组。 当将数据存储到磁盘中时就是将数据存储到数组中找到磁盘的特定扇区的位置就是找到数组特定的位置对磁盘管理就变为了对数组的管理。 一个磁盘的内存是很大的所以计算机会将磁盘中的内存进行分区这就是分区的过程我们的计算机中C盘、D盘就是磁盘分区而产生的。当磁盘进行分区后在每个分区中都有一块Boot Block的启动块该启动块中写死了磁盘的启动程序等。每一个分区内又会分为多个块组。这样对磁盘的管理就变为了对分区的管理而对分区的管理就变为了对块组的管理。 而每个块组中的结构是下面这样的。 Linux在磁盘上存储文件的时候是将文件的内容和属性分开存储和管理的。 Block Groupext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。 超级块 (Super Block)存放文件系统本身的结构信息。记录的信息主要有block 和 inode的总量、未使用的block和inode的数量、一个block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏可以说整个文件系统结构就被破坏了。 GDT (Group Descriptor Table)块组描述符描述块组属性信息。例如这个块组多大已经使用了多少有多少个inode已经占用了多少等。 块位图Block BitmapBlock Bitmap中记录着Data Blocks中哪个数据块已经被占用哪个数据块没有被占用。假设有10000个blocks10000比特位比特位和特定的block是一一对应的其中比特位为1表示该block被占用否则表示可用。 inode位图inode Bitmapinode Bitmap中记录着哪个inode已经被占用哪个inode没有被占用。假设有10000个inode结点就有10000个比特位比特位和特定的inode是一一对应的。其中bitmap中比特位为1代表该inode已被占用否则表示可用。 inode节点表inode Table:inode是一个大小为128字节的空间保存的是对应文件的属性一个块组内所有文件的inode空间的集合需要标识唯一性所以每一个inode块都有一个inode编号一般而言一个文件一个inode一个inode编号。inode Table存放文件属性 如 文件大小所有者最近修改时间等。 数据区Data blocks存放文件内容多个4KB(扇区*8)大小的集合存储特定文件的内容。 虽然磁盘的基本单位是扇区(512字节)但是操作系统(文件系统)和磁盘进行IO的基本单位是4KB(8*512byte)。这是因为 (1). 如果基本单位太小了操作系统和磁盘就要进行多次IO进而导致效率的降低。 (2). 而如果操作系统使用和磁盘一样的基本单位那么当磁盘的基本单位变化了之后操作系统的源码就需要更改了所以这样规定可以使软件(OS)和硬件进行解耦。 通过上面的这些东西就能够让一个文件的信息可追溯可管理。 我们将块组分割成为上面的内容并且写入相关的管理数据如果每一个块组都这样做那么整个分区就被写入了文件系统信息这就是格式化。 我们知道一个文件对应一个inode属性结点也只对应一个inode编号那么一个文件只能有一个block吗 答案肯定是不一定因为有一些大文件一个block肯定存储不完所以一个文件可能有很多个block并且block中不只是存文件数据也可以存其它块的块号。 9.2 inode和文件名 在Linux中inode属性里面并没有文件名这样的说法但是找到文件需要这个文件的inode编号然后就知道了该文件的分区和块组等信息然后就能找到文件的data block中的内容了。那么一个的inode中并没有文件名我们该怎么找到文件的inode编号呢 其实我们是通过该文件所在的目录的内容来找到该文件的我们知道Linux下一切皆文件那么目录也是一个文件目录文件也有自己的inode并且也有自己的data block其实在目录文件的data block中就存储了该目录下的所有文件名和对应inode编号的映射关系这样我们就能通过目录文件的data block里面的文件名和对应inode编号的映射关系来找到该目录下的文件了。所以当我们想要进入目录时其实就是执行目录这个文件所以我们需要该目录文件的执行权限x而当我们想要在目录中创建文件时需要将文件名和inode编号的映射关系写到目录文件的data block中所以需要目录文件的写权限w当我们想要查看目录下的文件信息时需要读取目录文件的data block中的数据所以需要目录文件的读权限r。 创建文件系统做了什么 根据文件要创建的目录找到这个目录文件所在的分区并且找到这个目录文件所在的块组然后遍历这个块组的inode Bitmap找到第一个为0的比特位将这个比特位置为1在遍历时进行累加那么在找到第一个为0的比特位的同时也找到了一个inode编号然后在inode Table里面将这个文件的inode的属性都写进去然后将这个inode的block块都先清零因为该文件为新创建还没有数据。此时就创建好了inode的编号然后通过用户的输入得到了文件名在目录文件的data block中将这一组文件名和inode编号的映射存入。 删除文件系统做了什么 删除文件时一定已经确定了要删除哪个目录下的文件然后找到这个目录文件的data block通过文件名在目录文件的data block中找到这个要删除文件的inode编号然后根据inode编号就可以找到这个文件所在的分区和块组然后将这个inode在块组内的inode Bitmap置为0并且将这个inode的block Bitmap占用的block块置为0然后将目录文件中的data block中的文件名和inode的映射关系删除这个文件就算删除了。 通过上面的过程我们发现删除文件其实并没有将该文件存储数据的data block清空而只是将该文件的inode和block置为无效所以删除的文件其实是能恢复的前提是这个inode编号没有被使用并且inode和data block没有被重复占用。 inode是固定的data block也是固定的所以有时候可能还有内存但是创建文件不成功这是因为inode用完了。 10、软硬链接 我们可以通过下面的命令来为文件建立一个软链接。-s 为 soft选项。 ln -s testLink.txt soft.link下面的命令为建立一个硬链接。 ln testLink1.txt hard.link从上面的结果中我们可以看到软硬链接的本质区别是有没有独立的inode我们看到软链接有独立的inode所以软链接是一个独立的文件而硬链接没有独立的inode所以硬链接不是一个独立的文件。 10.1 软链接 当在当前目录下执行另一个目录下的可执行程序时每次执行都要打出完整的路径所以可以建一个软链接。这就相当于windows下的快捷方式。 我们在test25目录下执行test24目录下的可执行文件时需要加上完整目录。 此时我们可以在test25目录中建立一个test可执行程序的软链接。使用这个软链接就可以执行test程序。即这个软链接就相当于windows下的快捷方式。可以理解成为软链接的文件内容是指向文件对应的路径。 10.2 硬链接 我们在创建硬链接时看到有一个数组从1变为了2这个属性就是文件的硬链接数。我们创建硬链接不是真正的创建新文件而是在指定的目录下建立了文件名 和 指定inode的映射关系。所以硬链接数会加1。当我们删除这个硬链接时这个文件的硬链接数就会减1。 那么硬链接数是什么呢 所以在Linux中还有一个删除文件的命令unlink即将文件的硬链接数减1。 unlink 下面我们新建一个文件和新建一个目录。我们看到新建立的文件的硬链接数为1因为这个文件的文件名和inode就是一组映射。而新建立的目录的硬链接数为2因为在当前目录下该目录的目录名和inode是一组映射而当进入该newdir目录里面时有一个. 文件这个文件名和inode也是一组映射即一个硬链接。 下面我们在newdir目录下再新建一个目录d1。发现newdir目录的硬链接数变为3因为在d1目录下有一个. . 目录为d1的上级目录newdir. . 和 inode又是一组映射。 当我们在newdir目录里面建一个目录newdir的硬链接数就加1因为每个目录里面都有一个 . . 。所以可以通过newdir的硬链接数-2得到该目录下有多少个目录。 在Linux下有很多的可执行程序都以符合为名字例如下面我们也可以使用符合来作为可执行程序的名字。 11、动静态库 静态库.a程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。 动态库.so程序在运行的时候才去链接动态库的代码多个程序共享使用库的代码。 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表而不是外部函数所在目标文件的整个机器码 在可执行文件开始运行以前外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中这个过程称为动态链接dynamic linking 动态库可以在多个程序间共享所以动态链接使得可执行文件更小节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用节省了内存和磁盘空间。 11.1 制作静态库 我们在lib目录下的mklib目录下创建四个文件。 然后在.h文件中写方法的声明在.c文件中实现方法。 然后我们再创建一个main.c文件在这个文件中写main函数并且调用上面实现的两个函数。我们将这三个.c文件共同编译为可执行文件my.exe。此时程序可以正常执行。 然后我们将mymath.c文件和myprint.c文件编译为.o文件。 此时我们再创建一个uselib目录并且将mklib目录下生成的.o文件和.h文件拷贝过来。然后我们将main.c编译为main.o文件然后将三个.o文件编译为可执行程序my.exe该程序中可以使用mymath.o和myprint.o中的方法。 可以看到当uselib想使用mklib里面的方法时需要将方法的.o文件和.h文件都拷贝到uselib目录下。这样当方法多了时就需要拷贝很多文件所以就有了打包。下面为将两个方法的.o文件打包为libhello静态库注意静态库名称以lib开头以.a为后缀。此时就形成了静态库。 下面我们来编写makefile文件自动生成库。我们看到makefile文件成功生成了静态库。 但是库中只有.o文件也是不行的还需要.h头文件。我们创建一个hello目录将所有.h头文件都放到include目录下将所有.a文件都放在lib目录下。然后我们再进行生成库这样hello目录下include目录中是库的所有头文件lib目录中是对应的库文件。下面我们在makefile中编写上面生成库的过程。 当我们指令make和make hello命令后可以看到生成了一个hello库该库的include目录下为库的所有头文件lib目录下为对应的库文件。 然后我我们将hello这个静态库拷贝到uselib目录下那么在uselib目录中该如何使用hello这个静态库呢直接使用会报出找不到头文件的错误。 如果想要使用一个库需要将这个库的头文件和库文件添加到系统库的头文件和库文件所在的目录中这样才可以搜索到我们的库的头文件和库文件。将库拷贝到系统的默认路径下的过程就叫做库的安装。 头文件gcc的默认搜索路径是/usr/include。 库文件的默认搜索路径是/lib64 或 /usr/lib64。 然后我们编译main.c时发现还会报错这是因为c语言自带的库不需要手动链接gcc会自动链接到c语言的静态库而我们自己写的库为第三方库所以需要手动链接。-l为链接库后面为库的名字库的名字要去掉前面的lib和后缀.a。然后我们发现main.c就成功编译生成了a.out可执行程序。 需要注意的是我们刚刚自己写的库没有经过测试也没有发布所以如果我们的库和系统中的库重名就会污染系统中的库所以我们测试完后就需要将自己的库删除掉。把头文件和库删除的过程就叫做卸载。 所以我们一般都是直接使用静态库。当想要直接使用库时需要 -I 然后后面跟头文件的路径即告诉c语言如果在系统库和当前目录下没有找到头文件就去这个路径下找。 -I然后我们发现报出了没有定义Print和addToTarget的错误这是因为我们没有指定库文件所在路径所以我们还需要使用 -L 指定库文件所在路径。 -L然后还是报错这是因为我们没有指定库文件的名称因为lib路径下可能有很多库文件我们需要写出具体要引用的库文件的名称。使用 -l 选项后面跟上库名并且库名称要去掉前面的lib和.a后缀。然后我们就在编译中使用了指定的库。 -l11.2 制作动态库 静态库在程序编译时会被连接到目标代码中程序运行时将不再需要该静态库。 动态库在程序编译时并不会被连接到目标代码中而是在程序运行时才被载入因此在程序运行时还需要动态库存在。 使用下面的-fPIC选项可以形成一个与位置无关的二进制文件。 gcc -fPIC -c mymath.c -o mymath.o可以使用下面的命令来读取一个二进制文件的内容 readelf -S mymath.o-shared选项就是生成动态库即将mymath.o和myprint.o两个文件生成一个动态库libhello.so。注意动态库是以.so结尾。 gcc -shared myprint.o mymath.o -o libhello.so下面编写makefile文件来生成动态库和静态库。 当我们执行make命令时可以看到就生成了以.a为后缀的静态库和以.so为后缀的动态库。 然后我们执行make output命令将动态库和静态库打包到一个目录下面。此时output目录下的include目录下都是.h的头文件output目录下的lib目录下都是库文件。 我们还可以将自己写的这个库进行压缩这样当其他人下载时就会更快了。 tar czf mylib.tgz output下面我们将这个压缩文件拷贝到uselib目录下并且使用解压命令将这个压缩文件进行解压可以看到mylib.tgz中的内容被成功解压。 tar -xzf mylib.tgz然后我们像使用静态库一样在编译main.c文件时告诉gcc头文件在哪个目录下动态库在哪个目录下。并且指定要使用的库文件。但是我们发现虽然成功编译并生成了可执行程序a.out但是当执行时发现并没有执行成功并且我们使用ldd命令查看a.out可执行程序依赖的动态库时发现libhello.so动态库找不到所以才会出现错误。 gcc main.c -I output/include -L output/lib -lhello//查看可执行程序依赖的动态库 ldd a.out我们知道output/lib目录下有静态库hello和动态库hello当我们执行-lhello命令时默认是连接到动态库的。只有当output/lib目录下没有hello动态库只有hello静态库时才会默认链接静态库。 当output/lib目录下有静态库和动态库hello时如果还想要链接到静态库那么可以在库名的后面加上 -static表示链接到静态库。如果报出下面的/usr/bin/ldcannot find **说明当前机器没有配置相关的编译环境所以需要执行下面的命令来安装相应的环境。然后我们编译main.c并链接到静态库可以看到生成的a.out可执行程序可以正常运行。 sudo yum install glibc-staticgcc main.c -I output/include -L output/lib -lhello -static所以使用gcc编译时动静态库的链接规则为 如果只有静态库那么gcc只能链接静态库。 如果动静态库都存在时gcc默认链接动态库。 如果动静态库都存在时想要链接静态库可以使用-static选项。 下面我们接着分析动态库链接我们看到当我们链接动态库进行编译生成的a.out可执行程序当运行时发现出现了错误并且使用ldd a.out命令查看该程序链接的动态库时发现libhello.so not found 我们知道动态库在程序编译时并不会被连接到目标代码中而是在程序运行时才被载入因此在程序运行时还需要动态库存在。并且动态库和可执行程序可以分批加载到内存中动态库在栈区和堆区之间的共享区中存在当a.out可执行程序中遇到动态库的内容时就会去共享区中找到动态库的地址然后通过这个地址在页表中的映射找到内存中动态库的地址然后得到动态库中函数的代码。我们在编译时虽然已经说明了动态库的路径但是那时对gcc编译器说的使gcc编译时通过这个路径找到动态库。但是当我们执行a.out可执行程序时并不知道动态库所在的位置所以才会报错。 我们有多种方法来解决上面可执行程序a.out找不到动态库的问题。 第一种方法将动态库拷贝到lib64目录下 可以看到a.out可以正常运行了。但是这样的方法因为直接将库添加到了系统的库所在的路径中所以会污染系统的库所以不建议使用。 第二种方法将自己的库所在的路径添加到环境变量中这样搜索库时也会去这个路径下搜索。 在linux中有一个LD_LIBRARY_PATH环境变量LD_LIBRARY_PATH环境变量用于在程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径。所以我们将自己的库所在的路径添加到这个环境变量中这样程序运行时也会去这个路径下查找动态链接库。但是这种方法当退出这次登录时环境变量的改变就没了此时还需要重新改变环境变量。 echo $LD_LIBRARY_PATH下面我们将自己写的动态库的路径添加到LD_LIBRARY_PATH环境变量中。 export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/home/drh/linux-learning/test26/lib/uselib/output/lib然后我们将/lib64目录下的libhello.so动态库删除此时a.out程序还可以正常运行因为我们将libhello.so动态库所在的路径已经添加到LD_LIBRARY_PATH环境变量中了。 第三种方法在配置文件中添加路径。 在/etc/ld.so.conf.d目录下创建一个以.conf为后缀的配置文件然后在该文件中将动态库所在的路径添加进去。 ls /etc/ld.so.conf.d/我们看到当在test.conf文件中添加完路径后执行a.out时还是报错了这是因为我们还没有更新配置文件我们执行sudo ldconfig命令更新配置文件后此时可以看到a.out程序成功执行了。并且我们退出登录后再登录时也可以正常执行a.out程序。 sudo ldconfig当我们将test.conf配置文件删除时我们发现此时a.out程序还可以正常运行这是因为我们没有更新配置文件此时缓存中还有test.conf这个配置文件当我们再次执行sudo ldconfig命令更新配置文件后a.out就不可以正常运行了。 第四种方法在lib64目录下建立软链接 这种方法在/lib64目录下建立libhello.so动态库的软链接。这样当去/lib64目录下查找libhello.so动态库时就会根据软链接去libhello.so动态库所在的路径下查找了。 sudo ln -s /home/drh/linux-learning/test26/lib/uselib/output/lib/libhello.so /lib64/libhello.so
http://www.hkea.cn/news/14292148/

相关文章:

  • 网站开发要学哪些设计师图片素材
  • 邯郸网站建设优化排名开封美食网站建设规划
  • 搜索引擎作弊网站有哪些海外seo投放
  • 网站开发学习淮南新浪网络推广公司
  • 昆明市住房和城乡建设局门户网站网站建设用什么书
  • 差异基因做聚类分析网站研发项目备案在哪个网站做
  • 学校网站在哪里找百度一下浏览器
  • 电子商务实验网站建设实训过程平台手机app开发
  • 天都城网站建设wordpress rpc
  • 网站开发的步骤wordpress精品主题
  • php下载站源码外贸建设网站公司
  • 成都网站建设制作公司河南高端网站高端网站建设
  • 英文网站推广公司wordpress关键词屏蔽
  • 上海虹口建设局官方网站装修免费出效果图
  • 达州市建设局网站盛世阳光-网站建设
  • 公司做网站的费用入账网页界面设计的尺寸
  • 新乡公司网站建设敦煌网网站评价
  • 住房和城乡建设厅门户网站重庆技术网站建设
  • 南昌做网站kaiu工业产品设计与创客实践赛题库
  • 购物网站模板站比分网站怎么做
  • 正能量网站免费入口不用下载网站获取用户
  • 用群晖做网站江门网站平台建设
  • 淄博哪里有网站建设平台物联网流量卡
  • 学用mvc4做网站广州软件外包公司排名
  • 做外贸需掌握的网站建设公司网站需要什么技术
  • 公司网站后台导航链接怎么做网站开发硬件
  • html5网站多少钱海外房地产网站建设
  • 广州营销网站制作网站建设维护公司排名
  • 网站开发工具设备要求百度搜图
  • 重庆营销型网站随做的好处用wordpress 登录界面