linux系统之基础io

重新谈论文件

  1. 空文件也要占据磁盘空间
  2. 文件 = 内容 + 属性
  3. 文件操作 = 对内容 + 对属性 + 对内容和属性
  4. 找到一个文件,必须通过文件:文件路径 + 文件名 【唯一性】
  5. 如果没有指明对应的文件路径,默认实在当前路径下进行文件访问
  6. 当我们把fopen,fclose,fread,fwrite等接口写完了之后,形成二进制可执行程序之后,但是没有运行,文件对应的操作并没有被执行 本质:文件操作是进程对文件的操作!
  7. 一个文件如果没有被打开,不能直接进行文件访问。 被谁打开?-> 用户进程 + 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 如果有一块数据,多次少量写入到外设
因为与外设打交道很慢,所以一次写入最快
缓冲区一定会结合具体的设备,定制自己的刷新策略:三个策略和两个特殊情况
三个策略:

  1. 立即刷新 – 无缓冲
  2. 行刷新 – 行缓存 – 显示 解释:显示器是给人看的,人习惯一行一行看,所以是行刷新
  3. 缓冲区满 – 全缓冲 – 磁盘文件

两个特殊情况

  1. 用户强制刷新,比如 fflush
  2. 进程退出 – 一般都要进行缓冲区刷新

那么缓冲区在哪里?
代码

#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地址?

  1. 便以操作系统管理
  2. 避免与硬件产生强耦合,如果不这样,换了别的磁盘或硬盘就要重新组织管理方式,这样太不方便。

  • 虽然磁盘的最小访问单元是一个扇区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个操作:

  1. 存储属性
    内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
  2. 存储数据
    该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
    复制到300,下一块复制到500,以此类推。
  3. 记录分配情况
    文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. 添加文件名到目录
    新的文件名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

动态库是在运行时加载进内存的,所以在运行的时候还需要找到动态库。

  1. 拷贝.so文件到系统共享库路径下, 一般指/lib64
  2. 更改 LD_LIBRARY_PATH,在变量后面加上自己创建的动态库地址
  3. ldconfig 配置/etc/ld.so.conf.d/,创建一个文件里面存储动态库的路径,然后ldconfig命令进行更新
  4. 在/lib64下建立自己定义的动态库软链接

动静态库加载细节:
静态库:静态库是在编译的时候加载进内存,然后把静态库拷贝到进程的进程地址空间的代码段,未来这部分代码,必须通过相对确定的地址位置进行访问

动态库:动态库在编译期间把每段代码的偏移量写入到我们的可执行程序中!而调用这段代码有相对地址是不够的,我们还需要起始地址,当我们运行的时候,动态库加载进内存,并把动态库的起始地址写入进程地址空间的共享区,然后进程就可以通过起始地址+偏移量来调用这段代码了。
在这里插入图片描述

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值