目录
一、文件操作
1.文件操作接口的多样性与统一性
我们之前就学过C语言的文件操作接口,那么对于C++,JAVA,Python等等语言都有自己的文件操作接口。虽然这些语言的接口有着同样的功能,但是这些接口的使用有一定的不同。
文件储存在磁盘上,磁盘是硬件,而操作系统负责管理软硬件,所以对于硬件的访问只有通过操作系统才能进行,只要想访问磁盘就绕不开操作系统。C语言也好,其他语言也罢,都是语言开发者开发出来让普通程序员可以方便使用的,所以任何上层语言想要进行文件操作,都会使用操作系统提供的接口。
2.C语言文件接口的复习
(1)文件指针
我们每当打开一个文件的时候,系统会根据文件的情况自动创建这样一个FILE类型的结构体变量,并填充其中的信息,使用者不必关心细节。
定义pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区。通过该文件信息区中的信息就能够访问该文件。(FILE* pf)
(2)打开文件
FILE * fopen ( const char * filename, const char * mode );
const char * filename表示文件名, const char * mode表示操作对应的字符串,返回值为指向文件信息区的文件指针
常见的文件操作的字符串:
(3)关闭文件
int fclose ( FILE * stream );
(4)fwrite
size_t fwrite(const void* buffer,size_t size,size_t count,FILEstream);
- 从内存的变量中获取二进制数据,放到文件中
- const void buffer表示获取数据的位置,size_t size一个变量类型的大小,size_t count表示读多少个这样类型的数据,FILE* stream为文件指针
(5)fread
size_t fread( void buffer, size_t size, size_t count, FILE stream );
从文件中获取二进制数据,放到内存的变量中
const void buffer表示获取数据的位置,size_t size一个变量类型的大小,size_t count表示读多少个这样类型的数据,FILE stream为文件指针
在linux执行如下代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
const char* arr = "Hello world\n";
int i = 0;
for (i=0; i<5; i++)
{
fwrite(arr, strlen(arr), 1, pf);
}
fclose(pf);
return 0;
}
执行结果:
然后在linux执行如下代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if(pf == NULL)
{
perror("fopen:");
return 1;
}
char buffer[100];
char* example = "Hello world\n";
while(1)
{
size_t s = fread(buffer, 1, strlen(buffer), pf);
if(s > 0)
{
buffer[s] = '\0';
printf("%s",buffer);
}
else if(feof(pf))
{
printf("文件读取结束\n");
break;
}
}
fclose(pf);
return 0;
}
二、系统文件的IO操作
不管是打开还是读写,都是在对文件进行操作,文件操作的根本上是进程操作被打开的文件,进程作为被打开文件的使用者,进程理所当然地将需要使用的文件记录于自己的PCB中。另外,由于进程对应的程序也是一个磁盘文件,因此PCB还要记录自己的文件的相关信息。但是一切文件操作都要经过操作系统操作系统,所以操作系统还要自己维护一个记录所有进程打开的文件的总表。
那么接下来就来讲解,文件操作的系统调用:
1.几个系统调用
调用接口:int open(const char *pathname, int flags, mode_t mode);
头文件:sys/types.h、sys/stat.h、fcntl.h
参数:path表示要打开或创建的目标文件名,flags表示选项,mode表示文件的起始权限
功能:成功打开文件返回该文的文件描述符,失败返回-1
open这里着重讲一下flags参数,它既可以是一个数字,也可以是几个数字的按位或。我们知道在按位或,只要几个数字中的某一个比特位上是1,那么最后的结果那一位上也一定是1。这里的flags也是利用率这样的设计,比如说0010是只写,而0001是检测文件是否存在,不存在则创建它,那么0010 | 0001 = 0011。也就是进程可以操作写文件,该文件不存在时也可以创建文件,但是我们不能进行读取文件等操作。
我们列举部分参数:
选项宏 | 功能 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读,写打开 |
O_CREAT | 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 |
O_APPEND | 追加写 |
注:O_RDONLY、O_WRONLY和O_RDWR只能三选一使用
这样,通过不同比特位对应不同的选项,就让计算机知道了,哪些操作可以干,哪些操作不可以干。
在C语言中写入文件接口是write,而fwrite是基于系统调用接口write的封装。
调用接口:ssize_t write(int fd, const void *buf, size_t count);
头文件:unistd.h
参数:fd为文件描述符,buf表示需要写入的数据的头指针,count表示需要写入的字节数。
功能:将内容写到对应文件内
调用接口:int close(int fd);
头文件:unistd.h
参数:fd为文件描述符
功能:关闭文件
下面让我们用系统调用来代替C语言的文件操作:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
if(fd<0)
{
perror("open fail");
return 1;
}
int cnt =5;
char outBuffer[64];
while(cnt)
{
sprintf(outBuffer,"%s:%d\n","hello world",cnt--);
write(fd,outBuffer,strlen(outBuffer));
//只有在C语言中规定字符串以\0结尾,但这个调用是系统级的,不再遵循这一规律,就不需要将\0也写入进去
}
close(fd);
return 0;
}
执行结果,创建了文件也写入了内容:
C语言中的fopen是open的封装,close是fclose的封装,fwrite是write的封装……也就是说C语言乃至任何语言的文件操作在底层都是使用了上面的系统调用,因为对文件的操作是绕不开操作系统的。
2.文件描述符fd
(1)什么是文件描述符
在上面的接口中,我们多次提到了文件描述符。那这个文件描述符是怎么标识文件的呢?
我们打开一个文件并打印open返回的被打开文件的文件标识符,结果为3
问题来了,文件描述符是从0开始递增的,那么0、1、2都去哪了?
在学习C语言文件操作的时候,我们提到过三个标准输入输出流。
标准输入流(stdin)对应键盘、标准输出流(stdout)对应显示器、标准错误流(stderr)也对应显示器,在Linux下它们也是文件,分别占据了0、1、2的位置。因为在Linux进程默认情况下会有3个缺省打开的文件标准输入输出和错误流,分别占用三个描述符。
我们在学习C语言文件操作的时候,我们曾经学过一个文件指针(FILE* pf),它和文件描述符又有什么联系呢?
这里的FILE是一个结构体,这个结构体里储存了一个文件的一些信息,这个文件指针指向这个结构体对象。但是我们在使用系统调用的时候,文件指针并没被使用,而是使用的是文件描述符fd,那么我们可以推断出在FILE结构体中必定有存储文件描述符的字段。
在操作系统中,一个进程在运行中可以打开很多个文件,通过先描述,后组织的方式,操作系统对被打开文件的信息进行管理,Linux都为它们创建对应的内核数据结构——struct file,这个结构体对象内包含文件的大部分属性。
但是注意这个结构体可并不是我们上面文件指针指向的那个结构体对象。
Linux中一切皆文件,不管是键盘,显示器还是硬盘,他们在操作系统看来都是文件,它们也都是有自己的struct file结构体。
而每一个进程都会有它的PCB,它打开了几个文件是很重要的信息,所以PCB中就有一个struct file* fd_array[]的数组,内部保存着指向不同struct file对象的指针。我们观察到的文件描述符是从零开始的连续整数,其实这些数字就是PCB中保存的struct file fd_array[]数组的下标。
所以我们也可以理解了,在本质上C语言的文件指针实际上是对文件描述符的封装。
当进程打开一个文件时,也就是进程执行open系统调用,此时每个进程PCB都有一个指针指向一个指针数组,这个指针数组着指向各个打开的文件的file结构体。只要有文件描述符,就可以找到对应的文件的file结构体,找到这个结构体后,进程就可以操作文件了。
(2)文件描述符的分配
在Linux上运行下面的代码:
#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);
//close(1);
umask(0);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n",fd);
close(fd);
return 0;
}
如果我们关掉0,那么open新打开的文件fd为0;如果我们关掉2,那么open新打开的文件fd为2,如果我们关掉1和2,那么open新打开的文件fd为1……
这里我们可以得到文件描述符的分配规则:在默认情况下进程会打开的标准输入输出和错误流,分别占用0、1、2三个描述符,如果再打开其他文件,该文件的描述符会是当前文件描述符中未被占用的最小值。
3.重定向
(1)什么是重定向
我们再次修改并使用上面的代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
umask(0);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("open fd: %d\n", fd);
fprintf(stdout, "open fd: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
此时我们关掉了标准输出流,新打开的文件占用了它的文件描述符,本来这些内容是要打印到屏幕上的,但是这里却输入到了test.txt这个文件中。
本来是写入标准输出文件中,但是改变文件描述符对应的文件指向,最后打印到了test.txt文件中,这个就是重定向。所以,重定向的本质就是:保持上层使用的fd不变,而在内核中更改fd对应的struct file* 的地址,偷天换日。
(2)dup2系统调用
调用接口:int dup2(int oldfd, int newfd);
头文件:unistd.h
参数:oldfd为需要转移位置的文件描述符,newfd表示为需要转移到文件描述符的位置
功能:将oldfd的内容覆盖到newfd处(不是交换)
使用dup2系统调用
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
printf("open fd: %d\n", fd);
close(fd);
return 0;
}
运行后的代码依旧可以将内容写到文件中
3.shell的完善
(1)部分重定向符
符号 | 描述 |
> | 符号左边输出作为右边输入(标准输出) |
>> | 符号左边输出追加右边输入 |
< | 符号右边输出作为左边输入(标准输入) |
echo "hello world" > test.txt 将输出在显示器的内容重定向输出到test.txt中且清除之前的内容
echo "hello world" >> test.txt 将输出在显示器的内容重定向输出到test.txt中且为追加
cat < test.txt 将test.txt中的内容作为cat语句的输入
(2)添加重定向操作
在shell中添加一个commandCheck函数
void commandCheck(char* commands)
{
//正扫描
assert(commands);
//用于循环扫描
char* start = commands;
char* end =commands+strlen(commands);
while(start<end)
{
if(*start == '>')
{
*start = '\0';
++start;
if(*start=='>')
{
//"ls -a" >>file.log
redirType = APPEND_REDIR;
++start;
}
else{
//"ls -a" >file.log
redirType = OUTPUT_REDIR;
}
trimSpace(start);
redirFile = start ;
break;
}
else if (*start =='<')
{
//"cat < file.txt"
*start ='\0';
start++;
trimSpace(start);//用于过滤空格
//填写重定向信息
redirType =INPUT_REDIR;
redirFile =start;
}
else{
start++;
}
}
}
shell主程序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
//#define DEBUG
#define NUM 1024
#define OPTION 64
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define trimSpace(
start)do{\
while(isspace(*start)) start++;\
}while(0)
char command[NUM];//储存用户输入的指令
char* my_argv[OPTION];
int lastcode = 0;
int lastsig = 0;
int redirType = NONE_REDIR;//重定向的文件类型
int main()
{
while(1)
{
//输出命令行
printf("[用户@主机名 当前路径]¥");
fflush(stdout);
//获取输入的内容
char* p = fgets(command, sizeof(command)-1, stdin);//清除最后输入的\n
assert(p != NULL);
command[strlen(command)-1] = 0;//把command最后一位编程\0
commandCheck(commands);
//字符串切割
my_argv[0] = strtok(command, " ");//第一段一定是指令名,先切割下来
int i = 1;
if(my_argv[0] != NULL && strcmp(my_argv[0], "ls") == 0)//ls的特殊处理
{
my_argv[i++] = (char*)"--color=auto";
}
while(my_argv[i++] = strtok(NULL, " ")){}
//循环分段,传NULL表示接着上一次切割完的位置继续切割,不可分时会返回NULL退出循环
#ifdef DEBUG
int j = 0;
for(j = 0 ; my_argv[j]; j++)
{
printf("myargv[%d]:%s\n", j, my_argv[j]);
}
#endif
if(my_argv[0] != NULL && strcmp(my_argv[0], "cd") == 0)
{
if(my_argv[1] != NULL)
{
chdir(my_argv[1]);
}
continue;
}
if(my_argv[0] != NULL && my_argv[1] != NULL && strcmp(my_argv[0], "echo") == 0)
{
if(strcmp(my_argv[1], "$?") == 0)
{
printf("%d, %d\n", lastcode, lastsig);
}
else
{
printf("%s\n", my_argv[1]);
}
continue;
}
//创建子进程运行相关可执行程序
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
execvp(my_argv[0], my_argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);//子进程执行指令,父进程阻塞式等待
assert(ret > 0);
lastcode = ((status>>8) & 0xFF);
lastsig = (status & 0x7F);
}
return 0;
}
注:执行程序替换时,不会影响之前进程打开的重定向文件。由于进程替换是将代码进行覆盖,而重定向是在pcb中改变fd的指向,所以程序替换和重定向在PCB中改变的内容是不同的,二者没有联系。
三、缓冲区
1.提出问题
我们在以前的学习中经常会接触到缓冲区,那么这个缓冲区使如何工作的,在哪里定义也是我们应当关注的问题。
在Linux上运行这段代码,然后再次运行并将结果重定向输入到一个文件中:
#include <stdio.h>
#include <string.h>
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
//C语言库函数
const char *s="hello write\n";
write(1,msg,strlen(msg));
//系统调用
fork();
return 0;
}
运行结果:
我们发现所有的C语言接口内容都打印了两遍,而系统调用只打印了一遍。
如果我们注释掉fork呢?
程序又把每个只打印一次了,这个现象就和我们将要学习的缓冲区有关。
2.认识缓冲区
先举个栗子:
比如说,你在大学中结识了关系很好的朋友,在假期中你们回到了各自的家。他的家在上海而,你的家在天津。朋友希望你能带一些当地的特产给他,你也同意了。
此时就有两种选择:你自己给他送过去或者寄快递给他送过去。
如果你选择自己送过去,天津离上海一千多公里,就算是从天津西站坐高铁还要5个多小时才能到。虽然跟过去相比已经是很快的了,但是又浪费自己的时间,而且费用也高,得不偿失,我们应该将货物交到当地的驿站,让快递公司帮忙运送。
这段路程就好比是进程将数据输入到磁盘,如果进程自己去发送数据,消耗的时间会很长,而且代价也会很大。所以,我们设计了一块内存叫做缓冲区,缓冲区就相当于中间的快递公司,快递公司是专门运送货物的。而同样缓冲区也是专门送数据的,以缓冲区为中介运送数据,速度快且代价小。
通过上述例子,我们可以得出缓存区存在的意义:节省进程进行数据的IO时间
所以,我们之前的系统调用fwrite实际上是进程将数据传递到缓冲区中,fwrite并不是直接写入到文件中,实质fwrite就是一个拷贝函数,将数据从进程拷贝到缓冲区或者外设中。
3.缓冲区刷新策略
缓冲区刷新策略跟快递公司送货的策略相似,一般情况下我们寄件的时候并不是物件一交到快递公司手里就立刻寄货,而是物件积累到达一定量才送走。就像快递公司用飞机运货,如果一有货飞机就起飞未免过于浪费资源。
缓冲区也是一样的,进程将数据拷贝到缓冲区中,缓冲区会结合具体的设备,定制自己的刷新策略
以下是三种常见的刷新策略:
名称 | 规则 | 文件或设备 |
立即刷新 | 无缓冲,有数据就刷新 | 使用场景很少 |
行刷新 | 行缓存,当出现进程终止、遇到\n、缓冲区满的情况就刷新 | 显示器 |
缓冲区满 | 全缓冲,缓冲区满才刷新 | 磁盘文件 |
行刷新一般情况下是显示器采用的,因为文字是用于人与人沟通的。我们给朋友发消息,每次都等到缓冲区占满了再发给他,一看消息都是等的时间又久,话的长度还长,也太反人类了。而在文件读写的时候常会采用全缓冲,减少读写的次数,减少对操作系统资源的占用,使效率达到最高的。
还有一些特殊情况:
(1)用户强制刷新————使用fflush函数等
(2)进程退出————进程退出一般都要进行行缓冲区刷新
4.缓冲区的本质
又回到最开始的问题,为什么重定向后C接口又重复打印了一遍?不过我们目前可以推断出缓冲区一定不在内核中,如果缓冲区在内核中,那么系统接口write也应该打印两次。
也就是说,我们所提到的所有缓冲区,都是指的语言层面给我们提供的缓冲区,不涉及操作系统内部。
我们可在xshell中,用vim打开该文件vim /usr/include/stdio.h查看FILE结构体的定义
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;//fd
#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
};
所以,说到最后,这些现象到底是什么原因呢?
(1)如果我们没有进行重定向,看见4条消息的原因:
在Linux中显示器也是文件,向显示器中写数据也是文件操作,而标准输出流默认使用行刷新而输入的内容每一个都以\n结尾,在子进程被创建之前,三个C函数已经将数据输出到显示器上(外设),你FILE内部的缓冲区此时是空的, 也就不会接着输出。
(2)如果我们进行了重定向,7条消息中少1条write消息的原因:
write属于系统调用,不经过语言级的缓冲区,而是直接交给操作系统的缓冲去调度。
我们所谓的无缓冲(立即刷新)只是没有语言级的缓冲区,直接将数据给操作系统安排的缓冲中。
(3)调用fork显示7条消息,而注释fork显示4条的原因:
fork一旦被调用,创建进程时,父子进程共享数据和代码。但是紧接着就是进程退出,不管是子进程还是父进程先退出,一定要进行缓冲区刷新(就是修改),这个时候就会发生写时拷贝,没有退出的那个进程的缓冲区数据还在,最终数据会显示两份也就打印了7条条消息。我们关闭fork,没有发生写时拷贝,那么输入到显示器上的还是4条消息。
5.代码模拟缓冲区
到这里我们知道了fwrite等函数都是通过底层系统调用实现的,所以我们可以对这些C函数进行简单的模拟实现
(1)模拟实现及测试
myStd.h
#pragma once
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
//buffer-缓冲区大小
#define SIZE 2014
//缓冲模式
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4
typedef struct _FILE
{
int flags; //刷新方式
int fileno; //文件标识符
char buffer[SIZE]; //缓冲区
int cap; //buffer的总容量
int size; //buffer当前的使用量
}FILE_;
//打开文件
FILE_ *fopen_(const char *path_name,const char *mode);
//写入
void fwrite_(const void* ptr,int num,FILE_ *fd);
//关闭文件
void fclose_(FILE_* fp);
//强制刷新缓冲区
void fflush_(FILE_* fp);
myStd.c
#include "myStdio.h"
FILE_ *fopen_(const char *path_name,const char *mode)
{
//打开文件时,可以传入多个参数选项用下面的一个或者多个常量进行“或”运算,构成flags。
int flags =0;
int defaultMode =0666;//默认打开文件权限
if(strcmp(mode,"r")==0)//选择以读的方式打开文件
{
flags |=O_RDONLY;//只读模式:O_RDONLY
}
else if(strcmp(mode,"w")==0)//选择以写的方式打开文件
{
flags |=(O_TRUNC|O_WRONLY|O_CREAT);//只写模式:O_WRONLY-只写,O_CREAT-创建新文件,O_TRUNC-覆盖原有
}
else if(strcmp(mode,"a")==0)//选择以追加的方式打开文件
{
flags |=(O_WRONLY|O_APPEND|O_CREAT);//追加模式:O_WRONLY-只写,O_CREAT-创建新文件,O_APPEND--追加
}
else
{
//还可以有更多的打开方式
}
//调用系统接口实现
int fd = 0;
if(flags & O_RDONLY) fd=open(path_name,flags);//只读不需要其他权限
else fd = open(path_name,flags,defaultMode);
//调用失败,说明原因,返回null
if(fd < 0)
{
const char* err = strerror(errno);//获取错误码
write(2,err,strlen(err));//向fd=2-stderr中打印,显示错误原因
return NULL;
}
FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));//开辟结构体大小空间
assert(fp);
fp->flags =SYNC_LINE; //默认设置成行刷新
fp->fileno =fd; //open返回的文件标识符--文件描述符:通过映射 路径+文件名
fp->cap = SIZE; //缓冲区总容量
fp->size = 0; //实际总容量
memset(fp->buffer,0,SIZE); //初始化buffer
return fp;//为什么打开一个文件,就返回一个FIEL* 指针--因为是用FIEL的方式组织,便于后续操作--写,刷新缓存,关闭文件等
}
void fwrite_(const void* ptr,int num,FILE_ *fp)//写文件
{
//先写入到缓冲区中
memcpy(fp->buffer+fp->size,ptr,num);//默认缓冲区足够大,不考虑溢出
fp->size+=num;
//规定刷新方式
if(fp->flags & SYNC_NOW)//立即刷新
{
write(fp->fileno,fp->buffer,fp->size);//将缓冲区数据拷贝到打开文件中
fp->size = 0; //清空缓冲区
}
else if(fp->flags & SYNC_LINE)//\n--行刷新
{
if(fp->buffer[fp->size-1]=='\n')//有\n就拷贝并清空缓冲区
{
write(fp->fileno,fp->buffer,fp->size);
fp->size=0;
}
}
else if(fp->flags & SYNC_FULL)//实际与当前容量相等才刷新,也叫全刷新
{
if(fp->size==fp->cap)
{
write(fp->fileno,fp->buffer,fp->size);
fp->size = 0;
}
}
else
{
//还可以有更多的刷新方式
}
}
void fflush_(FILE_* fp)
{
if(fp->size>0)
write(fp->fileno,fp->buffer,fp->size);
fsync(fp->fileno);//这个函数可以强制要求将数据写入磁盘
fp->size=0;
}
void fclose_(FILE_* fp)
{
fflush_(fp);//文件关闭时刷新缓冲区
close(fp->fileno);
}
实验代码:
#include "myStdio.h"
int main ()
{
FILE_* fp=fopen_("test.txt","w");//传入路径名和刷新模式
if(fp==NULL)
{
return 1;
}
int cnt = 10;
const char *msg = "hello!\n";
while(1)
{
fwrite_(msg, strlen(msg), fp);
//fflush_(fp);
sleep(1);
printf("count: %d\n", cnt);
//if(cnt == 5) fflush_(fp);
cnt--;
if(cnt == 0) break;
}
fclose_(fp);
return 0;
}
监控脚本:while :; do cat test.txt ; sleep 1 ;echo "##########################" ; done
如果我们注释掉fflush,那么五个hello会在最后被打印出来,而如果我们不注释fflush每一个hello都会一行一行打印出来。
(2)数据写入文件的全过程
此时一个完整的读写数据的过程就呈现在我们眼前了。
- 首先打开文件时需要找到对应的struct file结构体
- 找到后在进程PCB中添加指向它的指针同时文件描述符也被确定
- 调用C或其他语言的接口,将数据拷贝到FILE结构体的语言级缓冲区内
- 调用系统调用将数据按无缓冲、行缓冲或全缓冲等拷贝进内核缓冲区,同时刷新语言级缓冲区
- 操作系统自主决定数据刷新规则并将内核缓冲区的数据拷贝到文件,也会刷新缓冲区。但是这里是操作系统层从缓冲区刷新到磁盘的过程是非常复杂的,不能只用我们学过的这三个简单刷新规则来理解且这里的操作人为不可干预。
四、文件系统
我们知道文件是储存在磁盘里的,但是我们对于这个朝夕相处的磁盘是陌生的。我们不知其为何物,不知它的内部是什么样子,更不知道它如何存数据,找数据和删数据的。下面我们就先了解我们最熟悉也最不熟悉的硬件之一——磁盘。
1.认识磁盘
磁盘分为硬盘与软盘。其中软盘是计算机最早的可移介质,曾经盛极一时。但是软盘容量小,寿命短,由于U盘的出现,软盘的应用逐渐衰落直至淘汰。而硬盘是一个机械结构加外设的组合体,机械结构即使再快相比于光速级别运动的电子还是慢了太多。所以硬盘的访问速度相比于内存和CPU是非常慢的。而硬盘中还分为机械硬盘(HDD)和固态硬盘(SSD),它们的工作方式不同。
现在笔记本电脑中更多是选择固态硬盘,相比传统的机械硬盘,固态硬盘具有读写速度快、防震抗摔、低功耗、无噪音、工作温度范围大、轻便的特点。但是由于固态硬盘做工更加精细,因此价格更贵,而且固态硬盘是有读写的次数限制的,如果读写过多可能会导致固态硬盘的击穿。虽然固态硬盘优势不小,但是依旧不能取代传统机械硬盘。
由于传统机械硬盘与固态硬盘都各有所长,各有所短,在很多时候人们也会选择混盘就是磁盘和固态硬盘混用。
2.机械硬盘的物理结构
硬盘的结构可分为外部结构和内部结构,我们主要讲解内部结构与工作原理。
(1)外部结构
硬盘外部会有一个金属的面板,金属面板与其外壳封存着硬盘的内部结构,用于保护整个硬盘。
在金属板的背面,我们能看到它控制内部结构的电路板。(这一部分都是暴露在外的)该电路板上的电子元器件大多采用贴片式元件焊接,这些电子元器件组成了功能不同的电子电路,这些电路包括主轴调速电路、磁头驱动与伺服定位电路、读写电路、控制与接口电路等。
(2)内部结构
硬盘的内部结构专指盘体的内部结构,它是一个密封的腔体,里面密封着磁头、盘片(磁片)等部件。
- 硬盘的盘片是硬质磁性合金盘片,它的两面都可以通过磁效应储存数据,所以对应的磁头也是上下各一个。盘片的片厚一般在0.5mm左右,直径主要有1.8in(1in=25.4mm)、2.5in、3.5in和5.25in四种,其中2.5in和3.5in盘片应用最广。
- 硬盘的盘片一般不止一张,这些盘片安装在主轴电机的转轴上,在主轴电机的带动下高速旋转。每张盘片的容量称为单碟容量,而硬盘的容量就是所有盘片容量的总和。早期硬盘单碟容量低,所以,盘片较多,而现代硬盘的盘片一般只有少数几片。
- 一块硬盘内的所有盘片都是完全一样的,使用不同数量的盘片,也就实现了一个系列不同容量的硬盘。
- 磁头摆动和盘片旋转是通过马达控制的,我们可以通过硬件电路组成伺服系统给磁盘发二进制指令,然后让磁盘定位去寻址某个特定的区域,然后从磁盘上读取数据。
- 盘片的转速与盘片大小有关,通常盘片越大转速越低。一般来讲,2.5in硬盘的转速在5 400 r/min~7 200 r/ min之间,3.5in硬盘的转速在4 500 r/min~5 400 r/min之间,而5.25in硬盘转速则在3 600 r/min~4 500 r/min之间。随着技术的进步,现在2.5in硬盘的转速最高已达15 000 r/min,3.5in硬盘的转速最高已达12 000 r/min。虽然已经是如此惊人的速度,但是机械结构相比于电子流动还是差了很远。
- 磁头和盘片是不接触的,它们的间距大概在0.1微米到0.3微米之间。(一根头发的直径为几十微米)打个比方就像一架波音757在你的头上1米的距离飞行,真正的刀尖上跳舞。
- 磁盘是绝对不能沾灰尘的。因为磁头与盘面的距离过于微小且盘片旋转速度极快,所以磁盘必须防抖动,一旦磁盘沾上灰尘,即使是我们常说的PM2.5,其直径也有2.5微米。一旦落灰,磁头和盘面接触就会使得盘面刮花,丢失数据。一旦磁盘的内部结构被打开,这个磁盘就直接报废了,所以磁盘的组装一般都会在特定的高度无尘环境中。
- 机械硬盘的内部运行
硬盘无盖运行
3.磁道和扇区
数据储存在盘面上,我们拿出一个盘片,观察其中的一面。
磁道是盘面上的半径不同的同心圆环,而每一个磁道按角度被分为几个相同的圆弧叫做扇区,也是磁头寻址的基本单位。虽然越靠近圆心外侧面积越大,但是可以通过工艺进行不同扇区的电子密度大小进行调解,使得每个扇区存储都是512byte。
由于主轴上一般都有多个盘片,每个盘片上又有两个盘面,而且所有磁头的运动都是同步的(你哪那边动我也往那边动),也就是说每一个磁头都会同时偏转到自己盘片的相同磁道上,这些磁道构成一个上下的面,叫做柱面。所以我们除了说定位磁道,也可以叫定位柱面。
磁头在在单面中定位一个扇区的规则:先确认磁道再定位扇区,磁头先来回摆动确认磁道,盘片旋转过程中确定扇区。
4.磁盘的逻辑结构
(1)数组化的抽象理解
我属于00后,还记得小时候拿录音机听磁带的时光。不过随着存储技术的进步,甚至光盘都逐渐退出了历史舞台,小时候用录音机放英语听力的记忆再也不会发生了。
仔细观察磁带,它的两个滚轮上缠着一条长长的带子。而如果我们把磁带扯直,它就是一条瘦长的矩形,这条长带中存放着数据。而磁盘也是一样的,我们将磁盘的磁道拉直,也是矩形,里面同样存在着数据。
我们一般电脑选择的是500GB硬盘,这500GB是可以分盘的,这也就是我们在电脑上看到的CDEF等磁盘的原因。所有的磁盘只是一个范围分区,最终存储的位置都是一块硬盘。
这样我们就可以将不同盘面理解成一整块数据,而整个磁盘看做一个数组,这样就方便对磁盘进行管理。
所以一个盘面为4(2个盘),每一面磁道数为10,每一个磁道扇区为100,每个扇区可存储512个字节的数据的硬盘的容量为:4*10*100*512 = 2048000bytes
对磁盘进行管理就变成了对数组进行管理,通过磁道(track)->盘面(platter)->扇区(sector)的顺序寻找数据位置。在操作系统中我们就可以通PTS(一种查找方法)进行查找,找到的扇区地址,这个扇区的地址在操作系统中被称为LBA地址。
所以对于LAB-127地址:
127/1000 = 0 —— 第0盘面
127/100 = 1 —— 第1号磁道
127%100 = 27 —— 第27号扇区
(2)访问数据
访问一个扇区是512字节,如果将磁盘的访问的基本单位设置512字节,对于IO访问来说效率太低,一般是对8个扇区进行同时访问,也就是4KB的空间。OS内的文件系统的硬盘信息读取是以4KB为基本单位的,即使只读取或者修改一个比特位,也必须将4KB的数据从硬盘加载到内存中,再进行读取或者修改,如果有需要,就再写回磁盘。
在本质上,内存被划分成了无数个4KB大小的空间,也叫页框。而为了适配内存的空间分配规则,磁盘中的数据也按照4KB大小一块一块划分好,它每一个4KB的空间,叫做页帧,在后面的称呼中我们叫它块(block)。所以磁盘向内存拷贝数据,实质就是将页帧的数据拷贝到页框中。
不管是拷贝数据还是修改数据,那么最开始都是需要将数据管理起来,这里我们通过分治的思想,一级一级的到向下一层减少管理空间,我们将最底层管理,对于上层而言也就是重复的工作了。所以理解文件管理我们就开始从这5GB开始(自定义5GB)。
5.理解文件系统
(1)文件属性
我们在使用ls -l指令时,我们不光看到文件名,还有关于这个文件的许多属性。
比如对于下面的ls -l显示:
drwxrwxr-x 40 example example 4096 Jan 15 16:25 file
文件类型和权限 硬链接数 文件所有者 文件所属组 大小 最后修改时间 文件名
我们知道一个文件包括文件的内容和文件的属性,但我们不知道的是在linux中文件属性和文件内容是分批储存的。
一个文件属性的,文件的大小,权限,等等信息在都储存在一个叫Inode的结构体中,同样反过来说,每一个文件都对应一个独一无二的inode对象。
当我们调用ls -l等查看文件属性的命令时,操作系统会将硬盘中储存的inode结构体的信息拷贝到内存中,然后再拷贝到显示器这个外设中,我们也就看到了文件的属性。
除了ls -l,也可以使用:stat+文件名,查看文件属性。
stat test.c
File: ‘test.c’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fd01h/64769d Inode: 656174 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1005/ hongxin) Gid: ( 1005/ hongxin)
Access: 2023-01-16 15:44:36.036230458 +0800
Modify: 2023-01-16 15:44:36.036230458 +0800
Change: 2023-01-16 15:44:36.036230458 +0800
Birth: -
这里显示I/O块(IO Block)为4kb,也再次证实磁盘访问的基本单位4kb(大多数操作系统)。
(2)硬盘空间的划分
在ext2文件系统中,一块硬盘的空间分配满足下图:
一块硬盘由一个启动块和许多个块组组成
启动块(Boot block):大小为1kB,用于存储磁盘的分区信息和系统的启动信息,任何文件系统都不能使用该块。如果这个块损坏,整个文件系统也就无法启动,甚至操作系统的启动也不能进行。
块组(Block group):文件系统会根据分区的大小划分为数个Block Group,每个Block Group都有着相同的结构,block group从启动块开始编号从0到n。
在一个块组(Block group)还可以被分为超级块(Super block)、块组描述(Group Descriptors)、数据块位图(Data block Bitmap)、inode位图(inode bitmap)、inode表(inode table)、数据区(Data blocks)
- 超级块(Super block):存放文件系统本身的结构信息。记录的信息主要有:整块硬盘内bolck(4KB大小)和inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。
当Super Block的信息被破坏,整个文件系统的结构就被破坏了,所以在每一个块组的超级块中都备份了一份文件系统信息,当一个块组的超级块数据出错时,就可以将其他分组的Super Block拷贝到当前,文件系统就能得到恢复。
- inode表(inode Table) :保存了分组内部所有可用的(已经使用和未被使用)inode。当我们创建一个文件时,第一时间找的就是inode Table,将文件属性如文件大小,所有者,最近修改时间等存放在这个表中
- 数据区(Date blocks):保存分组内部所有文件的数据库
- inode位图(inode Bitmap):每个比特位对应一个inode,用0和1表示一个inode是否被占用。
位图中比特位的位置和当前文件对应的inode一一对应的。比特位的位置代表的是它是第几个inode,比特位为0 ,代表inode未被占用,比特位为1,代表inode被占用。通过位图中比特位的偏移量就可以从inode Table中通过同样的偏移量找到对应的inode,从而进行增加属性等操作 。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。与inode位图效果一致
- 块组描述符GDT(Group Descriptor Table):储存对应分组的宏观的属性信息
(3)查找文件的属性和内容
一方面,操作系统是怎么在你需要一个文件的属性和内容时能够快速帮你找到它的,另一方面,你在电脑的目录上看到的许多文件名,它们真的就是文件本身吗?
我们需要了解我们如何创建一个文件
首先,储存文件属性
当我们创建一个文件时,操作系统会先在inode bitmap查找一个未能被占用的inode(位图对应位置为0),找到后就在inode table中建立一个新的inode结构体并初始化,而这个结构体内有一个编号变量,这个编号就是申请的inode值。
接着,储存文件内容
Data block中也被分为很多的4KB大小的block,根据文件大小确定需要几个内存块。比如说,该文件需要存储在三个磁盘块,内核同时也找到了三个空闲块,它们的编号为:300,500,800。那么,内核缓冲区保存的文件内容将先拷贝到第一块数据,复制到编号为300的数据块,满了之后接着复制到编号为500的数据块,直到完全拷贝完成。
那么对于一个很大的文件,这些数据块并不止是链表式的查找,而更多的是树形结构。一个内存块储存文件的内容,会对应到别的多个块。从一棵树的根到枝叶,从一个块链接到N个块,N个块中的每一个又连接到N个块,这样数据的读取就十分快速了。
然后,记录内存块的分配情况
虽然是树形结构,但是没有指针可以链接各个块,所以内核会在inode上的磁盘分布区记录了上述块列表。
最后,添加文件名到目录
inode是操作系统用于识别文件的工具,而文件名是用户识别文件的工具。
我们打开目录时,其实是打开了一个目录文件,一旦我们创建了一个文件,那么这个文件的文件名和inode对应的映射就被添加到这个目录文件内。用户通过对应路径的文件名找到文件,当我们打开文件时,操作系统会通过目录文件的文件名和inode映射找到文件的inode结构体,从而找到在硬盘内储存的文件属性和内容。
这四步就是文件被创建的整个过程,操作系统通过inode找到你需要的文件的属性和内容时。你在电脑上打开文件夹时本质打开了一个目录文件,而目录上看到的许多文件也只是文件名和inode的映射关系。
五、软硬连接
1.软硬连接的建立
(1)建立软连接
语法:ln -s+[目录或文件] + [软连接的名称](ln -s test.c soft_test.link)
功能:创建指向对应文件的软链接文件
我们发现软连接文件指向了test.c
(2)建立硬连接
语法:ln + [目录或文件] + [软连接的名称](ln test.c hard_test.link)
功能:创建指向对应文件的硬链接文件
2.软硬连接的区别
注意观察,硬链接hard_test.link和test.c的硬链接数变为2,而且二者的inode值相同,文件大小也一致,而且如果我们向test.c文件中写入数据,二者的大小都会变化。
软链接sort_test.link的硬链接数为1,它的inode和test.c的inode不同,文件大小也不一致。
二者的区别:
- 软链接:有独立的inode,是一个独立的文件,它可以通过路径和文件名链接到对应的文件。
- 硬链接:硬链接没有独立的inode,建立一个硬链接就是在指定路径下,新增一个文件名和inode编号的映射关系。
在shell中的做法 :硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。
3.文件的删除
删除文件使用的是惰性删除法,当一个文件不存在任何文件名与inode的连接关系时,换句话说就是硬链接数为0的时候,一个文件才算真正被删除。
比如,我们删除test.c
我们发现软连接文件变红了,那个这个文件还存在吗?其实是存在的,虽然test.c文件被删除了但是soft_test.link -> test.c的inode链接关系还是在的。由于软链接是根据路径和文件名找到对应文件,所以当我们删除对应文件时软连接就不可用了。
不过,如果我们再次新建一个test.c,软连接依旧可以使用,但是链接的对象就是一个新的文件了。
软链接其实和我们在windows桌面的快捷方式是一致的。
你点开快捷方式,快捷方式文件链接到对应的启动程序目录下打开对应文件。
4.软硬连接的应用
(1)软链接
我们在./link目录下建立test.c文件,然后随便写上代码,生成可执行文件test
#include<stdio.h> int main() { printf("I am a process\n"); return 0 }
建立软连接
用软连接运行
软链接与Windows中的快捷方式的优势是一样的,不需要进入相应的目录就可以运行对应程序
(2)硬链接
普通文件或文件夹的硬链接没有太特殊的应用,我们使用cd等命令时。我们很多时候会使用cd .或者cd ..等语句,那你知道为什么会这样使用呢?
我们知道Linux的目录也是文件,而一如果我们使用ls -al观察目录,你会发现上面有一个点和一个点点的文件,一个点就是本目录的硬链接,两个点就是该目录的父目录的硬链接。
我们可以按link->empty->dir的顺序依次创建文件夹,则它们的硬链接数满足以下条件:
用指令验证:
六、动静态库
1.动静态链接与动静态库
在我们之前的讲解中我们知道C语言库中的代码想要使用必须要包含头文件,头文件中有库函数的声明,而定义在库文件中
同样对于动态链接,我们需要某一个函数的定义就可以直接到相应的库里去找,就不需要把库中的函数实现拷贝一份到程序文件中,减少了程序占据的存储空间大小。但是每次遇到某个函数的定义都要再去对应位置寻找,降低了程序的运行效率。
而对于静态链接,我们就需要把库中的函数实现拷贝一份到程序文件中,我们也不需要去对应的库中寻找函数定义,增加了程序的运行效率,但是又大大增加了程序占据的存储空间大小。
静态库是指使用静态链接时,库中文件的代码全部加入到可执行文件中,这个被加载的库就是静态库。静态编译的程序在运行时不再需要库文件的查找。在Linux下静态库的后缀名一般为.a,Windows下为.lib
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时 链接文件加载库,这个需要被查找的库就是动态库。在Linux下动态库一般后缀名为“.so”,Windows下为.dll。
2.静态库的生成和使用
(1)生成可执行程序
我写了几个函数,分布在几个.c文件中
my_add.h
#pragma once
extern int Add(int x,int y);
my_add.c
#include "my_add.h"
int Add(int x,int y)
{
printf("entrt Add func,%d+%d=?\n",x,y);
return x+y;
}
my_sub.h
#pragma once
extern int Sub(int x, int y);
my_sub.c
#include "my_sub.h"
int Sub(int x, int y)
{
printf("entrt Add func,%d-%d=?\n",x,y);
return x-y;
}
main.c
#include "my_add.h"
#include "my_sub.h"
int main()
{
int a = 10;
int b = 20;
int ret = Sub(a,b);
printf("Sub result: %d\n", ret);
ret=Add(a, b);
printf("Add result: %d\n", ret);
}
我们如果需要生成可执行程序,第一种方式就是将多个.c文件进行编译和链接形成一个可执行程序。不过现在只是三个文件,指令的编写还是不算很长。但是当我们有十个或者二十个这样的.c文件,那么这样编写要关联太多的源文件和头文件,这时就新出现了库。
另一种方式就是,让这些定义函数的.c文件编译为.o文件,然后再将它们合成一个库,让主程序使用库中的函数实现来运行。
(2)静态库的生成
语法:ar + -rc + [将生成的静态库名称] + [需要集合的.o文件](例如:ar -rc libmymath.a my_add.o my_sub.o)
功能:创建.o文件组合成的静态库,其中ar(archive)是gnu归档工具,rc表示(replace and create)
库的发布(output):把一个文件移动到另一个自己定义的output文件夹。
为了方便我们可以写一个makefile
libmy_math.a:my_add.o my_sub.o
ar -rc $@ $^
my_add.o:my_add.c
gcc -c my_add.c
my_sub.o:my_sub.c
gcc -c my_sub.c
.PHONY:output
output:
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp -f *.a my_lib/lib
cp -f *.h my_lib/include
.PHONY:clean
clean:
rm -rf *.o libmy_math.a
其中$@指my_math.a,$^指后面的所有.o文件,output就是将所有的库和.h拷贝到对应文件夹作为未来的发行版本提供给用户使用。其中.h文件可以看函数的声明从而确定使用方式,库中定义了函数的实现,但.o文件已经是二进制码,只有计算机能看懂,所以也变相保护了程序员耗费心血写的源代码不被盗用。
生成库和发布库:
如果此时的库已经可以达到发布的要救,就可以使用压缩软件打包,库和头文件从而给他人使用。(压缩命令:tar czf + 文件名)
他人使用时可以直接解压使用(解压缩命令:tar xzf + 文件名)
(3)用库生成可执行程序
在生成可执行文件时,有几个细节需要特别注意:
通过路径找到头文件 -I ./mylib/include/
通过路径找到目标文件 -L ./mylib/lib
如果链接第三方库,必须指明库名称 libmymath.a需要去掉前缀和后缀 -- mymath
最后指令就是:gcc -o m_ymath main.c -I ./my_lib/include -L ./my_lib/lib -I my_math
这时就可以生成可执行文件了。
gcc默认是动态链接,而对于特定的库,它取决于你提供的是动态库还静态库。如果是多个库,gcc也是对库一个一个进行链接,例如:2个动态库,1个静态库,gcc处理动态库用动态链接处理两次,用静态链接处理一次。
我们写的代码是静态库,但是会用到动态库,那么gcc会把代码拷贝到可执行程序里。所以我们查看mymath可执行程序它是动态链接--gcc默认。
(4)安装和卸载
操作系统链接库需要对应目录,一般 Linux 系统把/usr/lib和/usr/local/lib两个目录作为默认的库搜索路径,所以使用这两个目录中的库时不需要进行设置搜索路径即可直接使用。
拷贝(从你自己建立的output目录拷贝):
sudo cpoutput目录/include/* /usr/include/
sudo cp output目录/lib/*.a /lib64/
所以将我们自己实现的第三方库拷贝到对应的默认路径中后,此时生成可执行程序就只需要指明需要使用的库。
再次编译:
gcc main.c -l mymath
而这样的库和头文件的拷贝就是我们常说的安装。
而把默认路径的库和头文件删除,这个过程就叫做卸载。
3.动态库的生成和使用
(1)动态库的生成
动态库的生成也可以用makefile统一编写
libmymath.so:my_add.o my_sub.o
gcc -shared -o $@ $^
my_add.o:my_add.c
gcc -c -fPIC my_add.c
my_sub.o:my_sub.c
gcc -c -fPIC my_sub.c
.PHONY:output
output:
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp -f *.a my_lib/lib
cp -f *.h my_lib/include
.PHONY:clean
clean:
rm -rf *.o libmymath.so
动态库以.so结尾,首先将.c文件编译为.o文件,而且还要加上-FPLC的生成路径无关码的选项,再用gcc合成库。当然也可以清除和发布,将头文件和库压缩后就可以提供给他人使用。
(2)动态库生成可执行程序
同样,我们也可以用动态库生成可执行程序,命令如下:
gcc -o mymath main.c -I mylib/include -L mylib/lib -l mymath
虽然此时我们生成了可执行文件,但是当我们运行它时却会报错。这又是为什么呢?
gcc形成了可执行文件,程序编译已完成,但程序执行的过程和gcc无关,程序运行是操作系统的管理,所以操作系统也需要找到动态库,总结就是:只要动态库文件没有在系统路径下,操作系统就无法找到。
第一种方式,可以将库路径添加到环境变量LD_LIBRARY_PATH中
指令:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:+动态库的路径
此时运行就可以成功了,但是改变环境变量只是改变内存的内容(暂时性),每次环境变量都需要在开机时进行拷贝,一旦关机,内存中内容的改变就都不存在了。
第二种方式,如果我们需要永久保留这种改变,那就需要将动态库路径添加到配置文件的路径中(永久性)。
指令合集:
- 进入系统目录:cd /etc/ld.so.conf.d/
- 在当前配置文件下新建文件:sudo touch Test.conf
- 进入Test.conf添加动态库路径(用vim打开输入路径):sudo vim Test.conf
- 更新动态路径缓存:sudo ldconfig
- 再次运行可执行程序:./mymath
第三种方式,可以在系统的配置文件夹中添加该库文件的软连接
第四种方式,就是直接将库拷贝到默认搜索的路径,最简单粗暴
4.使用外部库--ncurses库
(1)什么是ncurses库
ncurses库(new curses)是一套编程库,它提供了一系列的函数以便使用者调用它们去生成基于文本的用户界面。
(2)安装
使用yum指令安装:sudo yum install ncurses-deve
(3)测试
#include <curses.h>
int main()
{
char c;
initscr();//ncurse界面的初始化函数
printw("please input a char:\n");
c = getch();
printw("\nc = %c\n",c);
getch();//等待用户的输入,如果没有这句话,程序就退出了,看不到运行的结果,也就是无法看到上面那句话
endwin();//程序退出,恢复shell终端的显示,如果没有这句话,shell终端字乱码,坏掉
return 0;
}
gcc编译且包含ncurses库:gcc test.c -lncurses,最后运行
运行后会进入一个图形化界面,你可以向里面随意输入内容,仅此而已。
5.动静态库的加载
(1)静态库不需要加载
当静态库参与编译时,在磁盘中的.c文件和静态库库,会先在磁盘中形成一段代码,然后对这段代码进行编译,编译会先经过预处理,编译,同时会查找错误,最后在无误时形成汇编代码,然后进行汇编形成二进制指令。而库是由.o文件集合形成的,它们各个文件在编译阶段的时候就已经形成了虚拟地址空间,在虚拟地址空间中,这段代码被存入了代码区。当执行这段代码的时候,操作系统就会直接在这个文件形成的代码区进行访问,虚拟地址再通过页表获取到真实地址从而达到运行的目的。
(2)动态库的加载
动态库加载的过程,就有讲究了。在磁盘中有一个my.exe(可执行)和lib.so(动态库),在形成可执行文件之前,还记得编译阶段时我们用-fPIC,产生位置无关码码?(gcc -c -fPIC my_add.c)
在这个阶段,动态库会将指定的函数地址,根据地址+偏移量的方式写入到可执行程序中(位置无关码决定各函数再虚拟地址中的偏移,从而决定了他们的储存,由于是偏移量,所以是位置无关的)。
形成好可执行文件之后,磁盘将可执行文件拷贝到内存中,内存通过页表映射到虚拟地址空间的代码区中,当操作系统执行程序时,扫描到my_add.c是需要调用动态库的时候,程序会停下来,OS会再通过函数的地址,然后页表映射去内存到磁盘中找动态库中,找到后拷贝到内存,又通过页表映射到共享区中。操作系统再去共享区调用该方法,然后继续向下执行程序。