初学rust,不允许遍历过程中修改HashMap
Tag rust, HashMap, 遍历, 修改, on by view 11

先看一段代码

#[test]
fn test_hash_map() {
    let mut mp: HashMap<&str, &str> = HashMap::new();
    mp.insert("k", "v");
    mp.insert("k1", "v1");
    let x = mp.keys().clone();
    for k in x {
        mp.insert("k2", "v1");
        mp.insert("k3", "v1");
    }
    mp.insert("k2", "v1");
    mp.insert("k3", "v1");
}

看,它报错

error[E0502]: cannot borrow `mp` as mutable because it is also borrowed as immutable
   --> src/process.rs:153:9
    |
151 |     let x = mp.keys().clone();
    |             -- immutable borrow occurs here
152 |     for k in x {
    |              - immutable borrow later used here
153 |         mp.insert("k2", "v1");
    |         ^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

报错告诉我们,不允许将mp作为mutable,因为它已经用于immutable了。难道,HashMap插入数据完毕,开始读取数据之后,不能再次插入数据了?我一开始这么怀疑,不应该啊,于是代码改成这样

#[test]
fn test_hash_map() {
    let mut mp: HashMap<&str, &str> = HashMap::new();
    mp.insert("k", "v");
    mp.insert("k1", "v1");
    mp.keys().clone();
    mp.insert("k2", "v1");
    mp.insert("k3", "v1");
}

正常了,没报错了。看样子问题出在这个for ..in..中。仔细分析代码,我在for中遍历了它的key,或者说我正在将这个HashMap中的数据拿出来,这时候,我在for中尝试往这个HashMap中写入数据,写入数据会让这个HashMap发生变更,这里第一感让我觉得可能有问题。比如我HashMap中有10个元素,我在遍历它,然后在for中间插入新元素,那么是不是有下列问题:1,我的HashMap会不会越遍历越多,会不会永远无法遍历完;2,HashMap是无序的,我将新元素插入HashMap中,会不会导致我已经遍历过的数据由于插入新数据,导致再次被读取出来,因为它可能位置发生变化了嘛。

其实这就是数据竞争和它带来的不确定性问题,rust作为一个内存安全第一的编程语言,编译器会教你做人。

于是,我再改

#[test]
fn test_hash_map() {
    let mut mp: HashMap<&str, &str> = HashMap::new();
    mp.insert("k", "v");
    mp.insert("k1", "v1");
    let x: Vec<&str> = mp.keys().map(|k| *k).collect();
    for k in x {
        mp.insert("k2", "v1");
        mp.insert("k3", "v1");
    }
    mp.insert("k2", "v1");
    mp.insert("k3", "v1");
}

这回正常了。可以看到我的操作let x: Vec<&str> = mp.keys().map(|k| *k).collect();是将keys()拿到的Keys迭代器(仍旧从前面的HashMap里迭代)通过.collect()方法将迭代器里的元素“倒”入到Vec<&str>,这样这个Vec就是一个独立与HashMap内存空间之外的变量,再基于这个Vec进行遍历,就可以避免“边遍历边修改的”的情况了。

那么大家可以思考一下,其他语言,比如golang,遇到这种情况是怎么处理的呢。


初学rust,踩坑私有依赖
Tag rust, 依赖, git, on by view 18

最近尝试在一个项目中引用一个私有crate,这个crate是一个lib,然后存储在私有git仓库中,并且这个git平台不支持rust的crate-index.

  • 第一坑,引用git依赖

于是我在我的项目中这样引用外部依赖包

[package]
name = "las"
version = "0.1.1"
authors = ["...."]
edition = "2018"

[dependencies]
elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = [
    "rustls-tls",
] }
logwatcher = "0.1.1"
json = "*"
reqwest = { version = "0.11.12", default-features = false, features = [
    "blocking",
    "rustls-tls",
] }
toml = "0.8.6"
serde_derive = "1.0.177"
serde = "1.0.177"
serde_json = "1.0.104"
once_cell = "1.18.0"
futures = "0.3"
tokio = { version = "*", features = ["full"] }
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
chrono = "0.4"
core_affinity = "0.8.0"
geoip = { git = "ssh://git@git.xxxx.com/xxxx/tgeoip.git", branch = "master", package = "geoip" }

nix = "0.26"
libc = "0.2.146"
clap = { version = "4.4.2", features = ["derive"] }
log4rs = { version = "1.2.0" }
log = { version = "0.4.20" }
[build-dependencies]
regex = "1.6.0"

需要注意的是,我git@git.xxxx.com:xxxx/tgeoip.git仓库中是一个workspace,里面有2个子项,一个是可执行bin项目,叫做regen,一个是外部可依赖包,叫做geoip(后续实践表明同一个workspace里面的子项引用就应该这么干,git路径是同一个,使用package指定子项的包名),我这样引入之后,发现死活拉取不了私有git依赖。报错没权限拉取,git鉴权失败

error: failed to get `geoip` as a dependency of package `las v0.1.1 (/data/code/rust/las)`

Caused by:
  failed to load source for dependency `geoip`

Caused by:
  Unable to update ssh://git@git.xxxx.com/xxxx/tgeoip.git?branch=master#e41c5279

Caused by:
  failed to clone into: /root/.cargo/git/db/tgeoip-9094aceea5940357

Caused by:
  failed to authenticate when downloading repository

  * attempted ssh-agent authentication, but no usernames succeeded: `git`

  if the git CLI succeeds then `net.git-fetch-with-cli` may help here
  https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli

Caused by:
  no authentication methods succeeded

各方查询后,都是建议添加这个选项,我开始还没搞清楚加在哪儿,但是发现使用环境变量好使。各方尝试后:

添加到~/.cargo/config文件中,如下

[net]
git-fetch-with-cli = true

然后再次执行cargo build无需环境变量即可。(前提是你本地已经配置好了访问ssh://git@git.xxxx.com/xxxx/tgeoip.git的ssh key)

  • 第二坑,外部依赖包要申明为rlib

我在这个bin项目中引用geoip,发现一直报错

failed to resolve: use of undeclared crate or module `geoip`

百思不得其解,最后发现是我依赖包申明类型的问题,我Cargo.toml申明的是

[package]
....

[lib]
name = "geoip"
crate-type = ["staticlib", "cdylib"]

[dependencies]
bincode = "1.3.3"
serde = { version = "1.0.193", features = ["derive"] }

实际lib应该申明rlib类型,才能被正常引用,改为如下,更新依赖,就没报错了

[package]
....

[lib]
name = "geoip"
crate-type = ["staticlib", "cdylib", "rlib"]

[dependencies]
bincode = "1.3.3"
serde = { version = "1.0.193", features = ["derive"] }

初试riscv机器
Tag riscv64, linux, on by view 9

最近入手了一块riscv64的开发版,是某宝上入手的StarFive2,准备拿它当做riscv64架构的服务器用来编译程序。开始刷上的是官方给的debian系统,成功启动了,nvme正常,但是有两个问题,第一个是apt update会报签名错误,第二个是不支持docker,想要让它支持docker,需要自己编译升级内核,非常麻烦。于是决定换用ubuntu版本的镜像。

根据官方教程,需要先将板载固件刷新到ubuntu社区的指定版本,从这里下载,需要注意的是,目前(2023/12/18)最新版的固件(u-boot-starfive-2024.01~rc4-0ubuntu1~ppa1这个)会导致kernel panic,需要下载下面的版本

2flhl8vj,展开后,用wget下载deb安装包,找个ubuntu机器,用dpkg -x解包,将里面的下面两个文件上传到sdcard.img刷机包启动的系统里面

g80xfqba

更新SPL

flashcp -v u-boot-spl.bin.normal.out /dev/mtd0

更新U-Boot

flashcp -v u-boot.itb /dev/mtd2

然后安装ubuntu系统,注意从这里选择下面版本的系统

jerua3mn

使用etcher将固件写入到sd卡,插入sd卡,启动,正常情况就可以启动成功。

在这中间我踩过一些坑,在此也一起分享一下。首先是hdmi插在4k屏无法显示,这样一来,我就不知道当前系统启动得怎么样了,因此kernel panic了,我也不知道,后来只能使用usb转串口用电脑连接串口,将串口控制台当做显示器用,才知道启动过程中发生了什么,串口连接如下图

3atnih7j

从串口控制台看到如下图(学到了,串口还能当显示器用)

y2m6970v

可以看到,启动过程卡死在kernel panic,我这才知道是内核崩溃了。看起来是u-boot-starfive-2024.01~rc4-0ubuntu1~ppa1这个版本的固件,对现有的23.04版本的系统兼容性有问题,所以我换了上个版本的固件,成功启动。

到此,在StarFive2上成功的安装上了ubuntu系统,docker也成功的安装,不过这个版本的板载固件 有个问题,就是无法识别nvme硬盘,希望下个正常版本能够支持。


初学rust,错误处理
Tag 错误处理, rust, on by view 6

rust中如何比较优雅的进行错误处理,这是一直以来困扰我的一个问题。最近写了一个ip地址库查询包,于是在其中实践了一下自定义错误、错误抛出等处理。

rust中,如果一个函数需要返回错误,那么应该用Result包裹返回值,Result定义如下

enum Result<T, E> {
    Ok(T),
    Err(E),
}

通常,Result中的第二个参数就是错误。比如Result<String, String>,那么它的错误就是一个String类型的值,如下

// 返回了正常值hi
fn find() -> Result<String, String> {
    Ok("hi".to_string())
}

// 返回了错误信息error
fn find() -> Result<String, String> {
    Err("error".to_string())
}

那么有另外一个问题,就是,我们如何处理其他库或者第三方函数抛出给我们的错误呢?我们可以用expect(),unwrap()来解决可以解决的错误,但是有时候我们不希望处理错误,希望能够将错误抛出给上层,让上层调用者去处理。这时候,我们应该怎么定义错误类型?你当然可以在你的函数中进行错误处理之后,抛出String类型的错误,以便继续使用String类型作为错误类型。

#[test]
fn test_find() {
    let x = foo();
    println!("{:?}", x.unwrap())
}

fn foo() -> Result<File, String> {
    let x = bar();
    match x {
        Err(e) => Err(e.to_string()),
        Ok(f) => Ok(f),
    }
    ...
}

fn bar() -> Result<File, io::Error> {
    File::open("regions.txt")
}

你可以看到,我在foo()中调用bar()bar()中返回了一个包含io::Error的错误,而我的foo()中要求返回的是String,我在foo中用match处理了错误的情况,并且foo中可能调用其他第三方库函数,返回的错误类型不尽相同,我每种错误类型都可以使用match解开,然后返回String类型的错误。但是这样处理起来,代码看起来就非常乱,到处都是match错误。

其实我们有另一种方法,可以自己定义一个Error类型,然后对Error类型进行扩展,让它兼容其他类型,如下

use std::{fmt, io};

#[derive(Debug)]
pub enum Error {
    ParseError,
    ReadError,
    InvalidIPError,
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::ParseError => write!(f, "Parse Error"),
            Error::ReadError => write!(f, "Read Error"),
            Error::InvalidIPError => write!(f, "Invalid IP Error"),
        }
    }
}

impl From<io::Error> for Error {
    fn from(_: io::Error) -> Self {
        Error::ReadError
    }
}

impl From<bincode::Error> for Error {
    fn from(_: bincode::Error) -> Self {
        Error::ParseError
    }
}

impl From<std::net::AddrParseError> for Error {
    fn from(_: std::net::AddrParseError) -> Self {
        Error::InvalidIPError
    }
}

我们的Error类型,实现了fmt::Display,让它能够处理我的包项目中可能出现的第三方错误,并且针对这些错误实现了各自的From<T>from方法,这样,这些第三方错误就可以直接以我的Error返回,使用?简写之后,代码就改成这样了

#[test]
fn test_find() {
    let x = foo();
    println!("{:?}", x.unwrap())
}

fn foo() -> Result<File, Error> {
    let x = bar()?;
    Ok(x)
}

fn bar() -> Result<File, io::Error> {
    File::open("regions.txt")
}

对比可以看到,我在foo中处理bar抛出的错误变得简单了,直接一个问号就将可能得错误抛出到Error了,然后再将Ok(x)返回。这样一来,错误的抛出和处理就变得优雅多了。


gin.Context 异步调用踩坑
Tag gin, context, 异步, goroutine, on by view 13

事情的背景是这样了,我在gin框架的action中有个异步调用逻辑,然后异步调用需要使用context.Context接口作为参数传入,异步调用的模块中会从context中取request_id作为追踪追踪标记。于是我就直接讲*gin.Context作为参数传入了异步调用中。

然后灾难就发生了,我日志记录中能查到当前请求的request_id,但是发现条数不对,异步请求中记录的日志条数有10条,我用request_id去搜索,只查到了4条,最后我只能添加另外标记,通过另外的标记查询到10条日志,让我奇怪的是,另外6条日志的request_id却不是当前请求的request_id。

最后,我发现gin.Context专门有个Copy方法,是将context拷贝一份,我调用Copy拷贝一份context之后将拷贝的context传入异步调用,果然10条日志的request_id一致了。看来,从gin框架传入进来的context在不同的action中是复用的。然后在请求处理完毕之后,会给其他请求复用,这样传给异步调用模块的context中的request_id已经变了。

r.GET("/async", func(c *gin.Context) {
	// 需要搞一个副本
	copyContext := c.Copy()
	// 异步处理
	go func() {
		time.Sleep(3 * time.Second)
		log.Println("异步执行:" + copyContext.Request.URL.Path)
	}()
})

这里必须要谨记,如果需要在action中异步调用并使用context传参,必须要将context.Copy()之后再传入。否则context就会被其他协程修改。


Bazel对依赖库进行patch遇到的问题
Tag bazel, patch, 依赖, on by view 12

最近使用bazel需要对一个go第三方依赖进行patch,这个依赖是gorm/v1,原作者对v1已经放弃了,我们项目中使用了v1,但是因为v1到v2是不兼容的升级,所以我们项目中无法升级。针对v1的优化和bug修复,就只能使用patch的方式进行了。

第一步将github.com/jinzhu/gorm克隆下来,check出最后一个版本v1.9.16,然后做修改。修改完毕之后,不要进行git提交,执行git diff >> new1.patch将会生成这次变更的patch文件。

将patch文件存储如下0001-go-errors.patch

third_party_patches
└── com_github_jinzhu_gorm
    ├── 0001-go-errors.patch
    ├── 0002-fix-sqlite.patch
    └── BUILD.bazel

其中,BUILD.bazel空文件即可。另外deps.bzl中的引用如下

go_repository(
    name = "com_github_jinzhu_gorm",
    build_file_proto_mode = "disable",
    importpath = "github.com/jinzhu/gorm",
    patch_args = ["-p1"],
    patches = [
        "//third_party_patches/com_github_jinzhu_gorm:0001-go-errors.patch",
        "//third_party_patches/com_github_jinzhu_gorm:0002-fix-sqlite.patch",
    ],
    sum = "h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=",
    version = "v1.9.16",
)

但是我编译的时候,却报错了

➜  code git:(feature/migrate-flow) ✗ bazel build //...
INFO: Analyzed 74 targets (3 packages loaded, 141 targets configured).
INFO: Found 74 targets...
ERROR: /root/.cache/bazel/_bazel_root/aa05a35ce6b6cd2ce6b5a504fd9d9e22/external/com_github_jinzhu_gorm/BUILD.bazel:3:11: GoCompilePkg external/com_github_jinzhu_gorm/gorm.a failed: (Exit 1): builder failed: error executing command bazel-out/host/bin/external/go_sdk/builder compilepkg -sdk external/go_sdk -installsuffix linux_amd64 -tags sqlite -src external/com_github_jinzhu_gorm/association.go -src ... (remaining 71 argument(s) skipped)

Use --sandbox_debug to see verbose messages from the sandbox
compilepkg: missing strict dependencies:
        /root/.cache/bazel/_bazel_root/aa05a35ce6b6cd2ce6b5a504fd9d9e22/sandbox/linux-sandbox/4877/execroot/ias-admin/external/com_github_jinzhu_gorm/main.go: import of "github.com/go-errors/errors"
No dependencies were provided.
Check that imports in Go sources match importpath attributes in deps.
INFO: Elapsed time: 0.843s, Critical Path: 0.06s
INFO: 4 processes: 4 internal.
FAILED: Build did NOT complete successfully

按照报错打开bazel cache路径,发现bazel为依赖生成的BUILD.bazel文件中,并没有添加我patch中引入的依赖。于是我将cache中的BUILD.bazel拷贝出来,手动加入依赖,然后git diff,得到BUILD.bazel的patch文件如下


diff --git a/BUILD.bazel b/BUILD.bazel
index 0849476..d26e9f7 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -32,7 +32,10 @@ go_library(
     ],
     importpath = "github.com/jinzhu/gorm",
     visibility = ["//visibility:public"],
-    deps = ["@com_github_jinzhu_inflection//:inflection"],
+    deps = [
+        "@com_github_jinzhu_inflection//:inflection",
+        "@com_github_go_errors_errors//:go_default_library",
+    ],
 )
 
 alias(
@@ -76,5 +79,6 @@ go_test(
         "//dialects/sqlite",
         "@com_github_erikstmartin_go_testdb//:go-testdb",
         "@com_github_jinzhu_now//:now",
+        "@com_github_go_errors_errors//:go_default_library",
     ],
 )

加入到我们的patch中,然后再次编译,就正常了。

我推断bazel patch这里面的逻辑是,先拉下原始包,第二步生成BUILD.bazel,最后再patch。google的研发bazel的人简直就是没长脑子,这样的步骤我还要它生成BUILD.bazel干啥,还不如直接把BUILD.bazel生成到patch文件里。总之,折腾了许久总算解决了,记录一下。


初识串口
Tag 串口, on by view 9

一直很想了解设备驱动编程。所以就从最基础,最简单的设备开始了解。串口,应该是所有设备中最简单最常见的设备。在linux中,有一种最简单的串口设备在/dev下表现的是字符设备(c),字符设备可以被打开并且读写。

我认识的第一个最简单的串口设备是一个usb接口的gps模块,如下图 fmvi8se8 它是我目前为止,用过的最简单的串口设备,它只需要读,无需写,单向通讯,我只需要打开字符设备,然后不停的从中读取数据就可以了,里面的数据是GPS定位信息相关的字符串,根据它的文档,进行字段解析,就能获取到经纬度、时间、角度、速度等信息。我曾经用golang写过一个一个程序获取这些信息 sbtao9zf 这个程序,实际上可以算是一个用户态的设备驱动程序。

我认识的第二个串口设备是一套蓝牙模块,用了usb转串口线连接蓝牙模块 ddlsq0na 按照设备的文档,我将它通过usb转串口线连接,然后将usb插上电脑,成功识别到串口。然后手机通过客服提供的调试app连接蓝牙信号,电脑上用串口读写软件将串口打开,手机上发送信息,电脑串口上能收到信息,电脑串口上还能发送特定的AT指令对设备进行查询与设置。 3wwg8uas 值得注意的是,我在连接模块的过程中,将usb转串口上的tx连接了模块tx,rx连接了模块的rx,导致无法收发消息,客服帮我找出问题并告诉我应该将tx连接rx,rx连接tx。想到这一点我恍然大悟,这跟rust里的channel,go里的chan,以及linux上的通用管道一样,都属于管道模型,对端的输出接入到设备的输入,对端才能将信息传给设备。后续我想我会进一步对这个蓝牙模块编写用户态驱动程序,进而尝试编写内核态驱动程序。

其实早在我认识GPS串口设备之前,我也试过温湿度传感器DHT22,但是,这玩意儿根本不是串口,它只有一个数据接口(DATA),也就是不像串口那样分rx,tx,它需要通过gpio口连接DATA针,然后通过高电位低电位来实现双向通讯,从而获取到温湿度数据。它既然不是串口设备那么也不能作为通用设备直插一般的电脑了。

在我看来,程序能驱动物理设备,也是一件很有趣的事情。


初学rust,如何实现在运行时对全局变量设置和读取
Tag 运行时, 全局变量, 读写, on by view 19

在rust中,如何实现在运行时对全局变量设置和读取,这个问题困扰了我一段时间。因为在rust中,全局变量是不能在运行时修改的。rust的全局变量是属于全局静态变量,使用关键词static来定义,如下:

static mut count: usize = 0;

pub fn init_config(file_path: &str) {
    count = count + 1;
}

编译器会告诉你修改count不安全(unsafe)。

➜  las git:(feature/self-update-with-watcher-fork) ✗ cargo build
   Compiling las v0.1.1 (/root/code/las)
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
  --> src/config/cfg.rs:35:13
   |
35 |     count = count + 1;
   |             ^^^^^ use of mutable static
   |
   = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

error[E0133]: use of mutable static is unsafe and requires unsafe function or block
  --> src/config/cfg.rs:35:5
   |
35 |     count = count + 1;
   |     ^^^^^^^^^^^^^^^^^ use of mutable static
   |
   = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `las` (bin "las") due to 2 previous errors

一定要修改的话,需要加unsafe块包裹住:

static mut count: usize = 0;

pub fn init_config(file_path: &str) {
    unsafe { count = count + 1 };
}

但是,在rust中使用unsafe是一件不好的事情,我们应该尽量避免使用unsafe,这里有另一个方案可以实现在运行时设置全局变量,就是使用once_cell这个包,虽然它的底层也是基于unsafe来实现的,但是它对“不安全”代码进行了一定的包装。具体用法如下:

static CFG: OnceCell<Conf> = OnceCell::new();

// 初始化和设置
pub fn init_config(file_path: &str) {
    CFG.get_or_init(|| load_config(file_path));
}

// 读取
pub fn get_config() -> &'static Conf {
    CFG.get().unwrap()
}

可以看到,我们可以在运行时使用once_cellget_or_init方法对全局变量进行初始化设置,它还有set方法也可以在运行时对全局变量进行设置,get方法在运行时对全局变量进行读取。从而实现不直接使用unsafe块,来操作全局变量。而且once_cell使用的全局变量完全不必申明为可变(mut)变量。

我们可以通过追踪once_cell::get_or_init方法,可以看到在initialize方法中unsafe块中,将value设置给了*slot

qby9tqm9

可以看到,once_cell对全局变量的操作进行了安全封装,因此,建议使用once_cell进行全局变量的操作,而不是到处使用unsafe块进行不安全的操作。


服务程序实现自己更新自己
Tag 服务, 自更新, on by view 19

作为一个广泛存在于众多设备上的常驻程序如何更新?他可以是一个agent,也可以是一个service,可以运行在众多容器中作为基础支持的程序,也可以运行在路由器、空调、洗衣机等家用设备或者物联设备上的支持程序。或许有些程序可以通过第三方平台进行更新,比如某些服务程序,配有专门的发版平台。但是更多的是无法通过发版平台发布的程序,比如是linux上的一个自启动的service,这个linux是你家里的路由器。这时候就需要程序自己更新自己了。

程序更新通常有下列步骤,下载新的程序,替换旧的可执行文件和配置文件,重启程序(退出旧进程,拉起新进程)。

程序自己如何去做这些操作?通常下载和替换文件并不难,只需要有一个接口能查询到新的程序版本和下载地址,很容易实现下载新版程序和替换。唯一需要注意一点的是重启程序这一步骤,这一步骤在业务程序进程上是无法实现的,因为它要拉起一个业务进程(worker),并且退出自己,在它拉起业务进程的时候,如果自己本身就是业务进程(worker),这样就会有两个业务进程(worker),这对于大多数服务是不允许的,会出错。那么我们可以专门设计一个守护进程(watcher)仅用于程序的更新与重启。

因此watcher进程里,更新程序和重启程序有如下流程 hhy70tzc

下载替换与杀旧进程(worker)放在一个线程中,另一个线程只负责监视worker进程是否存在,如果worker进程不存在,则拉起worker进程。

同一个程序中,如何区分worker进程与watcher进程?我这里使用的是特定环境变量来区分的。终端中拉起程序,默认启动的是watcher进程,watcher进程启动后检测到worker进程不存在,那么它将会通过exec.Command拉起进程,并且注入特定环境变量,如下

cmd := exec.Command(procName, os.Args[1:]...)
cmd.Env = append(os.Environ(), "PROC_FLAG=worker")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if e := cmd.Start(); nil != e {
	log.Printf("ERROR: %v\n", e)
	return
}

而且程序启动的时候,首先也会判断该环境变量是否存在,如果存在该环境变量,那么就进入业务进程的业务逻辑。如果不存在该环境变量,则进入守护进程的守护逻辑。

这样一来就能够实现程序自己更新自己了。


初学rust,没有途径修改argv[0]
Tag argv0, rust, 进程标题, on by view 23

我们知道在C语言程序中,可以通过修改argv[0]的值,来实现改变一个进程在ps命令中显示的标题,先给一个C语言的demo如下:

#include <stdio.h>
#include <string.h>
  
extern char **environ;
  
int main(int argc , char *argv[]) {
    int i;
  
    printf("argc:%d\n" , argc);
    for (i = 0; i < argc; i++) {
        printf("argv[%d]:%s\t0x%x\n" , i , argv[i], argv[i]);
    }
  
    for (i = 0; i < argc && environ[i]; i++) {
        printf("evriron[%d]:%s\t0x%x\n" , i , environ[i], environ[i]);
    }

    strcpy(argv[0], "nginx: process is shuting down");

    sleep(1000);
    return 0;
}

进程原本的名称是demo,但是我们通过strcpy修改了argv[0]之后,ps命令显示进程的名称变为我们指定的nginx: process is shuting down,如下

➜  build git:(master) ✗ ps -ef | grep nginx
root     1263942 1252322  0 15:29 pts/2    00:00:00 nginx: process is shuting down
root     1263973 1253542  0 15:29 pts/3    00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox nginx

这也是nginx程序实现在修改进程标题为nginx: master, nginx: worker 以及 nginx: process is shuting down 的原理,从而实现在ps的标题中标识不同类别的进程。

我们在rust语言中如何达到相同的效果呢,查阅资料了解到,通过下面方法可以修改进程名

use nix::libc::{PR_SET_NAME, prctl};
use std::ffi::CString;

fn main() {
    let new_name = CString::new("NewProcessName").expect("CString::new failed");
    unsafe {
        prctl(PR_SET_NAME, new_name.as_ptr() as usize, 0, 0, 0);
    }
}

但是实际使用后,发现,这种方法修改的进程名并不是ps命令显示的进程标题,ps命令显示的进程标题还是不变,而是修改了pstree命令显示的进程树种的进程名称,所以,这种方法并不能达到我们想要的效果。

我们尝试修改argv[0],在rust中是通过env::args()来获取程序的传参,也即argv,追踪到env::args()调用的是env::args_os(),于是我们有这么一段代码尝试修改argv[0]:

use std::env;
use std::ffi::{CStr, CString, OsString};
use std::os::unix::ffi::OsStringExt;

fn set_process_title(title: &str) {
    let args: Vec<OsString> = env::args_os().collect();
    let mut argv: Vec<*mut i8> = args.iter()
        .map(|arg| {
            let arg_cstring = CString::new(arg.as_bytes()).expect("Failed to create CString");
            arg_cstring.into_raw()
        })
        .collect();
    argv.push(std::ptr::null_mut());

    let title_cstring = CString::new(title).expect("Failed to create CString");

    unsafe {
        strcpy(argv[0] as *mut c_char, title_cstring.as_ptr());
    }
}

fn main() {
    set_process_title("MyWorker");

    // 继续执行其他操作...
}

但是很遗憾,并没有修改argv[0]的效果,继续追踪args_os()发现,其实在多个地方存在clone()操作,我们获取到的argv早就不是原始的argvargs_os()的实现如下

#[stable(feature = "env", since = "1.0.0")]
pub fn args_os() -> ArgsOs {
    ArgsOs { inner: sys::args::args() }
}

sys::args::args()的实现如下

/// Returns the command line arguments
pub fn args() -> Args {
    imp::args()
}

imp::args()的实现如下

pub fn args() -> Args {
    Args { iter: clone().into_iter() }
}

这里clone()实现如下

fn clone() -> Vec<OsString> {
    unsafe {
        // Load ARGC and ARGV, which hold the unmodified system-provided
        // argc/argv, so we can read the pointed-to memory without atomics
        // or synchronization.
        //
        // If either ARGC or ARGV is still zero or null, then either there
        // really are no arguments, or someone is asking for `args()`
        // before initialization has completed, and we return an empty
        // list.
        let argv = ARGV.load(Ordering::Relaxed);
        let argc = if argv.is_null() { 0 } else { ARGC.load(Ordering::Relaxed) };
        let mut args = Vec::with_capacity(argc as usize);
        for i in 0..argc {
            let ptr = *argv.offset(i) as *const libc::c_char;

            // Some C commandline parsers (e.g. GLib and Qt) are replacing already
            // handled arguments in `argv` with `NULL` and move them to the end. That
            // means that `argc` might be bigger than the actual number of non-`NULL`
            // pointers in `argv` at this point.
            //
            // To handle this we simply stop iterating at the first `NULL` argument.
            //
            // `argv` is also guaranteed to be `NULL`-terminated so any non-`NULL` arguments
            // after the first `NULL` can safely be ignored.
            if ptr.is_null() {
                break;
            }

            let cstr = CStr::from_ptr(ptr);
            args.push(OsStringExt::from_vec(cstr.to_bytes().to_vec()));
        }

        args
    }
}

可以看到argv是从ARGV.load(Ordering::Relaxed);中加载出来的。可以看到argv经历了多次拷贝,最终才到args,然后通过args_os()再呈现在我们面前,实际早就不再是最初的那个argv。