Bo2SS

Bo2SS

1 文件、目录操作与实现ls的思路

课程内容#

文件操作#

  • 【引入】之前学过的 cp、mv、cat 命令,都涉及到文件的读写
    • cp:读→写
    • mv:读→写→删
    • cat:读→写
  • 上面这些步骤是如何实现的呢?

【底层操作,基于文件描述符】

open#

打开或创建一个文件 [别名:openat、create]

  • man 2 open【关注函数原型及描述】
  • 原型
    • 图片
    • 返回值 int:文件描述符 或 -1
      • 图片
      • 常用文件描述符:0-stdin、1-stdout、2-stderr
      • -1:发生错误,并会设置 errno [可供 perror 使用,见代码演示]
    • flags:文件的打开方式
    • [PS] 不需要特意记头文件
  • 描述
    • 图片
    • 系统调用 [system call]:帮助你做你没有权限做的事情
    • 如果 open 的文件不存在,可能会新建该文件 [当 flags 里定义了 O_CREAT]
      • O_CREAT
        • open 函数的 flag
        • C 语言体系中,全大写表示宏定义
        • 底层是一个 int 型数据,叫做位掩码
          • 32 位,可表示 32 种状态,每一位表示一种状态
          • 状态之间可使用与、或、异或的方式转换
    • 文件描述符 [file descriptor]
      • 小、非负、后续系统可调用 [read、write...]
      • 返回值永远是当前进程中可以取的最小的数字
        • 可用来判断文件数量 [如果返回 1000,当前文件数量一定超过 1000 了]
    • 打开文件后,文件指针默认在文件头部
    • 文件描述 [file description]
      • 每次调用 open 会创建一个新的 open 文件描述,它是系统全局文件表中的一条信息
        • 记录文件偏移量和文件的状态
      • [PS] 文件描述符是一个 open 文件描述的引用,不因 pathname 的改变而受影响
    • ⭐flags
      • 必须包含 O_RDONLY、O_WRONLY、O_RDWR 中的一个
      • flag 之间用位或组合
      • O_CREAT 创建
      • O_TRUNC 截断
      • O_DIRECT 直接 IO
        • 图片
        • 直接 IO—— 同步写,文件会直接写进去,而不经过缓冲
        • 缓冲 IO
          • 缓冲结束条件:① 攒到一堆数据;② 等固定时间
            • 往磁盘写一个字符 a,不会马上写入磁盘,可以减小成本
            • 但断电时易丢失数据
            • [PS]
              • 磁盘的最小单元是块,每个块是 4K
                • 所以磁盘又叫块设备
              • 类似 printf 输出到 stdout 的条件 [行缓冲]
                • 遇到回车 / 程序结束,系统自动冲洗缓冲区
                • 缓冲区满,自动冲洗
                • fflush 函数,手动冲洗
      • O_NONBLOCK 非阻塞 IO
        • 阻塞
          • 如:scanf 的时候,要等标准输入流中有输入才能进行后面的操作
          • 缺点:浪费资源
        • 非阻塞
          • 不会等
          • 缺点
            • 需频繁回来查看,也浪费资源
            • 需有某种机制监测,花费技术成本
      • O_TMPFILE 创建临时文件
        • 进程运行结束后文件就会被删除,交易关闭也会
        • 类似系统的临时文件夹 /tmp
  • ❗ 底层读写文件前,需要调用 open 函数获得文件描述符

read#

通过文件描述符读取数据

  • man read
  • 原型
    • 图片
    • 返回值 ssize_t:读取的字节数 或 -1
      • 以_t 结尾,一般是用户自定义类型
      • 猜测:也是基本类型之一,可能是 long long,可能是 int
      • 通过 ctags 一步步找具体类型:ctrl + ] 、ctrl + o
        • 图片
        • 答案:int [32 位系统下];long int [64 位系统下]
        • [PS] 按理说,32 位系统下,long int 大小等同于 int
    • buf、count:每次最多读取 count 字节数据到 buf 中
  • 描述 + 返回值
    • 图片
    • 尝试读取最多 count 字节到 buffer 中
      • 读取字节数达不到 count 的情况:被人中断 [signal];数据本身不足 count 大小
    • 每成功读取 num [≤ count] 字节数据,文件偏移量 [像指针] 会自动往后走 num 大小
      • 如果文件偏移量在 EOF [没有数据可读了],函数返回 0
    • count
      • 如果设为 0,错误可能被检测出来,如果没有检测到错误,返回 0
      • 如果大于 SSIZE_MAX [int /long int 的最大值],返回的结果将是定义好的 [POSIX.1 标准]
    • 返回值
      • ≤ count
      • 出错时返回 - 1,并会设置 errno
  • [PS] ERRORS
    • EAGAIN
      • 图片
      • 读文件 [包括 socket] 的时候,尽管文件已被设置为 O_NONBLOCK,read 将会阻塞

write#

通过文件描述符写数据

  • man 2 write
  • 原型
    • 图片
    • 与 read 很相似
  • 描述 + 返回值
    • 图片
    • 与 read 很相似
    • 写的字节数达不到 count 的情况:物理空间不够;系统资源限制;被 signal 中断
    • open 文件时设置了 O_APPEND [追加]
      • 文件偏移量 [offset] 在文件末尾,写操作会追加
      • 否则放在开头,写操作会覆写

close#

关闭一个文件描述符

  • man close
  • 图片
  • 主要就是关闭文件描述符
  • [PS]
    • 记录锁将被移除
    • 特殊情况
      • close 的是文件描述的最后一个文件描述符,文件描述对应的资源会被释放
      • close 的是文件的最后一个引用的文件描述符,文件将会被删除
    • 暂时不用在意内核具体做了什么

【标准文件操作,基于文件指针】

<stdio.h>

fopen#

通过流打开文件

  • man fopen
  • 原型
    • 图片
    • 返回值 FILE *:文件指针
      • 早期是宏定义,这里大写是为了兼容性
    • mode
      • 类型是 char *,而不是 int
  • 描述
    • 图片
    • 关联一个流 [stream]
      • [PS] 网络上发布数据,字节流;文件流 < 类型:FILE *>
    • mode
      • r /r+:读 / 读写
        • 流在文件开始
      • w /w+:读 / 读写
        • 流在文件开始
        • 文件存在,截断文件 [打开就会将原数据清除]
        • 文件不存在,创建文件
      • a /a+:追加 / 读与追加
        • 追加时,流在 EOF;读时,流在文件开始
        • 文件不存在就会创建
      • +:读写通吃
      • [PS]
        • b:可以在 mode 字符串的最后或者两个字符之间,可用于处理二进制文件,但在 Linux 上一般没有效果
        • 图片
        • ❓ 任何被创建的文件会被进程的 umask value 修正
  • 返回值
    • 图片
    • 成功时,返回文件指针
    • 出错时,返回 NULL,并设置 errno

fread、fwrite#

二进制流的 IO

  • man fread / fwrite
  • 图片
  • fread:从 stream 中读 nmeb 次数据 [size 字节 / 次] 到 ptr
  • fwrite:把 ptr 的数据写 nmeb 次数据 [size 字节 / 次] 到 stream
  • 返回值 size_t:读 / 写的 items 数量 [成功]
    • [无符号的 ssize_t]
    • 发生错误或提前遇到 EOF 时 👉 0 ≤ 返回值 < nmeb
      • ❗ 所以不能通过返回值区分 EOF 和错误,需使用 feof、ferror 确认
    • [PS] 当 size 为 1 时,返回值等于传输的字节数

fclose#

刷新流并关闭文件描述符

  • man fclose
  • 图片
  • 刷新流实际调用的是 fflush
  • 返回值
    • 0 [成功]
    • -1 (EOF),并设置 errno [失败]
    • 未定义行为 [传入的是一个非法指针或已经被 fclose 过]
  • ⭐标准 IO 里的所有操作都是缓冲 IO
    • 本身没有权限写,需等待内核控制
  • ❓ 标准 IO 更适合文本 [用户],底层的 IO 更适合二进制文件

目录操作#

本质上也是文件 [早期可以直接 open]

opendir#

  • man opendir
  • 图片
  • 返回值 DIR *:目录流指针 或 NULL
    • 目录流默认被放置在目录的第一个条目
    • 发生错误时,返回 NULL,并设置 errno

readdir#

  • man readdir
  • 图片
  • 返回值 struct dirent *:目录项 或 NULL
    • 目录流中下一个目录项 [结构体] 的指针
      • 结构体的主要字段:d_ino、d_name
      • [PS]
        • 一次一次地返回下一个文件
        • d_off:与 telldir 返回的值一样,又类似 ftell ()
          • 此 offset [每个文件的大小不同] 与一般意义 [字节为单位] 上的不一样
          • ftell () 获取当前文件所在位置指示器的值
    • NULL [遇到目录流结尾或发生错误时]

closedir#

  • 关闭目录

实现 ls -al 的基本思路#

  • ls -al 效果
    • 图片
    • 需要的信息有:文件权限、连接数、用户名、组名、文件大小、修改时间、文件名
  • 思路
    • readdir()
      • man readdir
      • 读取目录里的每一个文件
      • 可获取文件名
    • stat()、lstat()
      • man 2 stat
      • 根据文件路径获取文件信息:stat 结构体
      • 图片
      • 可获取文件权限、硬连接数、uid、gid、文件大小、修改时间
      • 可参考里面的 EXAMPLE:lstat
      • lstat () 和 stat () 的区别
        • 图片
        • lstat () 可以查看软连接的信息,而不会跳到软连接指向的文件
    • getpwuid()
      • man getpwuid
      • 根据 uid 获取 passwd 结构体
      • 图片
      • 可获取对应的用户名
    • getgrgid
      • man getgrgid
      • 根据 gid 获取 group 结构体
      • 图片
      • 可获取对应的组名
    • 如果自己去实现 → 读文件、切分
      • 用户信息:/etc/passwd
      • 组的信息:/etc/group
  • 其他细节
    • 颜色
    • 排序
    • 纯 ls 命令输出的显示列数随宽度变化
      • 获取终端大小
      • 图片
      • 参考 ioctl,man ioctl
    • 列宽如何确定,可用暴力方式、二分查找、慢慢逼近

代码演示#

底层文件操作#

  • 图片
  • ⭐ 详见注释,关注使用
  • ❗ 避免乱码情况
    • 字符串 buff 末尾留一位放 '\0'
      • sizeof(buff) - 1
    • 最后一次不足 512 字节的读取,需要排除多余字节的干扰
      • 方法①:手动 menset (buff, 0, sizeof (buff))
      • 方法②:始终保持数据末尾是 '\0',buff [nread] = '\0'
    • [PS] 学习系统上层命令时,不必太关注这些
  • perror 打印一个系统错误信息
    • man 3 perror
    • 原型
      • 图片
      • fopen 等发生错误时就会设置 errno
    • 描述
      • 图片
      • 在 stderr 上输出上一个调用的错误信息
      • s 通常包含函数的名称
  • 建一个常用的 common 头文件夹,放常用的头文件
    • head.h
    • 图片

标准文件操作#

  • 图片
  • buffer 放在循环里,每次都会初始化
  • nread 为非负数,并且不能区分 EOF 和错误

标准 IO 是缓冲 IO#

  • 图片
  • 第一个 "Hello world" 直接输出,stderr 没有缓冲
  • 第二个 "Hello world" 本来会等 sleep 结束,无法输出到 stdout,但是可以立马输出,通过👇
    • 手动刷新缓冲区:fflush
    • 输出换行
  • sleep 函数在 unistd.h 中

附加知识点#

  • ulimit -a,可查看可打开的文件数量上限
    • 图片
    • 每个进程中文件打开数量上限为 1024
      • 超过会使系统崩盘
      • [PS]
        • 系统崩盘还需考虑内存
        • 要做一个负责任的程序:手动 close /free、输出错误日志
  • 只有标准输出是行缓冲的

思考点#

Tips#

  • 在 vim 中,Shift + K 可跳到 man 手册
  • 推荐复制即翻译软件:CopyTranslator
  • man 手册在线文档:man page——die.net

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.