前言前段时间我本想写更多关于如何使用Rust进行系统编程的文章,但是我完全不知道该写什么。现在我偶尔会使用Rust,但是我自我感觉除了ptrace和共享内存访问方面之外,我对系统编程知之甚少。 我认为学习系统编程的最佳途径是The Linux Programming Interface ,这本书的目录给了我写作的灵感,不过我认为大部分内容枯燥无味,尽管如此,我还是想要尝试一下这些内容,如果最后事实证明确实很枯燥,那我便另寻它路。 我在此申明我并不是想要将那些C代码重写成Rust——这毫无意义并且有可能侵犯版权。我想要做的是提供一个Rust的视角,并且我十分鼓励你们阅读此书
文件操作书中一开始就提到了文件操作的问题,所以我们也以文件操作开始 下面的代码实现了打开一个文件并将内容读到缓冲区中,这些代码与C有很大的不同,但最后的运行结果是相同的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::fs::
File
;
use std::io::Read;
use std::path::Path;
fn main() {
let file_path
=
Path::new(
"test.txt"
);
match
File
::
open
(file_path) {
Ok(mut
file
)
=
> {
/
/
Read to a fil
let file_length
=
file
.metadata().unwrap().
len
();
let mut buf
=
vec![
0
; file_length as usize];
file
.read(&mut buf).unwrap();
println!(
">> {:?}"
, buf);
},
Err(err)
=
> {
eprintln!(
"Failed to open {} -> {}"
, file_path.display(), err);
}
}
}
你可能一眼就看到了File::open并且想要阅读官方文档 ,不要着急,让我们先了解一些基本知识
参数函数定义如下:fn open<P:AsRef<Path>>(path: P) -> Result<File>。与C中的文件操作不同的是,C中的file open的第一个参数是一个字符数组,但在Rust中,我们需要提供一个路径;你或许会好奇这有什么区别。
事实上不仅仅是path,任何对象在解读的时候都会返回一个path
其实path是OsString的一个薄包装器,path能获得的不仅是目录,还包括了文件名等信息,这极大地提高了效率 Rust使用的字符编码是UTF-8,这导致字符中可能包含零,而在才做系统中,文件名不能包含零;而path可以自动的处理这些问题
返回值和错误接下来需要讲的是返回值;在C中,当fopen出错时会返回一个“-1”,这种返回并不准确;Rust则使用Result 取而代之,这种方式的返回值会解压到文件处理程序或错误中 至于错误,则是由io::Error其中的值来表示。在示例代码中,我们并不区分错误类型,我们只是捕获错误,并显示给用户;如果你想以不同的方式处理错误,可以自行增加处理代码
一个有趣的事实:我们使用eprintln来打印错误,它与println的唯一区别是eprintln写入的是stderr而不是stdout,毕竟这些内容也是文件描述符的一部分
若我们没有遇到任何错误时,我们会得到一个文件处理程序——这里由文件结构表示;它将允许我们对文件执行各种操作,你可能会好奇,为什么我必须将文件标记为可变的?可变与文件可写是否有某种联系? 简单来说,文件可变与文件本身的可写性没有联系——我们打开文件使得文件只能是可读的;可变性使得文件本身可以与程序同步,因为首先访问的状态和同步是程序无法控制的;其次,阅读通过改变文件的偏移量来改变文件结构的状态
元数据当文件被打开时,我们会想要获得关于这个文件的信息,比如:文件所有者,文件的长度等等。为此,我们有一个metadata()调用,它返回一个metadata结构(同样是Result形式的);有了这些信息后,我们便可以执行更多的操作了,关于其他信息可以在文档 中查看 标准的metadata对象是平台无关的,这意味着你只能使用通用的处理方法(比如长度或文件类型);如果想要使用某些特定的功能,可以通过导入std::os::unix::fs::MetadataExt来启用它,启用后便可以访问像uid、gid、atime等其他字段
文件读取对于普通的文件描述符,可以通过调用read()函数来读取文件的内容。通过查看函数签名 ,我们看到它将目标缓冲区作为单个参数;此处需要注意,缓冲区使用前需要先初始化,否则会报错 当调用read()函数时,我们会从游标(也称文件偏移量)开始读取,直到文件的末尾;如果出现缓冲区比文件大的情况,则读取与缓冲区大小相同的字节,并更新游标 read()函数有许多变体,例如将内容转换为字符串的read_to_string()或将迭代给定文件中的各个字节的bytes()。Python程序员估计想不到还能读取单行内容,这在非缓冲区模式中是不存在的,后面我会给出解决方法
关闭文件细心的读者肯能注意到我们在这个例子中并没有关闭文件。这是因为文件变量超出范围时,会调用 Drop trait for File,文件将会被关闭;这有点儿类似于Python的上下文管理 有一点需要注意的是:在文件关闭的过程中遇到的任何错误都会被忽略。如果想要手动处理这些错误,则需要调用sync_all()函数
文件操作权限上面的代码中,我们只是用了File::open()来打开文件,事实上File::open()只是一个别名,他们依赖的是std::fs::OpenOptions构建器 ,这个构建器还可以实现其他操作,例如:创建文件或以仅追加模式打开文件; 样例代码如下
1
2
3
4
5
6
7
8
9
10
fn main() {
let file_path
=
Path::new(
"test.txt"
);
let
file
=
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.
open
(file_path);
}
在上面的样例代码中,我们正在创建一个可写可读的新文件,并且将其大小截断为零
缓冲区访问有时标准读取的效率并不高。假设我们想要用逐字节迭代的方式,将换行符之前的所有内容读取到缓冲区中,这意味着我们正在为读取的每个字节执行系统调用 不过在Rust中有更好的方法实现这一点;我们可以用std::io::BufReader来包装 一个文件处理程序,这会创建一个底层的内存缓冲区(目前是8192个字节),它将被一个read()调用填满,并允许用户操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let file_path
=
Path::new(
"/etc/passwd"
);
match
File
::
open
(file_path) {
Ok(
file
)
=
> {
let mut reader
=
BufReader::new(
file
);
for
_
in
0.
.
5
{
let mut line
=
String::new();
reader.read_line(&mut line).unwrap();
print
!(
"=> {}"
, line);
}
}
Err(err)
=
> {
eprintln!(
"Problem reading from {}: {}"
, file_path.display(), err);
}
}
}
如果我们遍历缓冲区,底层机制将再次调用read()以使用当前文件偏移量重新填充缓冲区;当进行多个小而重复的操作时,缓冲读取器(或写入器 )会是更好的选择
结束语本系列的第一部分到此结束,正如开头所说,我正在计划更多的内容,如果你喜欢此类内容,请告诉我 ,我会写更多(译者:我也会翻译更多)。为了完整起见,我可能应该写一些关于目录、扩展属性以及监控文件的内容。
译者言因本人翻译水平有限,如有错误之处,请斧正原文链接
[培训]科锐逆向工程师培训第53期2025年7月8日开班!