C1 - Unix 系统编程概述
本文主要:
- 从操作系统职责入手, 解释如何编写利用 "系统 API" 和 "库函数" 的程序.
- 分析标准 Unix 命令和其用到的系统调用, 指导编写实现相应功能.
- 形成一套系统编程时的通用实践流程.
首先需要建立针对 "普通程序" 和 "系统" 的心理模型:
- 普通程序模型:
输入 -> 程序 -> 输出
- 系统模型:
- 古老的无操作系统状态:
多用户 * 多设备 * 多输入 -> 多程序 -> 多输出
- 现代的有操作系统状态:
- 内核: 通过一个程序管理多个用户, 多个设备, 多个程序
- 用户空间程序: 用户要访问设备必须通过内核, 只有内核能直接管理设备, 或说内核向程序提供服务以便程序能访问到设备
- 古老的无操作系统状态:
通过上述模型, 我们可以引出如下:
- 编写普通程序时: 可以认为程序是直接连接到设备的.
- 系统编程时:
- 必须先要对系统的结构和工作方式有了解
- 要知道内核提供哪些服务(系统调用)
- 如何使用这些系统调用
- 要知道系统有哪些资源和设备
理解系统编程
内核提供服务以便系统程序可直接访问系统资源, 系统有哪些资源正在被内核管理或内核提供了哪些服务呢?
- 处理器调度
- 进程管理和控制
- 内存管理及虚拟存储器
- 文件管理
- 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
可以发现使用流程是:
- 打开目录:
opendir
- 读目录内容:
readdir
- 关闭目录:
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()
}
附
- 创建/修改日期? macOS 扩展属性读取? 软链接指向?
- 可以用来扩展更多的内容:
du
命令 - 文件系统遍历器