Skip to main content

C1 - Unix 系统编程概述

本文主要:

  1. 从操作系统职责入手, 解释如何编写利用 "系统 API" 和 "库函数" 的程序.
  2. 分析标准 Unix 命令和其用到的系统调用, 指导编写实现相应功能.
  3. 形成一套系统编程时的通用实践流程.

首先需要建立针对 "普通程序" 和 "系统" 的心理模型:

  • 普通程序模型: 输入 -> 程序 -> 输出
  • 系统模型:
    • 古老的无操作系统状态: 多用户 * 多设备 * 多输入 -> 多程序 -> 多输出
    • 现代的有操作系统状态:
      • 内核: 通过一个程序管理多个用户, 多个设备, 多个程序
      • 用户空间程序: 用户要访问设备必须通过内核, 只有内核能直接管理设备, 或说内核向程序提供服务以便程序能访问到设备

通过上述模型, 我们可以引出如下:

  • 编写普通程序时: 可以认为程序是直接连接到设备的.
  • 系统编程时:
    • 必须先要对系统的结构和工作方式有了解
    • 要知道内核提供哪些服务(系统调用)
    • 如何使用这些系统调用
    • 要知道系统有哪些资源和设备

理解系统编程

内核提供服务以便系统程序可直接访问系统资源, 系统有哪些资源正在被内核管理或内核提供了哪些服务呢?

  • 处理器调度
  • 进程管理和控制
  • 内存管理及虚拟存储器
  • 文件管理
  • I/O 管理
  • 进程间通信和网络通信(可理解为特殊的 IPC)
  • etc.

上述内核管理职责或提供的服务, 原理和细节详见 "计算机操作系统". 我们要做的, 是从编程开发的角度去看:

  • 这个服务有什么特点? 关联了哪些设备?
  • 编程需要哪些参数? 会提供哪些数据?

以便我们在自己的程序中使用这些服务. 要探究上述问题, 可以使用三步法: 分析现有程序(能做什么?) -> 用到哪些系统调用(如何实现的?) -> 自己写一个

编写一个简化版 ls 命令

ls 命令可以列出目录中所有文件的名字, 以及这些文件的信息.

三步法第一步 分析现有程序: ls 功能分析

直接执行 ls:

带参数 ls -l:

通过 man ls 可以看到 ls 还有很多功能及参数选项等, 不再一一列举:

综上, ls 命令的主要功能是:

  • 列出目录内容
  • 显示文件的信息

ls 能够区分目录和文件并正确展示其信息, 通过上面的功能分析, 我们需要调查实现的内容也就明确了:

  • 需要具备的基础知识: Unix 是如何管理文件系统的
  • 实现调查1: 如何列出目录中的内容
  • 实现调查2: 如何判断是文件还是目录
  • 实现调查3: 如何读取文件信息

三步法第二步 如何实现

基础知识概述

Unix 系统中, 目录和文件被组成一颗文件树(每个结点都是文件).

目录是一种特殊的文件, 其内容是文件的列表(文件记录的列表), 细节谷歌.

对文件可以创建软链接/硬链接, 其中硬链接会将文件的引用计数+1, 目录生来就有 2 个硬链接数(...), 正常情况下无法对目录创建硬链接, 因为这样会破坏文件系统结构(循环).

如何列出目录内容

透过谷歌或 man -k directory | grep read, 很容易就能找到, 我们需要用到一个名为 readdir 的系统调用, man readdir 可以发现使用流程是:

  1. 打开目录: opendir
  2. 读目录内容: readdir
  3. 关闭目录: closedir

如何判断是文件还是目录

同理, 很容易发现可以在文件上使用 stat 获取信息.

如何读取文件信息

同上.

三步法第三步 开始实现

整体步骤如下:

上码. 仍然利用 Rust 实现

nix = { version = "0.28.0", features = ["user", "signal"] }
use std::{
fs::{self, FileType},
path::Path,
};

pub fn ls_cmd_simple(path: &Path) {
if !path.is_dir() { return }
fs::read_dir(path).unwrap().for_each(|e| {
if let Ok(e) = e {
println!(
"{: <20}\tfile name: {: <20}",
file_type_string(&e.file_type().unwrap()),
e.file_name().to_str().unwrap_or("no name")
);
}
});
}

fn file_type_string(ft: &FileType) -> &str {
if ft.is_dir() {
return "dir";
} else if ft.is_file() {
return "normal file";
} else if ft.is_symlink() {
return "symlink";
}
"other"
}

对 Home 目录执行:

添加 ls -l 命令

ls -l 能做什么

输出包括如下字段:

  • mode: 文件类型 & x文件访问权限
  • 链接数: 硬链接数
  • 所有者名称
  • 所属组名称
  • 文件大小
  • 最后修改时间
  • 文件名

怎么实现

稍微一搜就知道, 获取上述信息可以使用 stat 系统调用.

man 2 stat

开始实现

在原来的基础上, 进行一轮改造, 即可.

use std::{fs, path::Path};

use nix::{
libc::{
self, mode_t, S_IFDIR, S_IFMT, S_IFREG, S_IRGRP, S_IRUSR, S_IWGRP, S_IWUSR, S_IXGRP,
S_IXUSR,
},
sys::stat,
unistd::{Gid, Group, Uid, User},
};

pub fn ls_with_l(path: &Path) {
if !path.is_dir() {
return;
}
fs::read_dir(path).unwrap().for_each(|e| {
if let Ok(e) = e {
let st = stat::stat(e.path().as_path()).unwrap();
let info = stat_print(st);
println!(
"{: >20}\t{: <20}",
// file_type_string(&e.file_type().unwrap()),
info,
e.file_name().to_str().unwrap_or("no name")
);
}
});
}

// fn file_type_string(ft: &FileType) -> &str {
// if ft.is_dir() {
// return "dir";
// } else if ft.is_file() {
// return "normal file";
// } else if ft.is_symlink() {
// return "symlink";
// }
// "other"
// }

fn stat_print(s: libc::stat) -> String {
// 文件类型和权限
let mode_str = mode_to_string(s.st_mode);
// 硬链接数
let nlink = s.st_nlink;

// names: 未考虑当文件所属的用户或组不存在时候的展示(比如被删了)

// getpwuid(uid) -> name
let user_name = User::from_uid(Uid::from(s.st_uid)).unwrap().unwrap().name;
//
let group_name = Group::from_gid(Gid::from(s.st_gid)).unwrap().unwrap().name;

format!(
"{mode_str}\t{nlink}\t{user_name}\t{group_name}\t{}",
s.st_size
)
}

fn mode_to_string(mode: mode_t) -> String {
let mut res = ['-'; 10];
let sflag = mode & S_IFMT;
res[0] = match sflag {
S_IFDIR => 'd',
S_IFREG => '-',
_ => '?',
};
res[1] = if mode & S_IRUSR == 0 { '😈' } else { 'r' };
res[2] = if mode & S_IWUSR == 0 { '😈' } else { 'w' };
res[3] = if mode & S_IXUSR == 0 { '😈' } else { 'x' };

res[4] = if mode & S_IRGRP == 0 { '😈' } else { 'r' };
res[5] = if mode & S_IWGRP == 0 { '😈' } else { 'w' };
res[6] = if mode & S_IXGRP == 0 { '😈' } else { 'x' };

res.iter().collect()
}

  1. 创建/修改日期? macOS 扩展属性读取? 软链接指向?
  2. 可以用来扩展更多的内容: du 命令
  3. 文件系统遍历器