基础IO详解

发布于:2025-08-29 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、IO相关操作

文件操作本质就是 进程 和 被打开文件 的关系

1.C语言的文件操作

文件打开方式:

r 只读
r+ 读写
w 只写(会清空之前的内容)
w+ 读写
a 追加(在之前的内容后追加)
a+ 读写

文件操作相关函数:

int fgetc(FILE *stream)

int fputc(int c, FILE *stream)

char *fgets(char *str, int n, FILE *stream)

int fputs(const char *str, FILE *stream)

int fscanf(FILE *stream, const char *format, ...)

int fprintf(FILE *stream, const char *format, ...)

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream)

size_t fread(void *ptr, size_t size, size_t count, FILE *stream)

文件指针相关:

int fseek(FILE *stream, long offset, int whence)

long ftell(FILE *stream)

void rewind(FILE *stream)

int feof(FILE *stream)

写文件:

#include <stdio.h>
#include <string.h>
int main()
{
     FILE *fp = fopen("myfile", "w");
     if(!fp)
     {
         printf("fopen error!\n");
     }
 
     const char *msg = "hello bit!\n";
 
     int count = 5;
     while(count--)
     {
         fwrite(msg, strlen(msg), 1, fp);
     }
 
     fclose(fp);
     return 0;
}

读文件:

#include <stdio.h>
#include <string.h>
int main()
{
    FILE *fp = fopen("myfile", "r");
    if(!fp)
    {    
         printf("fopen error!\n");
    }
     char buf[1024];
     const char *msg = "hello bit!\n";
 
     while(1)
     {
        //注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
         size_t s = fread(buf, 1, strlen(msg), fp);
         if(s > 0)
         {
             buf[s] = 0;
             printf("%s", buf);
         }
 
         if(feof(fp))
             break;
 
     }
     fclose(fp);
     return 0;
}

2.文件的操作路径(当前路径)

无论读还是写,都需要文件的路径,以上代码都没有具体的路径,全是文件名,却在当前目录下创建或读取文件,这是为什么呢?

因为进程默认会在 当前路径 下操作,即 当前进程的工作目录 (默认为 进程当前“所在”的目录

修改当前进程的工作目录:

在程序中调用chdir()函数

#include <unistd.h>
       int chdir(const char *path);

3.创建的文件权限

创建的文件权限与 默认权限(mask)和umask 有关

新建文件默认权限=0666

umask=超级用户默认为0022,普通用户默认为0002

实际创建的出来的文件权限:

mask & ~umask(按位与)

直观理解:想要去掉的权限,其umask二进制位为1

修改umask值:程序内调用umask()函数

注:修改只针对当前子进程有效

#include <sys/types.h>
#include <sys/stat.h>

mode_t umask(mode_t mask);

4.标准流

C语言会默认打开三个输入输出流:stdin(标准输入), stdout(标准输出), stderr(标准错误)

 #include <stdio.h>
 extern FILE *stdin;
 extern FILE *stdout;
 extern FILE *stderr;

它们都是FILE*,文件类型指针,被操作系统当作文件管理

一般来说,stdin和stderr就是显示器,stdout就是键盘

后面还会提到他们...

举例:输出内容到显示器:

#include <stdio.h>
#include <string.h>
int main()
{
     const char *msg = "hello fwrite\n";
     fwrite(msg, strlen(msg), 1, stdout);
     printf("hello printf\n");
     fprintf(stdout, "hello fprintf\n");
     return 0;
}

二、系统IO

由于文件在磁盘中,而磁盘属于硬件,所以想要操作文件就必须使用系统调用接口

C语言,C++,以及其他语言的IO接口本质上就是去封装系统的IO接口

上面的 fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数(libc)

而下面要说的 open close read write lseek函数 都属于系统提供的接口,称之为系统调用接口 

1.文件描述符fd(file descriptor)

1)文件描述符的概念

当我们打开文件时,操作系统在内存中要创建file_struct来描述目标文件

每个进程的task_struct中都有一个指针*files, 指向一张表files_struct, 该表最重要的部分就是包含一个指针数组fd_array[],每个元素都是一个指向打开文件的指针

所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符的本质,就是数组下标!

2)文件描述符的分配规则

在 files_struct 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

但是当我们创建文件时,fd默认从3开始分配,这是因为0,1,2被占用了

如果关闭了0/1/2,fd也会将其重新分配

0:标准输入 stdin

1:标准输出 stdout

2:标准错误 stderr

3)举例

直接打开文件:此时创建的文件fd=3

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
 
int main()
{
     int fd = open("myfile.txt", O_WRONLY|O_CREAT|O_TRUNC);
     if(fd < 0)
     {
         perror("open");
         return 1;
     }
 
     printf("fd: %d\n", fd);
 
     close(fd);
     return 0;
}

关闭fd=0的文件(stdin),再打开文件:此时创建的文件fd=0

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
 
int main()
{
     close(0);
     int fd = open("myfile.txt", O_WRONLY|O_CREAT|O_TRUNC);
     if(fd < 0)
     {
         perror("open");
         return 1;
     }
 
     printf("fd: %d\n", fd);
 
     close(fd);
     return 0;
}

2.IO相关的系统调用函数

1)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);

a.参数:

pathname:

表示要打开的文件及路径(只有文件名则默认为当前路径)

flags:

表示打开方式

O_RDONLY 只读打开         O_WRONLY 只写打开         O_RDWR  可读可写打开

下列常数是可选择的:

O_CREAT 若文件不存在则创建它。使用此选项时,需要同时说明第三个参数mode,用其说明该新文件的权限。

O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错。       

O_APPEND 每次写时都加到文件的尾端,不会覆盖原先内容。

O_TRUNC 打开文件时,直接清空文件内部数据

这些flags本质都是宏,通过 特定的比特位为1,其余比特位为0 实现

可利用 位或运算符 同时使用多个选项

例如:

#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)

ONE|TWO|THREE

与C语言选项对比:

r O_RDONLY
w O_WRONLY|O_CREAT|O_TRUNC
a O_WRONLY|O_CREAT|O_APPEND

mode:

表示所创建文件的默认权限(mask),但是文件的最终权限是 mask&~umask (umask默认为0002)

只有在flags中使用了O_CREAT标志,才需要使用mode选项

b.返回值:

所打开文件的文件描述符fd

c.举例:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

//r只读
int fd = open("log.txt",O_RDONLY);
//w只写(增加功能:没有就创建新文件+清空数据)
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
//a追加(增加功能:没有就创建新文件)
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);

2)write和read函数

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

a.参数:

fd:

要 被写入/读出 的文件所对应的文件描述符

buf:

无类型的指针buf,可以存放要 写/读 的内容

可以看出,要 写/读 的内容不管是文本文件,还是其他种类文件,操作系统都会当作二进制类型处理

注:在输出字符串时,没必要将"\0"输出,因为"\0"在C语言中作为字符串结尾标志并且会被处理为二进制乱码输入文件中;在读入字符串时,需要加上"\0"

count:

表示 输出/输入 数据的最大字节数

b.返回值

表示 输出/输入 数据的实际字节数

c.举例

char outBuffer[] = {"hello\n"};
//不会将"\0"写入
write(1, outBuffer, strlen(outBuffer));

char inBuffer[64];
ssize_t num = read(0, inBuffer, sizeof(inBuffer)-1);
//在字符串末尾加上"\0"
if(num > 0) inBuffer[num] = 0;

3)close函数

#include <unistd.h>
int close(int fd);

参数:fd:文件描述符

返回值:成功返回0,失败返回-1

3.重定向

1)重定向概念

重定向是指将命令的 输入 或 输出 从默认的标准位置(如终端)重定向到其他位置(如文件或其他设备)

2)重定向本质

通过操作 文件描述符fd 来实现输入输出的重定向,上层用的fd不变,更改对应fd所指向的文件

那么,关闭fd=0/1/2所指向的文件,使其指向其他文件,即可实现重定向

3)重定向举例

关闭fd=1文件,打开新文件,fd=1指向新文件,完成重定向

必要说明:

printf 是 C 库当中的 IO 函数,一般往 stdout 中输出,但是 stdout 是文件类型指针FILE*,在底层访问文件的时候,找的还是 fd=1的文件。一般情况下,fd=1指向的文件是显示器,但此时, fd=1指向的文件是lg.txt。

所以,向显示器输出的任何消息都会写入到文件log.txt中,进而完成输出重定向

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
 
int main()
{
     //关闭了显示屏
     close(1);
     //将fd=1重新分配给文件log.txt
     int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
     if(fd < 0)
     {
         perror("open");
         return 1;
     }
     //原本应该输出到显示屏,现在输出到log.txt
     printf("fd: %d\n", fd);
     //刷新缓冲区
     fflush(stdout);
     close(fd);
     return 0;
}

4)重定向的方法

命令行中:

>操作符:输出重定向(覆盖写入)

>>操作符:输出重定向(追加写入)

<操作符:输入重定向(从文件中读取)

C程序中:

方式一:先关闭fd=0/1/2的文件,再创建新文件

//关闭了显示屏
close(1);
//将fd=1重新分配给文件log.txt
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

方式二:系统调用:dup2函数

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup2函数将oldfd中的地址拷贝给newfd,newfd原本指向标准位置,现在和oldfd同时指向新文件,同时会关闭newfd原来指向的文件

举例:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
 
int main()
{
    int fd = open("redirect.txt", O_WRONLY|O_TRUNC|O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 0;
    }
    dup2(fd, 1);
    printf("fd: %d\n", fd);
    return 0;
}

5)子进程重定向

子进程重定向不影响父进程

原因:子进程会拷贝files_sturct,包含了fd与文件对应关系的表,在其中进行修改

举例:子进程输出重定向

6)模拟shell中实现重定向功能

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>
 
#define NUM 1024
#define OPTION_NUM 64

//支持重定向命令
#define NONE_REDIR   0//无重定向
#define INPUT_REDIR  1//输入重定向
#define OUTPUT_REDIR 2//输出重定向
#define APPEND_REDIR 3//追加重定向
//宏函数:跳过命令中的空格
#define skipSpace(start) do{while(isspace(*start)) ++start;}while(0)

char lineCommand[NUM];//存放整个命令行
char* myargv[OPTION_NUM];//存放切割后的命令行参数
int status = 0;

const char* UserName()
{
    const char* name = getenv("USER");
    if(name)
        return name;
    else
        return "None";
}
 
 
const char* HostName()
{
    const char* name = getenv("HOSTNAME");
    if(name)
        return name;
    else
        return "None";
}
 

const char* CurrentWorkDir()
{
    const char* name = getenv("PWD");
    if(name)
        return name;
    else
        return "None";
}

//存放重定向类型
int redirType = NONE_REDIR;
//存放重定向命令中的文件名
char* redirFile = NULL;

//解析重定向命令
//"ls > log.txt"->"ls" "log.txt"
void commandCheck(char* commands)
{
    assert(commands);
    char* start = commands;
    char* end = commands + strlen(commands);

    while(start < end)
    {
        if(*start == '>')
        {
            *start = '\0';
            start++;
            //追加重定向
            if(*start == '>')
            {
                redirType = APPEND_REDIR;
                start++;
            }
            //输出重定向
            else
            {
                redirType = OUTPUT_REDIR;
            }
            skipSpace(start);
            redirFile = start;
            break;
            
        }
        //输入重定向
        else if(*start == '<')
        {
            *start = '\0';
            start++;
            skipSpace(start);
            //填写重定向信息
            redirType = INPUT_REDIR;
            redirFile = start;
            break;
        }
        else
        {
            start++;
        }
    }
}
 
int main()
{
    while(1)
    {
        //输出提示符
        printf("[%s@%s %s]$", UserName(), HostName(), CurrentWorkDir());
        fflush(stdout);//刷新缓冲区
        
        //获取用户输入
        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        s = NULL;//防止野指针
 
        //消除用户最后输入的\n
        lineCommand[strlen(lineCommand)-1] = 0;
        
        //重定向准备工作
        //初始化
        redirType = NONE_REDIR;
        redirFile = NULL;
        errno = 0;  
        //拆分命令
        commandCheck(lineCommand);        

        //切割命令行字符串,使其变为多个命令行参数
        myargv[0] = strtok(lineCommand, " ");//以空格为分隔符切割
        int i = 1;
        //如果没有可以分割的子串了,strtok()返回NULL,此时myargv[end]=NULL
        while(myargv[i++] = strtok(NULL, " "));
        
        //执行内置命令,如cd,echo命令
        //执行cd命令
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL)
            {
                chdir(myargv[1]);//直接改变父程序的工作路径
                continue;//无需再创建子进程执行命令
            }
        }
        //执行echo命令
        if(myargv[0] != NULL && myargv[1] !=NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d\n",(status>>8) & 0xFF);
            }
            else
            {
                printf("%s\n",myargv[1]);
            }
            continue;//无需再创建子进程执行命令
        }
 
        //创建子进程执行命令
        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);
                        if(fd < 0)
                        {
                            perror("open");
                            exit(errno);
                        }
                        dup2(fd, 0);
                        break;
                    }
                case OUTPUT_REDIR:
                case APPEND_REDIR:
                    {
                        int flags = O_WRONLY | O_CREAT;
                        if(redirType == APPEND_REDIR) flags |= O_APPEND;
                        else flags |= O_TRUNC;
                        int fd = open(redirFile, flags, 0666);
                        if(fd < 0)
                        {
                            perror("open");
                            exit(errno);
                        }
                         dup2(fd,1);
                         break;
                    }
                default:
                    printf("bug\n");
                    break;
            }
            execvp(myargv[0], myargv);
            exit(1);
        }
        //阻塞等待,获取子进程的退出结果
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
    }
}

4.缓冲区

1)缓冲区的作用

a.缓冲区作为临时中转站,程序可快速将数据写入缓冲区后继续执行,从而减少CPU中断次数

b.批量处理数据

2)缓冲区的意义

节省进程进行数据IO的时间

3)缓冲区的位置

用户级缓冲区:

C语言中,每个打开文件都有对应的FILE结构体,它里面有对应打开文件的fd,而打开文件的缓冲区也在FILE中

所以,该缓冲区在封装的C语言库中,属于语言层面

内核级缓冲区:在操作系统中

4)缓冲区的刷新策略

a.用户级缓冲区(库函数中):

1.立即刷新——无缓冲(用户强制刷新:fflush函数;进程退出)

2.行刷新——行缓存(向显示器写入)

3.缓冲区满——全缓冲(向磁盘文件写入)

强制刷新函数:

#include <stdio.h>
int fflush(FILE *stream);

b.内核级缓冲区(操作系统中):

操作系统自主决定

强制刷新函数:

 #include <unistd.h>
 int fsync(int fd);

5)子进程的缓冲区

输出举例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
int main()
{
    //验证C语言中的缓存区
    //C语言的IO函数
    
    const char* s = "fprintf\n";
    const char* S = "fwrite\n";
 
    fprintf(stdout, "%s", s);
    fwrite(S, strlen(S), 1, stdout);
 
    //系统调用
    const char* c = "write\n";
    write(stdout->_fileno/*fd*/, c, strlen(c));
 
    fork();
 
    return 0;
}

向显示器打印:

向文件中打印:

现象:C语言的函数都输出了两次;而系统调用的函数只输出了一次

原因:

C语言封装的缓冲区写入显示器时采用行缓存。在创建子进程之前,数据就已经打印到显示器上,FILE内的缓冲区中没有数据;

C语言封装的缓冲区写入文件时采用全缓冲。子进程创建后,数据仍在FILE内的缓冲区中,紧接着进程退出就会刷新缓冲区,此时,父子进程的任意一个退出都会刷新缓冲区,触发写时拷贝,后退出的进程会再次刷新其所拷贝的缓冲区,所以输出了两次。

而系统调用函数没有经过C语言封装的缓冲区,所以只输出了一次。

6)模拟C语言封装系统调用,并设置缓冲区

myStdio.h

#pragma once

#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE 1024
//刷新方式
#define SYNC_NOW  (1<<0)//立即刷新
#define SYNC_LINE (1<<1)//行缓存
#define SYNC_FULL (1<<2)//全缓冲

typedef struct _FILE
{
    int flags;//刷新方式
    int fileno;//fd
    int cap;//buffer的总容量
    int size;//buffer的当前使用量
    char buffer[SIZE];//缓冲区
}FILE_;

FILE_* fopen_(const char* path_name, const char* mode);
void fwrite_(FILE_* fp, const void* ptr, int num);
void fclose_(FILE_* fp);
void fflush_(FILE_* fp);

myStdio.c

#include "myStdio.h"
FILE_ *fopen_(const char* path_name, const char* mode)
{
    int flags = 0;
    int defaultMode = 0666;
    if(strcmp(mode, "r") == 0)
    {
        flags |= O_RDONLY;
    }
    else if(strcmp(mode, "w") == 0)
    {
        flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flags |= (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);

    //打开失败
    if(fd < 0)
    {
        const char* err = strerror(errno);
        write(2, err, strlen(err));
        return NULL;
    }
    
    FILE_* fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp);
    
    //初始化FILE_
    fp->flags = SYNC_LINE;//默认设置为行刷新
    fp->fileno = fd;
    fp->cap = SIZE;
    fp->size = 0;
    memset(fp->buffer, 0, SIZE);//返回FILE*指针

    return fp;
}

void fwrite_(FILE_* fp, const void* ptr, int num)
{
    //1.写入缓冲区
    memcpy(fp->buffer+fp->size, ptr, num);
    fp->size += num;

    //2.判断是否刷新
    if(fp->flags & SYNC_NOW)//立即刷新
    {
        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 if(fp->flags & SYNC_LINE)//行缓存
    {
        if(fp->buffer[fp->size-1] == '\n')
        {
            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);
}

测试文件main.c

#include "myStdio.h"
 
const char* filename = "./log.txt";
 
int main()
{
    FILE_ *fp = fopen_(filename, "w");
    if(fp == NULL) return 1;
 
    int cnt = 3;
    const char *msg = "hello world\n";
    while(cnt)
    {
        fwrite_(fp, msg, strlen(msg));
        sleep(1);
        cnt--;
    }
 
    fclose_(fp);
    return 0;
}

运行结果:

三、文件系统

文件系统是静态管理磁盘上没有被打开的文件

1.磁盘的结构

1)磁盘的物理结构

盘片:硬盘有多个盘片,每盘片2面
磁头:每面一个磁头

扇区:盘片被分为多个扇形区域,每个扇区存放512字节的数据,是硬盘的最小存储单位

磁道:同一盘片不同半径的同心圆,是由磁头在盘片表面划出的圆形轨迹
柱面:不同盘片相同半径构成的圆柱面,由同一半径圆的多个磁道组成

盘片旋转,磁头摆动,但是磁头和盘面没有接触

2)磁盘的存储结构

磁盘寻址的基本单位是扇区(512byte)

定位扇区的方法: CHS定位法:

定位柱面 => 定位磁头(一个磁头对应一个盘面) => 定位扇区

(柱面:Cylinder   磁头:Head     扇区:Sector)

1.选择柱面:将磁头移动到指定的柱面(磁道)。

2.选择磁头:选择对应的磁头,即选择哪个盘面的数据。

3.选择扇区:通过磁盘的旋转找到指定磁道上的目标扇区。

3)磁盘的逻辑结构

a.概念

磁盘物理上是圆形的,逻辑上看作一个线性数组。所以,管理磁盘就是管理数组

数组的最小单位是扇区(512byte),一般来说,扇区在每一个盘面和磁道上的数量相同,所存的数据量也相同

扇区对应的 数组下标 就是 LBA地址(Logical Block Addressing)

如图(数据虚构):

b.将 LBA逻辑地址 转化为 CHS物理地址:

根据一个扇区的逻辑下标n,可以做出以下计算:

n  /  一个盘面的扇区数   算出   位于第几个盘面;

n  %  一个盘面的扇区数   算出    目标位于单盘上的位置(临时数据,记作temp);

temp  /   一个磁道的扇区数   算出   位于第几个磁道(柱面);

temp  %   一个磁道的扇区数   算出    位于第几个扇区。   

举例(数据虚构):

c.为什么操作系统要逻辑抽象用LBA地址,不直接用CHS地址呢?

便于管理;不让OS的代码和硬件强耦合

4)磁盘的读取/修改

磁盘访问的基本单位是扇区(512byte),但是磁盘如果一次只读取/修改一个扇区,数据量就太小了,效率低。

所以,操作系统 读取/修改 一次磁盘的基本单位一般是4KB(8个扇区)。也就是说,即使只读取/修改1bit的数据,也要将4KB大小的磁盘数据加载到内存中,再进行读取/修改

所以,内存被分成多个4KB大小的空间(页框);磁盘中的文件尤其是可执行文件被划分成多个4KB大小的块(页帧),方便分块加载到内存中

2.磁盘的管理

分治思想:将一个大空间拆成若干个小空间,对小空间的进行管理;再把小空间的管理模式应用到其他空间。

如图(数据虚构):

磁盘文件系统图(数据虚构):

Boot Block:存储 分区表、操作系统等 开机就加载的文件

Super Block(超级块):存放文件系统本身(对应分区)的结构信息
  记录的信息主要有:bolck 和 inode 的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。

  通常在一个分区内多个分组共有一个超级块,假设一个100G的分区中有1000个分组,每20个分组共有一个super block,那么总共就有50个超级块。

       为什么需要这些超级块呢?是为了数据备份,如果某个块组或者inode丢失,那么就可以通过super block来进行恢复。
 

文件=内容+属性,Linux下的内容和属性是分开存储的。

  • 文件属性存储在Inode中,Inode大小固定,一个文件对应一个Inode。一个文件的所有属性几乎都存储在Inode中,但是文件名并不存储在Inode中
  • 文件的内容存储在data  block数据块中,数据块随着应用类型的变化,大小也会发生变化

Inode:

Inode是一个结构体,大小一般为128字节

Inode在内核中的大致结构:

struct inode
{
    类型
    大小
    时间
    ....
}

Inode在内核中的实际结构:

struct inode {
    umode_t         i_mode;//文件的访问权限(eg:rwxrwxrwx)
    unsigned short      i_opflags;
    kuid_t          i_uid;//inode拥有者id
    kgid_t          i_gid;//inode拥有者组id
    unsigned int        i_flags;//inode标志,可以是S_SYNC,S_NOATIME,S_DIRSYNC等
 
#ifdef CONFIG_FS_POSIX_ACL
    struct posix_acl    *i_acl;
    struct posix_acl    *i_default_acl;
#endif
 
    const struct inode_operations   *i_op;//inode操作
    struct super_block  *i_sb;//所属的超级快
    /*
        address_space并不代表某个地址空间,而是用于描述页高速缓存中的页面的一个文件对应一个address_space,一个address_space与一个偏移量能够确定一个一个也高速缓存中的页面。i_mapping通常指向i_data,不过两者是有区别的,i_mapping表示应该向谁请求页面,i_data表示被改inode读写的页面。
    */
    struct address_space    *i_mapping;
 
#ifdef CONFIG_SECURITY
    void            *i_security;
#endif
 
    /* Stat data, not accessed from path walking */
    unsigned long       i_ino;//inode号
    /*
     * Filesystems may only read i_nlink directly.  They shall use the
     * following functions for modification:
     *
     *    (set|clear|inc|drop)_nlink
     *    inode_(inc|dec)_link_count
     */
    union {
        const unsigned int i_nlink;//硬链接个数
        unsigned int __i_nlink;
    };
    dev_t           i_rdev;//如果inode代表设备,i_rdev表示该设备的设备号
    loff_t          i_size;//文件大小
    struct timespec     i_atime;//最近一次访问文件的时间
    struct timespec     i_mtime;//最近一次修改文件的时间
    struct timespec     i_ctime;//最近一次修改inode的时间
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    unsigned short          i_bytes;//文件中位于最后一个块的字节数
    unsigned int        i_blkbits;//以bit为单位的块的大小
    blkcnt_t        i_blocks;//文件使用块的数目
 
#ifdef __NEED_I_SIZE_ORDERED
    seqcount_t      i_size_seqcount;//对i_size进行串行计数
#endif
 
    /* Misc */
    unsigned long       i_state;//inode状态,可以是I_NEW,I_LOCK,I_FREEING等
    struct mutex        i_mutex;//保护inode的互斥锁
 
    //inode第一次为脏的时间 以jiffies为单位
    unsigned long       dirtied_when;   /* jiffies of first dirtying */
 
    struct hlist_node   i_hash;//散列表
    struct list_head    i_wb_list;  /* backing dev IO list */
    struct list_head    i_lru;      /* inode LRU list */
    struct list_head    i_sb_list;//超级块链表
    union {
        struct hlist_head   i_dentry;//所有引用该inode的目录项形成的链表
        struct rcu_head     i_rcu;
    };
    u64         i_version;//版本号 inode每次修改后递增
    atomic_t        i_count;//引用计数
    atomic_t        i_dio_count;
    atomic_t        i_writecount;//记录有多少个进程以可写的方式打开此文件
    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
    struct file_lock    *i_flock;//文件锁链表
    struct address_space    i_data;
#ifdef CONFIG_QUOTA
    struct dquot        *i_dquot[MAXQUOTAS];//inode磁盘限额
#endif
    /*
        公用同一个驱动的设备形成链表,比如字符设备,在open时,会根据i_rdev字段查找相应的驱动程序,并使i_cdev字段指向找到的cdev,然后inode添加到struct cdev中的list字段形成的链表中
    */
    struct list_head    i_devices;,
    union {
        struct pipe_inode_info  *i_pipe;//如果文件是一个管道则使用i_pipe
        struct block_device *i_bdev;//如果文件是一个块设备则使用i_bdev
        struct cdev     *i_cdev;//如果文件是一个字符设备这使用i_cdev
    };
 
    __u32           i_generation;
 
#ifdef CONFIG_FSNOTIFY
   //目录通知事件掩码
    __u32           i_fsnotify_mask; /* all events this inode cares about */
    struct hlist_head   i_fsnotify_marks;
#endif
 
#ifdef CONFIG_IMA
    atomic_t        i_readcount; /* struct files open RO */
#endif
    //存储文件系统或者设备的私有信息
    void            *i_private; /* fs or device private pointer */
};

Inode编号:

由于每个文件都有Inode,为了区分彼此,每个Inode都有自己的编号。编号是以每个分区为单位进行编的。

查看Inode编号:ll -i 命令

inode table:保存一个分组内部所有可用的(已经使用+没有使用)的inode。

Data blocks:保存分组内部所有文件的数据块(一个数据块大小为4KB)。

Inode Bitmap:

Inode对应的位图结构。用于查看每个inode的使用情况:0表示未使用;1表示已使用。

假设inode一共有n个,位图结构中的比特位的个数至少也为n个。位图中比特位的位置与当前文件的inode的ID是一一对应的。

block Bitmap:

数据块对应的位图结构。用于查看每个数据块的使用情况。位图中比特位的位置和当前data block对应的数据块的位置是一一对应的。

Group Descriptor Table:

包含对应分组的宏观属性信息,包括:一共有多少个数据块,使用了多少;一共有多少个Inode,使用了多少等等。 

3.在磁盘中查看文件

通过 Inode编号 查找文件

查看文件属性:

  • 通过 Inode Bitmap 查找对应的比特位的位置是1还是0
  • 如果这个编号被占用,在Inode Table中找到对应Inode结构体,然后确定一下是否是我们要查找的文件。

查看文件内容:

查看文件内容就需要找到文件的数据块

根据文件的 inode编号 找到对应的 inode结构体 ,inode结构体中有一个数组int  block[15](数组元素的个数是固定的15)用于存放该文件数据块的编号,然后就能找到文件的数据块,查看文件内容

关于int block[15]数组:

虽然int block[15]数组只有15个元素,但并不代表文件仅可以使用15个数据块。

数组中元素所指向的数据块中的内容:

  • [0, 11]:直接存放文件数据
  • [12,13]:其他数据块编号,指向存放文件数据的数据块,是一种二级索引
  • [14]: 其他数据块编号,而且其指向的数据块还是存着其他数据块编号,是三级索引

如此,就可以增加文件可使用数据块的个数。

如图:

4.在磁盘中删除文件

惰性删除:

将要删除文件的 Inode编号 在Inode Bitmap中对应的比特位由1置为0;将要删除文件的 所用数据块 在block bitmap中对应的比特位由1置为0。

这里并没有直接删除数据块,所以Linux下删除是可以恢复的。

5.文件名与Inode的关系

所有文件都在目录下,而目录的数据块中存放了当前目录下文件名与Inode编号的映射关系

所以,在查找文件的时候,只需要文件名,操作系统就会自动访问目录的数据块,找到对应的Inode编号,进行查找

四、软链接和硬链接

1.软链接

1)软链接的概念

软链接(Symbolic Link)也称为 符号链接 ,类似于 Windows 系统中的快捷方式。它是一个独立的文件,其内容是指向目标文件或目录的路径。通过软链接,我们可以在不同的位置访问同一个文件或目录,而无需复制文件。

2)创建软链接

ln(link) -s 命令:其中 -s 表示创建符号链接

ln -s <目标文件或目录> <软链接名称>

举例:

3)软链接的特性

  • 独立性:软链接是一个独立的文件,具有自己的 inode 编号。
  • 路径依赖:软链接依赖于目标文件的路径。如果目标文件被移动或删除,软链接将失效。

举例:

删除目标文件后,软链接将失效。

可以看到,file_soft.link 仍然存在,但它已经失效,因为目标文件 file.txt 被删除了。

4)软链接的用途

  • 路径重定向:将文件或目录的访问路径重定向到另一个位置。
  • 简化路径:通过软链接简化复杂路径,方便访问。
  • 文件共享:在不同目录中共享同一个文件。

2.硬链接

1)硬链接的概念

硬链接(Hard Link)是指多个文件名指向同一个 inode(索引节点)。硬链接不是独立的文件,它与目标文件共享同一个 inode,因此它们实际上是同一个文件的不同入口。

硬链接实际上就是增加了 文件名与Inode编号的映射关系

2)创建硬链接

ln 命令:

ln <目标文件> <硬链接名称>

举例:

从输出中可以看到,file_hard.link 和 file.txt 的硬链接数都是 2,这表明它们共享同一个 inode。

3)硬链接的特性

  • 共享 inode:硬链接与目标文件共享同一个 inode,因此它们实际上是同一个文件。
  • 路径独立:硬链接不依赖于目标文件的路径,即使目标文件被重命名或移动,硬链接仍然有效。
  • 删除行为:删除一个硬链接不会影响其他硬链接或目标文件,只有当所有硬链接都被删除时,文件才会被真正删除。

举例:

删除一个硬链接,目标文件仍然存在。

通过Inode中的引用计数实现

可以看到,file.txt 仍然存在,只是硬链接数减少到 1。

4)硬链接的用途

  •  文件备份:通过硬链接创建文件的备份,节省磁盘空间。
  •  文件共享:在不同目录中共享同一个文件。
  •  系统文件管理:用于系统文件的冗余备份和管理

5)目录硬链接的特殊性

  

目录结构解析:

  •  . 代表当前目录
  •  .. 代表父目录
  • 当前目录的 . 会增加当前目录的硬链接数
  •  每个子目录的..都会增加父目录的硬链接数

  

系统限制 用户创建目录硬链接 的目的:

禁止用户创建目录硬链接是为了防止出现循环引用,避免文件系统陷入死循环

3.软链接与硬链接的区别

1)inode 编号

  •  软链接:具有独立的 inode 编号。
  •  硬链接:与目标文件共享同一个 inode 编号。

  

可以看到,file_soft.link 有独立的 inode 编号(67158696),而 file_hard.link 与 file.txt 共享同一个inode编号(67158695)

2)路径依赖

  •   软链接:依赖于目标文件的路径,路径改变会导致软链接失效。
  •  硬链接:不依赖于目标文件的路径,即使目标文件被重命名或移动,硬链接仍然有效。 

举例:

移动目标文件后,软链接失效,硬链接仍然有效。

  

可以看到,file_soft.link 已经失效,而 file_hard.link 仍然有效。

3)删除行为

  •  软链接:删除软链接不会影响目标文件。
  •  硬链接:删除一个硬链接不会影响其他硬链接或目标文件,只有当所有硬链接都被删除时,文件才会被真正删除。

举例:

删除目标文件后,硬链接仍然有效。

    

五、文件的三个时间

当我们在linux中创建了文件或文件夹,文件/文件夹就有了时间属性,而且linux中的文件具有三个时间,

可以通过stat命令查看文件的三种时间:

  • 访问时间(Access time):对文件进行一次读操作,它的访问时间就会改变。
  • 修改时间(Modify time):文件内容最后一次修改的时间。
  • 改变时间(Change time):文件属性最后一次被修改的时间。

    

1.Access time

文件的访问时间,但不是每次读文件后都会改变,而是读文件的次数积累到一定数量后才会改变。

这是因为读文件的次数在实际情况中较多,这样可以提高效率

2.Modify time

文件内容最后一次修改的时间

举例:

  

change time 也发生了变化,因为文件大小的改变也是文件属性的改变

3.Change time

文件属性最后一次修改的时间

  

六、静态库与动态库

1.库的概念

  • 静态库(.a):程序在编译链接的时候把库的代码拷贝到可执行文件中,运行的时候将不再需要静态库。
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

命名规则:

去掉开头lib,去掉.so或者.a后的内容

例如:"llibc.so.6"=>"c"---C标准库

2.库的本质和意义

将含有函数实现方法的.c文件编译成.o文件(不包含主函数),打包后就形成了库。

使用时,将 库文件(函数实现)和头文件(函数目录) 提供给使用者,只需包含对应的头文件,并链接库中的.o文件即可。

这样就可以使用库函数的功能,提高效率。

3.库的制作和使用

举例:

my_add.h

#pragma once
#include <stdio.h>

extern int Add(int a, int b);

my_add.c

#include "my_add.h"

int Add(int a,int b)
{
    printf("enter Add func, %d + %d = ?\n", a, b);
    return a + b;
}

my_sub.h

#pragma once
#include <stdio.h>

extern int Sub(int a, int b);

my_sub.c

#include "my_sub.h"

int Sub(int a,int b)
{
    printf("enter Sub func, %d - %d = ?\n", a, b);
    return a - b;
}

main.c

#include "my_add.h"
#include "my_sub.h"

int main()
{
    int ret1 = Add(10,20);
    printf("result:%d\n",ret1);
    int ret2 = Sub(10,20);
    printf("result:%d\n",ret2);

    return 0;
}

1)不使用库

情况一:直接形成可执行文件

gcc会自动将.c文件编译为.o文件,并进行链接

gcc -o mymath main.c my_add.c my_sub.c

情况二:先编译形成.o文件,再进行链接形成可执行文件

与直接形成可执行文件相比,只是先手动将文件编译,并无本质区别,是为后面理解库做铺垫。

gcc -c main.c
gcc -c my_add.c
gcc -c my_sub.c
gcc -o mymath main.o my_add.o my_sub.o

2)静态库的制作和使用

a.制作静态库

(1)先将.c文件编译为.o文件(需要在当前目录下包含对应头文件)

gcc -c my_add.c
gcc -c my_sub.c

(2)将.o文件打包,形成库

ar -rc libmymath.a my_add.o my_sub.o

ar(archive)是gnu归档工具,rc(replace and create)表示替换或创建,.a表示生成的是静态库。

file命令:查出 libmymath.a 是归档文件

(3)整理库,准备交付

交付库本质就是提供 库文件(.a/.so)和匹配的头文件

b.使用静态库
情况一:安装静态库到任意路径

(1)安装静态库到任意路径

安装本质就是拷贝

(2)编译链接形成可执行文件

gcc -o mymath main.c -I ./mylib/include -L ./mylib/lib -l mymath
  • -I 指定头文件所在路径
  • -L 指定库的路径
  • -l 指定库名称
  • (使用这三个选项时,后面的空格可以省略)

要链接第三方库,必须指明库名称(库名称要去掉开头lib,去掉.so/.a

1.为什么使用库需要指明库名称?

因为一个路径下可能有多个库

2.为什么使用头文件不需要指明名称?

在main.c中已经指明了要用到的头文件

3.为什么在平时形成可执行文件时,不需要指明库名称?

在使用gcc/g++工具时,只使用了C/C++的标准库,默认能够找到

情况二:安装静态库到系统路径

(1)安装静态库到系统路径

  • 将头文件安装到:/usr/include/
  • 将库文件安装到:/lib64/

(2)编译链接形成可执行文件

gcc -o mymath main.c -l mymath

由于头文件和库文件都在系统路径下,所以不用指明路径,但还是需要指明库名称

3)动态库的制作和使用

a.制作动态库

(1)先将.c文件编译为.o文件(需要在当前目录下包含对应头文件)

与静态库制作相比,形成.o文件多了-fPIC选项

fPIC(position independent code):产生位置无关码(后面原理时会讲解)

gcc -c -fPIC my_add.c
gcc -c -fPIC my_sub.c

(2)将.o文件打包,形成库

与静态库制作相比,形成库文件时多了-shared选项

gcc -shared -o libmymath.so my_add.o my_sub.o

(3)整理库,准备交付

b.使用动态库

(1)安装动态库到任意路径

(2)编译链接形成可执行文件

形成可执行文件时,与静态库的使用相同

gcc -o mymath main.c -I ./mylib/include -L ./mylib/lib -l mymath

(3)执行可执行文件

与静态库使用不同的是:此时,可执行文件并不能直接运行,因为没有找到动态库

为什么在编译的时候已经指明了库文件的路径和名称,还是无法使用动态库呢?

因为编译时这些信息是告诉了gcc工具,而执行程序的OS并不知道库在哪里

方法一:直接安装动态库到系统路径(永久有效)

  • 将头文件安装到:/usr/include/
  • 将库文件安装到:/lib64/

使用时与静态库方法相同

方法二:配置环境变量LD_LIBRARY_PATH(临时有效)

程序运行寻找动态库时,OS除了在系统默认的路径下搜索,也会在环境变量 LD_LIBRARY_PATH 中搜索

缺点:每次重新登录,这个环境变量会被刷新,需要重新配置

添加动态库路径到 环境变量LD_LIBRARY_PATH 中

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/wmx/project/lesson23/test/mylib/lib

正常运行:

方法三:配置/etc/ld.so.conf.d/目录(永久有效)

1.在/etc/ld.so.conf.d/目录中创建.conf文件,将动态库的路径写入.conf文件

cd /etc/ld.so.conf.d #切换到对应目录
sudo touch lesson23.conf #创建.conf文件
sudo vim lesson23.conf #将动态库的路径写入

2.使用ldconfig命令更新

sudo ldconfig

执行程序

方法四:创建软连接

程序运行寻找动态库时,会在当前路径下和系统路径(/lib64/)下搜索。所以,可以在 当前路径下/系统路径(/lib64/) 创建软链接

//当前路径下
ln -s /home/wmx/project/lesson23/test/mylib/lib/libmymath.so libmymath.so
//系统路径下
sudo ln -s /home/wmx/project/lesson23/test/mylib/lib/libmymath.so /lib64/libmymath.so

程序运行:

4)使用外部库

使用外部库时,OS会在系统路径下将头文件和库文件自动安装

4.静态链接与动态链接

对于一个特定的库,链接的类型取决于库的类型。

  • 如果只有静态库,使用静态连接;
  • 如果只有动态库,使用动态连接。
  • 当同时有静态库和动态库时,gcc默认使用动态链接;使用 -static 选项,进行静态链接。
gcc -o test test.c 
gcc -o test test.c -static

对于一个可执行程序,只要有一个动态链接的库,那就是动态链接。

5.动态库的加载

在形成可执行程序时,静态库的二进制文件就已经拷贝到程序的代码区中。所以,在程序运行时,不需要加载静态库,只需要加载动态库。

1)基地址(Base Address)和偏移地址(Offset Address)

虚拟地址:当程序编译后就会生成虚拟地址(函数地址,变量地址…),提供给CPU读取。CPU通过虚拟地址访问进程地址空间,然后通过页表映射,对物理内存进行访问。

  • 基地址是具体的虚拟地址,是一个绝对地址
  • 偏移地址是相对某个基地址的差值,是一个相对地址

2)编译后程序的函数虚拟地址

a.使用静态库时

当程序编译完成时,相应的静态库函数已经加载到代码中。此时, 库函数和自己实现的函数 的虚拟地址都是基地址。

b.使用动态库时

在形成.o文件时,使用了fPIC选项:产生位置无关码。所以,当程序编译完成时,会将动态库函数的偏移地址写入程序中。

这个偏移地址是函数代码在动态库中的偏移量。

3)动态库的加载

当程序运行到需要使用库函数时,会将动态库加载到物理内存中,再通过页表映射到进程地址空间的共享区中。此时,库的起始位置就确定了再通过库函数的偏移地址,就可以找到函数实现的代码。

如图:

6.静态库和动态库的区别

1)静态库

  1. libxxx.a格式命名
  2. 在程序编译的时候,直接将静态库继承到可执行程序中,因此可执行程序比较大
  3. 程序在运行的时候占用内存大,存储的时候占用磁盘大
  4. 可执行程序,脱离静态库后不受影响
  5. 程序的更新部署比较麻烦

2) 动态库

  1. libxxx.so格式命名
  2. 可执行程序编译的时候,不会连接到动态库,而是在程序执行的时候,将动态库加载到内存中。内存中最多只有一份。
  3. 因此,可执行程序比较小
  4. 占用内存和磁盘比较小
  5. 可执行程序,脱离动态库后会崩溃
  6. 程序更新部署比较方便
     

网站公告

今日签到

点亮在社区的每一天
去签到