<-- Home |--rust |--c

Process in Rust中的进程编程

系统编程

系统运行模式

我学«微机原理»的时候在20十几年前,那个时候的教材是基于8086的,现在的计算机已经过于复杂,我现在已经完全看不太懂体系结构的文章。但是,大概的系统运行模式应该还是差不多的。

在我的概念里,计算机系统依然是硬件、操作系统、应用软件构成的。硬件和操作系统的内核运行在内核模式、操作系统的部分功能和应用软件运行在用户模式。大概就是这样,如果不对,请指正。

系统运行模式

操作系统的最底层的基本概念就是进程的概念。进程是一个独立的运行单元,包括独立的地址空间、独立的资源、独立的执行路径。

进程

大概而言,进程的运行状态转移过程如下:

stateDiagram-v2
    [*] --> Running(R)
    Running(R) --> Sleep(S): sleep(10)
    Sleep(S) --> Running(R): 10 seconds later
    Running(R) --> Zombie(Z): kill()
    Running(R) --> Stopped(T): Signal(SIGSTOP)
    Stopped(T) --> Running(R): Signal(SIGCONT)
    Zombie(Z) --> [*]: exit()

启动一个进程,操作系统会为进程分配一个唯一的进程ID,并创建一个进程控制块(PCB),PCB是进程的描述信息,包括进程的运行状态、进程的优先级、进程的地址空间等。

进程受到各种信号的影响,会进行状态的转移转移,例如,进程收到SIGSTOP信号,会从Running状态转移到Stopped状态;从Stopped状态收到SIGCONT信号,会从Stopped状态转移到Running状态。

进程收到SIGKILL信号,会从Running状态转移到Zombie状态,进程退出。

大概就是这样。

在Linux系统中,第一个创建的进程,是init进程,是所有进程的父进程。它的进程ID是1。在UNIX类似的系统中,进程创建子进程的方式特别好玩,它会调用一个函数fork(),这个函数会创建一个与父进程完全相同的进程,子进程会克隆父进程;创建完成后,子进程除了PID跟父进程不同,其他都一样。随后,操作系统会替换子进程的代码段,执行新的代码,这样就实现了进程的创建,通常,新的进程会执行某个程序,用exec()函数替换代码段。

信号列表

下面是Linux的Man Page中给出的信号列表。

信号标准动作说明
SIGABRTP1990CoreAbort signal from abort(3)
SIGALRMP1990TermTimer signal from alarm(2)
SIGBUSP2001CoreBus error (bad memory access)
SIGCHLDP1990IgnChild stopped or terminated
SIGCLD-IgnA synonym for SIGCHLD
SIGCONTP1990ContContinue if stopped
SIGEMT-TermEmulator trap
SIGFPEP1990CoreErroneous arithmetic operation
SIGHUPP1990TermHangup detected on controlling terminal or death of controlling process
SIGILLP1990CoreIllegal Instruction
SIGINFO--A synonym for SIGPWR
SIGINTP1990TermInterrupt from keyboard
SIGIO-TermI/O now possible (4.2BSD)
SIGIOT-CoreIOT trap. A synonym for SIGABRT
SIGKILLP1990TermKill signal
SIGLOST-TermFile lock lost (unused)
SIGPIPEP1990TermBroken pipe: write to pipe with no readers; see pipe(7)
SIGPOLLP2001TermPollable event (Sys V); synonym for SIGIO
SIGPROFP2001TermProfiling timer expired
SIGPWR-TermPower failure (System V)
SIGQUITP1990CoreQuit from keyboard
SIGSEGVP1990CoreInvalid memory reference
SIGSTKFLT-TermStack fault on coprocessor (unused)
SIGSTOPP1990StopStop process
SIGTSTPP1990StopStop typed at terminal
SIGSYSP2001CoreBad system call (SVr4); see also seccomp(2)
SIGTERMP1990TermTermination signal
SIGTRAPP2001CoreTrace/breakpoint trap
SIGTTINP1990StopTerminal input for background process
SIGTTOUP1990StopTerminal output for background process
SIGUNUSED-CoreSynonymous with SIGSYS
SIGURGP2001IgnUrgent condition on socket (4.2BSD)
SIGUSR1P1990TermUser-defined signal 1
SIGUSR2P1990TermUser-defined signal 2
SIGVTALRMP2001TermVirtual alarm clock (4.2BSD)
SIGXCPUP2001CoreCPU time limit exceeded (4.2BSD); see setrlimit(2)
SIGXFSZP2001CoreFile size limit exceeded (4.2BSD); see setrlimit(2)
SIGWINCH-IgnWindow resize signal (4.3BSD, Sun)

在不同的操作系统中,信号的支持和实现可能是不一样的。

C语言的进程编程

记得当时学习C语言的进程编程时,一堆人死活都搞不懂,我反正也是很糊涂。

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <sys/types.h>
 4#include <sys/wait.h>
 5
 6int main()
 7{
 8    pid_t pid;
 9    printf("Parent process starting, PID: %d\n", getpid());
10
11    pid = fork();
12    if (pid < 0)
13    {
14        perror("fork");
15        return 1;
16    }
17
18    if (pid == 0)
19    {
20        printf("Child process, PID: %d, Parent PID: %d\n", getpid(), getppid());
21        char *args[] = {"ls", "-l", NULL};
22        execv("/bin/ls", args);
23        perror("execv /bin/ls failed.");
24        return 127;
25    }
26    else
27    {
28        printf("Parent process continuing, PID: %d, Child PID: %d\n", getpid(), pid);
29        // wait for child process to exit
30        int status;
31        wait(&status);
32        printf("Child process exited with status %d\n", status);
33    }
34    return 0;
35}

我们在wsl下面可以运行下面的命令:

1gcc process.c -o process
2./process

运行的结果其实挺好玩的:

 1 ./build/process 
 2Parent process starting, PID: 3973
 3Parent process continuing, PID: 3973, Child PID: 3974
 4Child process, PID: 3974, Parent PID: 3973
 5total 24
 6-rwxrwxrwx 1 qchen qchen   265 May  7 09:37 CMakeLists.txt
 7-rwxrwxrwx 1 qchen qchen   159 May  7 09:39 Cargo.lock
 8-rwxrwxrwx 1 qchen qchen    86 May  7 09:38 Cargo.toml
 9drwxrwxrwx 1 qchen qchen  4096 May  7 15:05 build
10-rwxrwxrwx 1 qchen qchen   819 May  7 15:05 process.c
11-rwxrwxrwx 1 qchen qchen 19293 May  7 10:41 process.png
12drwxrwxrwx 1 qchen qchen  4096 May  7 09:38 src
13drwxrwxrwx 1 qchen qchen  4096 May  7 09:43 target
14Child process exited with status 0

首先,在父进程中,fork()返回的子进程的PID是3974,在子进程中,getpid()返回的PID也是3974,getppid()返回的父进程的PID是3973。但是在子进程中,变量pid的值是0。这样就把父进程和子进程区分开了。反正还是挺绕圈的。

当然我也准备了cmakelist.txt,可以运行下面的命令。

1cmake -B build
2cmake --build build
3./build/process
 1cmake_minimum_required(VERSION 3.25)
 2project(process_in_rust C)
 3
 4# Set C standard
 5set(CMAKE_C_STANDARD 11)
 6set(CMAKE_C_STANDARD_REQUIRED ON)
 7
 8# Add executable
 9add_executable(process process.c)
10
11# cmake -Bbuild
12# cmake --build build
13# ./build/process

从这个程序可以看到,父进程和子进程实际上共享了同一个程序的源代码,仅仅通过pid来区分。整个逻辑理解起来特别的困难。

Rust的进程编程

Rust的进程编程,其实和C语言的进程编程是类似的,只不过Rust的进程编程看起来更加清晰。

 1use nix::unistd::getppid;
 2use std::io::Read;
 3use std::os::unix::process::CommandExt;
 4use std::process::{Command, Stdio};
 5
 6fn main() -> std::io::Result<()> {
 7    println!("Parent process starting, PID: {}", std::process::id());
 8
 9    // 创建子进程
10    let mut command = Command::new("ls");
11    command.arg("-l");
12
13    unsafe {
14        command.pre_exec(|| {
15            println!(
16                "Child process starting, PID: {}, PPID: {}",
17                std::process::id(),
18                getppid().as_raw()
19            );
20            Ok(())
21        });
22    }
23
24    let mut child = command.spawn()?;
25
26    // 等待子进程完成并获取输出
27    let mut output = String::new();
28    if let Some(mut stdout) = child.stdout.take() {
29        stdout.read_to_string(&mut output)?;
30    }
31
32    // 等待子进程结束并获取状态
33    let status = child.wait()?;
34
35    println!("Child process output:\n{}", output);
36    println!("Child process exited with status: {}", status);
37
38    Ok(())
39}

运行结果如下:

 1Parent process starting, PID: 5294
 2Child process starting, PID: 5376, PPID: 5294
 3total 28
 4-rwxrwxrwx 1 qchen qchen   265 May  7 09:37 CMakeLists.txt
 5-rwxrwxrwx 1 qchen qchen  1643 May  7 15:23 Cargo.lock
 6-rwxrwxrwx 1 qchen qchen   112 May  7 15:23 Cargo.toml
 7drwxrwxrwx 1 qchen qchen  4096 May  7 15:05 build
 8-rwxrwxrwx 1 qchen qchen   819 May  7 15:06 process.c
 9-rwxrwxrwx 1 qchen qchen 19293 May  7 10:41 process.png
10drwxrwxrwx 1 qchen qchen  4096 May  7 09:38 src
11drwxrwxrwx 1 qchen qchen  4096 May  7 09:43 target
12Child process output:
13
14Child process exited with status: exit status: 0

为了证明,Rust子进程的PPID就是父进程的PID,又是引入libc/nix库,又是CommandExt,还整了unsafe,真是太麻烦了。

1[package]
2name = "process-in-rust"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7libc = "0.2"
8nix = "0.26"

如果不需要搞这些玩意,实际上这个代码可以很简单很简单。

 1use std::process::Command;
 2
 3fn main() -> std::io::Result<()> {
 4    let output = Command::new("ls")
 5        .arg("-l")
 6        .output()
 7        .expect("failed to run ls");
 8    println!(
 9        "Child process output: {}",
10        String::from_utf8_lossy(&output.stdout)
11    );
12    Ok(())
13}

简直是非常简单明了,整个都很清晰,output()会等待子进程结束,并返回子进程的输出。

总结

Rust的进程编程,其实和C语言的进程编程是类似的,只不过Rust的进程编程看起来更加清晰。


文章标签

|-->rust |-->process |-->system |-->system programming |-->fork |-->exec |-->signal |-->getpid |-->getppid


GitHub