目录
- 重新谈论文件
- 先来段c语言文件接口
- 标记位
- linux系统文件接口
- 文件描述符
- 文件描述符分配规则
- 重定向
- 使用dup2系统调用
- 如何理解linux下一切皆文件
- 缓冲区
- 磁盘
- 文件系统
- 理解软硬链接
- 动静态库
重新谈论文件
- 空文件也要占据磁盘空间
- 文件 = 内容 + 属性
- 文件操作 = 对内容 + 对属性 + 对内容和属性
- 找到一个文件,必须通过文件:文件路径 + 文件名 【唯一性】
- 如果没有指明对应的文件路径,默认实在当前路径下进行文件访问
- 当我们把fopen,fclose,fread,fwrite等接口写完了之后,形成二进制可执行程序之后,但是没有运行,文件对应的操作并没有被执行 本质:文件操作是进程对文件的操作!
- 一个文件如果没有被打开,不能直接进行文件访问。 被谁打开?-> 用户进程 + os
所以文件操作的本质: 进程和被打开文件的关系
是不是所有的文件都被打开了?不是!
被打开的文件
没有被打开的文件->文件系统
c语言右文件操作接口,c++也有文件操作接口,java,python,php等也有 ,但是它们的调用接口都不一样。
而文件在哪里呢?文件在磁盘中,磁盘是硬件,想要访问它必须经过操作系统,而操作系统也提供文件级别的系统调用接口,而且操作系统只有一个,只有一套系统调用接口。
所以,无论上层语言如何变化
a.库函数底层必须调用系统接口
b.库函数可以千变万化,但是底层不变,都是调用的系统接口。
先来段c语言文件接口
//测试c语言文件函数
void test_c_file_func()
{
//r,w, r+(读写,文件不存在报错),w+(读写,文件不存在创建),a+(读写,在文件末尾读写)
//以w的方式单纯打开文件,c会自动情况内部的数据
/* umask(0);
FILE* pf = fopen(FILE_NAME, "w");
int cnt = 5;
while(cnt)
{
fprintf(pf, "%s:%d\n", "hello world", cnt--);
}
fclose(pf); */
FILE* pf = fopen(FILE_NAME, "r");
char buffer[64];
while(fgets(buffer, sizeof buffer, pf))
{
//把字符串末尾\n替换成\0
buffer[strlen(buffer) - 1] = 0;
puts(buffer);
}
fclose(pf);
}
标记位
c/c++传标记位参数:一个整数代表一个标记位。
如果我们要传入多个参数的时候,这样的方法就很不方便。所以就有了比特位参数传递选项。
比特位参数代码展示:
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
//比特位标识符
void show(int flags)
{
if(flags & ONE) printf("one\n");
if(flags & TWO) printf("two\n");
if(flags & THREE) printf("three\n");
}
void test_bit()
{
show(ONE);
puts("-----");
show(ONE | TWO);
puts("-----");
show(ONE | TWO | THREE);
}
linux系统文件接口
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "log.txt"
//系统文件接口
void test_sys_file()
{
//写入接口
/* umask(0);
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
int cnt = 5;
char buffer[1024];
while(cnt)
{
sprintf(buffer, "%s, %d\n", "hello sys", cnt--);
write(fd, buffer, strlen(buffer));
} */
//读取接口
/* int fd = open(FILE_NAME, O_RDONLY);
char line[1024];
ssize_t sz = read(fd, line, sizeof(line) - 1);
line[sz] = 0; //把读取的数据变成字符串格式
puts(line);
close(fd); */
//追加接口
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND);
char buffer[1024];
sprintf(buffer, "%s", "aaa\n");
write(fd, buffer, strlen(buffer));
close(fd);
}
int main()
{
test_sys_file();
}
接口介绍 :
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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:文件描述符fd
成功:新打开的文件描述符
失败:-1
在认识open返回值之前我们先来认识一下两个概念: 系统调用 和 库函数
- 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
- 而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
- 回忆一下操作系统概念图
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
文件描述符
- 通过对open函数的学习,我们知道了文件描述符就是一个小整数
如何理解文件?
文件操作的本质:进程和被打开文件的关系
进程可以打开多个文件,系统中一定会存在大量的被打开的文件,被打开的文件需要被操作系统管理起来,如何管理?先描述再组织->操作系统为了管理打开的文件,必定要为文件创建对于的内核数据结构标记文件->struct file{}->包含了大部分的文件属性
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
文件描述符分配规则
直接看代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是 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数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符。
重定向
#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);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出
重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢?
虽然上述代码可以实现重定向,但是还有先关闭文件,这种方法不太好,遇到复杂的场景无法实现。
使用dup2系统调用
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
例子:在minishell中添加重定向功能
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#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 commondLine[1024];
char* myargv[64];
int sig = 0;
int code = 0;
//重定向文件
char* redirFile = NULL;
//重定向类型
int redirType = NONE_REDIR;
//对命令进行解析
void checkCommond(char* commond)
{
char* start = commond;
char* end = commond + strlen(commond);
while(start < end)
{
if(*start == '<')
{
*start = '\0';
redirType = INPUT_REDIR;
start++;
trimSpace(start);
redirFile = start;
}
else if(*start == '>')
{
*start = '\0';
start++;
if(*start == '>')
{
redirType = APPEND_REDIR;
*start = '\0';
}
else
{
redirType = OUTPUT_REDIR;
}
trimSpace(start);
redirFile = start;
}
else
{
start++;
}
}
}
int main()
{
int status;
while(1)
{
//每次需要重载重定向的类型和重定向文件
redirFile = NULL;
redirType = NONE_REDIR;
printf("[用户名@主机名 文件名]$ ");
fflush(stdout);
char* str = fgets(commondLine, 1024, stdin); //-1是保证\0被读入
assert(str != NULL);
(void)str;
//把读入的\n替换成\0
commondLine[strlen(commondLine) - 1] = 0;
checkCommond(commondLine);
//切割字符串,存入一个指针数组中
myargv[0] = strtok(commondLine, " ");
int i = 1;
//当命令未ls时,自动添加带颜色选项
if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
{
myargv[i++] = (char*)"--color=auto";
}
while(myargv[i++] = strtok(NULL, " "));
//内建/内置命令处理
if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "cd") == 0)
{
chdir(myargv[1]);
continue;
}
if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
{
if(strcmp(myargv[1], "$?") == 0)
{
printf("%d %d\n", code , sig);
}
else
{
printf("%s\n", myargv[1]);
}
continue;
}
//如果没有子串了,strtok返回NULL,此时myargv[end] = NULL
//条件编译检查是否截断字符串成功
#ifdef DEBUG
for(int j = 0; myargv[j]; ++j)
{
printf("argv[%d]: %s\n", j, myargv[j]);
}
#endif
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
switch (redirType)
{
case NONE_REDIR:
break;
case INPUT_REDIR:
{
int fd = open(redirFile, O_RDONLY);
assert(fd != -1);
dup2(fd, 0);
close(fd);
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
int set = O_WRONLY | O_CREAT;
if(redirType == INPUT_REDIR)
set |= O_TRUNC;
else
set |= O_APPEND;
int fd = open(redirFile, set, 0666);
assert(fd != -1);
dup2(fd, 1);
close(fd);
}
break;
default:
printf("redir error\n");
break;
}
execvp(myargv[0], myargv);
exit(1);
}
pid_t ret = waitpid(id, &status, 0);
assert(ret > 0);
(void)ret;
sig = status & 0x7f;
code = status >> 8 & 0xff;
}
}
问题1:子进程重定向会对父进程产生影响吗?
不会,创建子进程会拷贝父进程的pcb,也会拷贝父进程pcb里struct files_struct* files指向的管理文件的表,子进程进行重定向修改的是它自己的管理文件的表,所以不会对父进程产生影响。
问题2:创建子进程会拷贝父进程的struct files_struct表,会不会拷贝表中指向的文件struct file呢?
不会,文件的内核数据结构struct file属于文件管理,而表属于进程管理,创建子进程和进程管理有关,不会对文件管理产生影响,所以不会。
如何理解linux下一切皆文件
对于普通文件,目录我们可以很好的理解,但是对于键盘,显示等我们可能不好理解。
其实对于linux来说,所有文件都是用一个结构体来封装,文件的信息都在里面,而且它们的输入输出方法都是用一个函数指针来表示,只是函数指针指向的函数实现不同而已,这些函数一般在驱动里,如果这个方法不存在,比如键盘没有输出函数,那么它的结构体中函数指针指向null即可,所以linux下一切皆文件。
补充:其实struct file里面还有一个整形数据,是指所以进程中的struct files_struct表中指向它的个数,我们调用close函数的本质其实是取消指向它,那么这个整形数据将要减小1,当它为0时候,才会真正的关闭。
缓冲区
缓冲区刷新策略:
如果有一块数据,一次写入到外设(效率最高) vs 如果有一块数据,多次少量写入到外设
因为与外设打交道很慢,所以一次写入最快
缓冲区一定会结合具体的设备,定制自己的刷新策略:三个策略和两个特殊情况
三个策略:
- 立即刷新 -- 无缓冲
- 行刷新 -- 行缓存 -- 显示 解释:显示器是给人看的,人习惯一行一行看,所以是行刷新
- 缓冲区满 -- 全缓冲 -- 磁盘文件
两个特殊情况
- 用户强制刷新,比如 fflush
- 进程退出 -- 一般都要进行缓冲区刷新
那么缓冲区在哪里?
代码
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和
fork有关!
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据
- 的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的
- 一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,
都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的"上层", 是对系统
调用的"封装",但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是
C,所以由C标准库提供。
FILE结构体
我们可以看看linux内核中FILE结构体
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
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
};
模拟实现c语言文件库函数
有了以上知识,我们可以模拟实现一套c语言库函数调用
//myStdio.h
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#define SIZE 1024
#define SYNC_NOW (1 << 0)
#define SYNC_LINE (1 << 1)
#define SYNC_FULL (1 << 2)
typedef struct
{
int _flag; //刷新策略
int _fileno; //文件描述符
char _buffer[SIZE]; //缓冲区
int _size; //缓冲区有效数据大小
int _capacity; //缓冲区容量
}_FILE;
_FILE* fopen_(const char* fileName, const char* mode);
void fwrite_(const char* str, int num, _FILE* stream);
void fclose_(_FILE* stream);
void fflush_(_FILE* stream);
//myStdio.c
#include "myStdio.h"
_FILE* fopen_(const char* fileName, const char* mode)
{
int flag = 0;
if(strcmp(mode, "r") == 0)
{
//以读的方式打开
flag |= O_RDONLY;
}
else if(strcmp(mode, "w") == 0)
{
//以写的方式打开
flag |= O_WRONLY | O_CREAT | O_TRUNC;
}
else if(strcmp(mode, "a") == 0)
{
//以追加的方式打开
flag |= O_WRONLY | O_CREAT | O_APPEND;
}
else
{
//其他方式打开
}
int fd = 0;
if(flag & O_RDONLY)
fd = open(fileName, flag);
else
fd = open(fileName, flag, 0666);
//打开文件失败
if(fd == -1)
{
const char* err = strerror(errno);
write(2, err, strlen(err));
return NULL;
}
_FILE* pf = (_FILE*)malloc(sizeof(_FILE));
pf->_fileno = fd;
memset(pf->_buffer, 0, SIZE);
pf->_size = 0;
pf->_flag = SYNC_LINE;
pf->_capacity = SIZE;
return pf;
}
void fwrite_(const char* str, int num, _FILE* stream)
{
memcpy(stream->_buffer + stream->_size, str, num);
stream->_size += num;//不考虑缓冲区满的情况
//判断刷新策略
if(stream->_flag & SYNC_NOW)
{
write(stream->_fileno, stream->_buffer, stream->_size);
stream->_size = 0;
}
else if(stream->_flag & SYNC_LINE)
{
if(stream->_buffer[stream->_size - 1] == '\n')
{
write(stream->_fileno, stream->_buffer, stream->_size);
stream->_size = 0;
}
}
else if(stream->_flag & SYNC_FULL)
{
//暂时不考虑溢出的情况
if(stream->_size == stream->_capacity)
{
write(stream->_fileno, stream->_buffer, stream->_size);
stream->_size = 0;
}
}
}
void fflush_(_FILE* stream)
{
write(stream->_fileno, stream->_buffer, stream->_size);
//强制操作系统进行外设刷新
fsync(stream->_fileno);
stream->_size = 0;
}
void fclose_(_FILE* stream)
{
fflush_(stream);
close(stream->_fileno);
}
//test.c
#include "myStdio.h"
int main()
{
_FILE* pf = fopen_("log.txt", "w");
int cnt = 10;
while(cnt--)
{
const char* str = "hello world ";
fwrite_(str, strlen(str), pf);
if(cnt == 5)
fflush_(pf);
sleep(1);
}
fclose_(pf);
return 0;
}
磁盘
磁盘的物理结构:
-
其中圆盘的两个盘面都可以存储数据,而且每一面都有一个磁头,所以有多少盘面就有多少磁头,它们俩都有自己的马达来控制它们的转动。
-
而且,盘面和磁头是没有接触的,它们几件的距离非常小,所以一般磁盘的制动都是在真空下进行的,因为一旦有灰尘,判断和磁头高速旋转,灰尘就会影响它们的转动。
-
磁盘是一个物理结构,而且是一个外设,所以它的访问对于系统来说很慢,但对于人来说依旧很快。
一个盘面的结构图:
磁道 : 磁盘的盘面被划分成一个个磁道, 一个圈就是一个磁道。
扇区 : 磁盘寻址的最小单位,大小为512字节,每一个磁道被划分成一个个扇区,每个扇区就是一个个 " 数据块 ",各个扇区存放的数据量相同。
- 最内侧磁道上的扇区面积最小, 因此数据密度最大
在单位面上,如何定位一个扇区呢:
磁盘中定位任意一个扇区没采用的硬件基本的定位方式:CHS定位法
磁盘的逻辑结构
我们把磁盘当作一个磁带,把磁带抽出来就像是磁盘的逻辑结构。
通过这个数组的下标我们可以计算出物理地址即CHS地址。
为什么操作系统不直接用CHS地址?
- 便以操作系统管理
- 避免与硬件产生强耦合,如果不这样,换了别的磁盘或硬盘就要重新组织管理方式,这样太不方便。
-
虽然磁盘的最小访问单元是一个扇区512字节,但是依旧很小
-
os内的文件系统定制的进行多个扇区的读取 -> 1kb, 2kb, 4kb(主流,即8个扇区)为基本单位
-
你哪怕指向读取/修改一字节,也必须将4kb的数据加载进内存,进行读取和修改,如果必要,再写回磁盘
-
这是因为局部性原理!(我们修改数据时,它附近的数据也可能会受影响)
-
内存中的数据是被划分成了4kb的空间 -- 页框
-
磁盘中的文件尤其是可执行文件,都是按照4kb划分好的块 -- 页帧
[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
可以看到io block为4kb
文件系统
如何管理磁盘?
我们会先把磁盘进行分区,每个分区相当于一个文件系统,每个分区的管理方式是一样的,所以我们研究一个区就可以。
对于每个区我们又会进行分组。
一个分区的结构:
其中boot block是启动块,存储者启动信息,对于分组,每个分组的管理方式都是一样的。
block group 即为组
其中文件 = 属性 + 内容
其中属性存储在inode中,内容存储在data block中
-
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,
未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的
时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个
文件系统结构就被破坏了,但是我们可以发现超级块是在组里面存放的,
但是为什么有一个就够了,为啥多个组都有了?防止其中一个超级块数据损坏了,可以复制其他组的超级快过来,这样防止数据丢失。
-
GDT,Group Descriptor Table:块组描述符,描述块组属性信息,记录着有多少inode,data block,以及它们的使用情况
-
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没
有被占用
-
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
-
i节点表(inode table):存放文件属性 如 文件大小,所有者,最近修改时间等
-
数据区(data block):存放文件内容
其中文件inode存储者文件的属性,但是文件名不在inode中,而且inode还存储者它所使用的data block
那么文件名存储在哪呢?
目录的inode存储者目录的属性,目录的数据块存储者目录里文件的inode和文件名的映射关系。
创建一个新文件主要有一下4个操作:
- 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。 - 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
复制到300,下一块复制到500,以此类推。 - 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。 - 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文
件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
删除文件操作:
删除文件很简单,只需要把inode位图和数据块位图里的1变成0,即:使用变成未使用,所以文件并没有真正被删除,我们可以通过inode恢复,但是当以后我们使用了这个inode就很难恢复了。
理解软硬链接
硬链接
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个
inode, 这种方法其实就是硬链接。
创建硬链接命令:
ln 要链接的文件 链接后的文件
可以看到在进行硬链接前啊a.out的硬链接数是1,发生硬链接后,硬链接数变成了2,而且硬链接形成的文件和原来的文件除了文件名一模一样,包括inode,文件大小,文件内容都没有变。
硬链接数是什么呢?
其实在inode结构体中有一个数据就是硬链接数表示有多少文件名指向这个inode,只有当inode中硬链接数变成0了该文件才真正的删除,也就是该inode位图和data block位图中的1才真正变成0。
硬链接的应用:
创建一个目录的时候它的目录的硬链接数为2,因为在这个目录里有一个 点 硬链接 当前目录
当我们再一个目录里再创建一个目录,原来的目录的硬链接数就由2变成了3,因为新目录里有两个点硬连接上级目录
软链接
软链接是创建一个新的文件引用要被链接的文件,是通过名字引用被链接的文件的,并不是通过inode。
软链接命令:
ln -s 要链接的文件 形成的链接文件
可以看到a.out发生硬链接形成的soft_link具有自己的inode和内容,其实内容就是a.out的绝对路径,把a.out删去后就链接不上了。
再创建一个新的a.out又会自动链接上,这样就证明了软链接只认路径
软链接应用: window上的快捷方式其实就是软链接实现的。
动静态库
从我们的c语言知识我们可以知道,代码进行编译,链接形成可执行程序,其实不同的源文件我们是分离编译的,也就是不同的源文件我们分别编译成不同的.o汇编文件,然后再进行链接。
所以当我们让别人使用自己定义的函数时候,我们又不想要给它们看源文件,我们就可以先把源文件分别编译成汇编文件,然后把汇编.o文件和头文件.h给他们就行。其实这种方式就像是库一样
所谓动静态库就是把上述的.o文件打包
测试程序
/add.h/
int add(int a, int b);
/add.c/
#include "add.h"
int add(int a, int b)
{
return a + b;
}
/sub.h/
int sub(int a, int b);
/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(10, 20)=%d\n", a, b, add(a, b));
a = 100;
b = 20;
printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}
生成静态库
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
[root@localhost linux]# gcc main.c -I. -L. -lmymath
-I 指定头文件路径
-L 指定库路径
-l 指定库名
默认头文件和库路径:当前路径和系统/usr/include和/lib64
测试目标文件生成后,静态库删掉,程序照样可以运行。
库搜索路径:
-
从左到右搜索-L指定的目录。
-
由环境变量指定的目录 (LIBRARY_PATH)
-
由系统指定的目录
- /lib64
- /lib
生成动态库:
-
shared: 表示生成共享库格式
-
fPIC:产生位置无关码(position independent code),编译时加上-fPIC选项
-
库名规则:libxxx.so,其中xxx是真正的库名
代码示范://编译
gcc -fPIC -c add.c
gcc -fPIC -c sub.c//生成共享动态库
gcc -shared -o libmymath.so add.o sub.o
动态库是在运行时加载进内存的,所以在运行的时候还需要找到动态库。
- 拷贝.so文件到系统共享库路径下, 一般指/lib64
- 更改 LD_LIBRARY_PATH,在变量后面加上自己创建的动态库地址
- ldconfig 配置/etc/ld.so.conf.d/,创建一个文件里面存储动态库的路径,然后ldconfig命令进行更新
- 在/lib64下建立自己定义的动态库软链接
动静态库加载细节:
静态库:静态库是在编译的时候加载进内存,然后把静态库拷贝到进程的进程地址空间的代码段,未来这部分代码,必须通过相对确定的地址位置进行访问
动态库:动态库在编译期间把每段代码的偏移量写入到我们的可执行程序中!而调用这段代码有相对地址是不够的,我们还需要起始地址,当我们运行的时候,动态库加载进内存,并把动态库的起始地址写入进程地址空间的共享区,然后进程就可以通过起始地址+偏移量来调用这段代码了。