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

Linux复习——基础IO,认识文件描述符、软硬件链接

1.复习C文件接口

1.1 fopen

FILE *fopen(const char *path, const char *mode);
 

path:带路径的文件名称(待打开的文件)
 
mode:
 
        r:以可读方式打开,不可写,文件不存在,则报错
 
        r+:以读写方式打开,文件不存在,则报错
 
        w:以可写方式打开,但是不能读。文件不存在,则创建。文件存在,则截断(清空文件内容)文件。
 
        w+:以读写方式打开。文件不存在,则创建。文件存在,则截断(清空文件内容)文件
 
        a:追加写,不能读。在文件末尾进行追加写,文件不存在则创建文件。文件存在,则在文件末尾开始写
 
        a+:可读追加写,在文件末尾进行追加写,文件不存在则创建文件。文件存在,则在文件的末尾进行追加写
 
返回值:文件流指针 FILE

1.2 fclose

int fclose(FILE *fp);
 
关闭一个文件流指针

1.3 fread

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
 

ptr:要将读到的保存到哪里去,ptr保存用户准备的一个空间(缓冲区)的地址
 
size:块的大小,单位是字节
 
nmemb:块的个数
 
eg:size = 2 nmemb = 5 ==> 10
 
常见的用法:size = 1,nmemb可以指定任意块,读到的字节数量也就可以用nmemb表示
 
stream:文件流指针,表示要从哪里进行读
 
返回值:返回读到的块的个数

1.4 fwrite

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

ptr:要写入到文件当中的内容
 
size:块的大小,单位字节
 
nmemb:块的个数
 
总写入的字节数量 = 块的大小 * 块的个数
 
stream:文件流指针,表示要写到哪里
 
返回值:写入块的个数

1.5 fseek

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

stream:文件流指针
 
offset:偏移量,单位字节。相对于whence而言的偏移量
 
whence:
 
        SEEK_SET:文件的头部
 
        SEEK_CUR:当前文件流指针的位置
 
        SEEK_END:文件的尾部

注意:我们向文件写入内容以后文件流指针会发生偏移,此时如果我们要从文件里读取内容,就需要通过fseek来设置流指针的位置不然从当前流指针的位置将读不到任何内容

2.系统文件I/O接口

2.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 flag,mode_t mode);

pathname :要打开的文件

flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags

参数:

        O_RDONLY:只读打开

        O_WRONLY:只写打开

        O_RDWR:读,写打开

                这三个常量,必须指定一个且只能指定一个

        O_CREAT:若文件不存在,则创建它,需要使用mode选项,来指明新文件的访问权限

        O_APPEND:追加写

返回值:成功:新打开的文件描述符      失败:-1

2.2 close

int close(int fd); 这里的fd就是open的返回值

2.3 read

ssize_t read(int fd, void *buf, size_t count);
 

fd:文件描述符
 
buf:将读到的内容放到buf这个缓冲区当中
 
count:表示最大能够读多少字节,和buf的大小有关,需要在buf当中预留一个‘\0’的位置
 
返回值:
 
 >0:读到的字节数量
 
 -1:读失败

2.4 write

ssize_t write(int fd, const void *buf, size_t count);

  • fd:文件描述符
     
  • buf:写到文件当中的内容
     
  • count:写入的字节数量
     
  • 返回值
     
     >0:表示写入文件真实的字节数量
     
      -1:写入失败

2.5 lseek

off_t lseek(int fd, off_t offset, int whence);
 

  • fd:文件描述符
     
  • offset:偏移量
     
  • whence
     
    SEEK_SET:文件的头部
     
    SEEK_CUR:当前文件流指针的位置
     
    SEEK_END:文件的尾部

3.文件描述符

3.1 open函数返回值

        open的返回值其实就是这里要介绍的文件描述符,但是在正式认识他之前我们先来认识两个概念:系统调用和库函数

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

        而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

        回忆一下我们讲操作系统概念时,画的一张图

        通过上图我们可以清晰的看到其实f#系列函数,都是对系统调用的封装,方便二次开发

3.2 文件描述符fd

        文件描述符其实就是一个小整数

        linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2

        0,1,2对应的物理设备一般是:键盘、显示器、显示器,所以输入输出还可以采用如下方式

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main()
{
    char buf[1024];
    ssize_t s = read(0, buf, sizeof(buf));
    if (s > 0)
    {
        buf[s] = 0;
        write(1, buf, strlen(buf));
        write(2, buf, strlen(buf));
    }
    return 0;
}

        现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是有了file结构体,其表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来,每一个进程都有一个指针*file,指向一张file_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以本质上,文件描述符就是数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

3.3 文件描述符分配规则

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.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>
#include <unistd.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数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

3.4 重定向

        在3.3中我们了解了文件描述符的分配规则,如果我们将标准输出1关闭,那么printf还会打印在屏幕吗?下面进行测试

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.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,这种现象叫做输出重定向。常见的重定向有 >,>>,<

3.5 使用dup2系统调用

#include <unistd.h>
int dup2(int oldfd, int newfd);     //将原来写到newfd的内容写到oldfd
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
    int fd = open("./log", O_CREAT | O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    close(1);
    dup2(fd, 1);
    for (;;)
    {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0)
        {
            perror("read");
            break;
        }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

3.6 FILE

        因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。

        所以C库中的FILE结构体内部,必然封装了fd。

来段代码研究一下

#include <stdio.h>
#include <string.h>
#include <unistd.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;
}

        我们正常运行的结果和我们的预期一样,但是当我们对进程实现输出重定向后发现printf和fwrite(库函数)都输出了两次,而write(系统调用)只输出了一次,这显然和fork有关系

        一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲的

        printf和fwrite库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后

        但是进程退出之后,会统一刷新,写入文件当中

        但是fork的时候,父子进程会发生写实拷贝,所以当你的父进程准备刷新的时候,子进程也有同样的一份数据也准备刷新,所以产生了两份数据

        write没有变化,说明write没有所谓的缓冲。

        综上:printf和fwrite库函数会自带缓冲区,而write系统调用没有缓冲区。另外,我们这里所说的缓冲区都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不在我们讨论范围之内。

        那这个用户级缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的上层, 是对系统调用的“封装,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

4.理解文件系统

        我们使用ls -l的时候看到的除了文件名,还有文件元数据

        每行7列:模式、硬链接数、文件拥有者、组、大小、最后修改时间、文件名

 

        其实这个信息除了通过这种方式来读取,还有一个stat 命令能够看到更多信息
        上面的执行结果有几个信息需要解释清楚
        (1)inode
        为了能解释清楚inode我们需要先简单了解一下文件系统
         
        Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block 。一个 block 的大小是由格式化的时候确定的,并且不可以更改。例如 mke2fs -b 选项可以设定block 大小为 1024 2048 4096 字节。而上图中启动块( Boot Block )的大小是确定的
Block Group ext2 文件系统会根据分区的大小划分为数个 Block Group 。而每个 Block Group 都有着相同的结构组成。政府管理各区的例子
超级块( Super Block ):存放文件系统本身的结构信息。记录的信息主要有: bolck inode 的总量,未使用的block inode 的数量,一个 block inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了
GDT Group Descriptor Table :块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下
块位图( Block Bitmap ): Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用
inode 位图( inode Bitmap ):每个 bit 表示一个 inode 是否空闲可用。
i 节点表 : 存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容

        将属性和数据分开的想法看起来很简单,但实际上是如何工作的,这里我们通过touch一个新文件来看看如何工作

 

        为了说明问题,我们将上图简化

        创建一个新文件主要有以下4个操作

                1.存储属性:内核先找到一个空闲节点(这里是263466)。内核把文件信息记录到其中

                2.存储数据:该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800.将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推

                3.记录分配情况:文件内容按顺序300,500,800存放。内核在inode上的磁盘分区记录了上述块列表

                4.添加文件名到目录:新的文件名abc,内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

        注意:通过4我们应该明白内核中其实存储的不是文件名而是inode,怎么说可能不准确应该是内核里的文件名是inode可能对些

        理解硬链接:我们真正找到磁盘上的文件并不是文件名,而是inode。其实在linux中可以让多个文件名对应于同一个inode看下图。

        abc和def的链接方式完全相同,他们被称为指向文件的硬链接。内核记录了这个链接数,inode 54850的硬链接数为2

        我们在磁盘中删除文件时干了两件事:1.在目录中将对应的记录删除   2.将硬链接数-1,如果为0,则将对应的磁盘释放

        连接软连接:硬链接是通过inode引用另一个文件,软连接是通过名字引用另一个文件,在shell中的做法

下面解释一下文件的三个时间:
        Access 最后访问时间
        Modify 文件内容最后修改时间
        Change 属性最后修改时间

相关文章:

  • 13 - linux 内存子系统
  • iQOO手机投屏到Windows有两种方法,其中一种可远程控制
  • Python 的 ​ORM(Object-Relational Mapping)工具浅讲
  • Llinux安装MySQL教程
  • 数组连续和 - 华为OD统一考试(C卷)
  • python中的allure报告使用
  • 【Python3教程】Python3基础篇之函数
  • c语言基础编程入门练习题
  • 【Android】安卓原生应用播放背景音乐与音效(笔记)
  • LeetCode-两数之和
  • 虚拟机 | Ubuntu操作系统:su和sudo理解及如何处理忘记root密码
  • 前端样式库推广——TailwindCss
  • 清晰易懂的 Kotlin 安装与配置教程
  • 深入理解 Linux ALSA 音频架构:从入门到驱动开发
  • 2025.3.19总结
  • 【Qt】private槽函数可以被其他类中的信号连接
  • DeepSeek扫盲篇: V3 vs R1全面对比架构差异与蒸馏模型演进史
  • SSD目标检测算法的学习与实践
  • Ubuntu 软件仓库配置文件详解及详细注释
  • 7-2-10 简易连连看
  • 山东:小伙为救同学耽误考试属实,启用副题安排考试
  • 习近平会见智利总统博里奇
  • 媒体:“西北大学副校长范代娣成陕西首富”系乌龙,但她的人生如同开挂
  • 美英贸易协议|不,这不是一份重大贸易协议
  • 《审判》|“被告”的魅力:K在等什么?
  • 十三届全国政协经济委员会副主任张效廉被决定逮捕