OSTEP NOTES & HW:进程

本文最后更新于:2022年9月28日 凌晨

进程是什么

进程是操作系统为正在运行的程序提供的抽象

程序本身是没有生命周期的,它只是存在磁盘上面的一些指令与静态数据,那么操作系统如何创建并启动一个进程?

  • 将代码和静态数据从磁盘中加载到内存中进程的地址空间
    • 早期系统会把所有数据在程序运行前加载完成,现代操作系统使用懒加载,也即是在程序执行期间需要加载的代码或数据片段,才会加载
  • 为程序的运行时栈分配内存
  • 为堆分配内存
  • 一些别的初始化任务,比如在 UNIX 系统中,默认情况下每个进程都有 3 个打开的文件描述符(file descriptor),分别对应标准输入、输出和错误
  • 跳转到程序的main函数入口处运行

进程状态

进程可以处于以下状态之一

  • 运行 running
    • 正在被CPU执行
  • 就绪 ready
    • 已经准备好运行,可以转换为运行态
  • 阻塞 blocked
    • 一个进程执行了某种操作(比如向磁盘发起I/O请求),他就会被阻塞,直到发生其他事件时才会准备运行
    • 阻塞态的程序不可以直接转为运行态,需要先转为就绪态才可以转为运行态
  • 初始 initial
    • 进程刚被创建
  • 最终 final
    • 也被称为僵尸状态

进程的数据结构

进程的机器状态:

  • 进程可以访问的内存(地址空间)
  • 寄存器。许多指令明确地读取或更新寄存器,他们构成了该机器状态的一部分

以xv6为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};


// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };


// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};

进程API

fork

  • fork函数执行后,后将存在两个进程,且每个进程都会从 fork()的返回处继续执行
  • 新的子进程几近于对父进程的翻版:子进程获得父进程的栈、数据段、堆和执行文本段的拷贝
  • 执行 fork()之后,每个进程均可修改各自的栈数据、以及堆段中的变量,而并不影响另一进程
  • 父进程的fork返回值为子进程的pid,子进程的fork返回值为0

    wait

    父子进程的前后顺序是不固定的,如果父进程想要等待子进程结束之后再进行某种操作,父进程可以先调用 wait(),该系统调用会在子进程运行结束后才返回,从而延迟自己的执行,直到子进程执行完毕

exec

fork+exec可以让子进程执行与父进程不同的程序。给我可执行程序的名称以及需要的参数后,exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过 argv 传递给该进程

CPU虚拟化与进程

一些目标:

  • 如何提供有许多 CPU 的假象,使得让我们感觉同时有许多进程同时运行?
  • 性能:如何在不增加系统开销的情况下实现虚拟化?
  • 控制权:如何有效地运行进程,同时保留对 CPU 的控制?

运行进程的一个简化的方式:

操作系统:

  • 在进程列表上创建条目
  • 为程序分配内存,并将程序加载到内存中
  • 根据 argc/argv 设置程序栈
  • 清除寄存器
  • 执行 call main() 方法

进程:

  • 执行main开始的代码
  • return

操作系统:

  • 释放进程内存
  • 从进程列表中移除进程

但是这种方式存在一些问题:

  • 操作系统怎么能确保程序不做一些受限操作,如向磁盘发出 I/O 请求或获得更多系统资源(如 CPU 或内存)?
    • 进程必须能够执行 I/O 和其他一些受限制的操作,但又不能让进程完全控制系统
  • 当我们运行一个进程时,操作系统如何让它停下来并切换到另一个进程,从而实现虚拟化 CPU 所需的时分共享?

通过区分内核模式和用户模式,操作系统可以提供受保护的控制权转移:

在用户模式(user mode)下,应用程序不能完全访问硬件资源。在内核模式(kernel mode)下,操作系统可以访问机器的全部资源。还提供了陷入(trap)内核和从陷阱返回(return-from-trap)到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置

进程不可以不受约束的做所有它想做的事情,那么如果用户希望执行某种特权操作怎么办?

答案是内核小心地向用户程序暴露某些关键功能(通过提供几百个系统调用),如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存

要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令同时跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作,完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。为了保证在操作系统发出从陷阱返回指令时能够正确返回,必须确保存储足够的调用者寄存器。例如,在 x86 上,处理器会将程序计数器、标志和其他一些寄存器推送到每个进程的内核栈(kernel stack)上

因此,在上面运行进程的基础上,需要加以下操作:

  • 内核栈相关操作
  • trap与return-from-trap
  • 中断处理程序

进程的上下文切换(context switch)

  • 进程上下文切换概念:操作系统要为当前正在执行的进程保存一些寄存器的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)
  • 为了完成进程上下文切换,操作系统会执行一些底层汇编代码,来保存通用寄存器、程序计数器,以及当前正在运行的进程的内核栈指针,然后恢复寄存器、程序计数器,并切换内核栈,供即将运行的进程使用

chapter 4 HW

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ./process-run.py -l 5:100,5:100   



Time PID: 0 PID: 1 CPU IOs
1 RUN:cpu READY 1
2 RUN:cpu READY 1
3 RUN:cpu READY 1
4 RUN:cpu READY 1
5 RUN:cpu READY 1
6 DONE RUN:cpu 1
7 DONE RUN:cpu 1
8 DONE RUN:cpu 1
9 DONE RUN:cpu 1
10 DONE RUN:cpu 1

Stats: Total Time 10
Stats: CPU Busy 10 (100.00%)
Stats: IO Busy 0 (0.00%)


# 5:100表示一个程序由5条指令组成,每条指令是 CPU 指令的几率是100%

2、3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Time        PID: 0        PID: 1           CPU           IOs
1 RUN:cpu READY 1
2 RUN:cpu READY 1
3 RUN:cpu READY 1
4 RUN:cpu READY 1
5 DONE RUN:io 1
6 DONE WAITING 1
7 DONE WAITING 1
8 DONE WAITING 1
9 DONE WAITING 1
10 DONE WAITING 1
11* DONE RUN:io_done 1

Stats: Total Time 11
Stats: CPU Busy 6 (54.55%)
Stats: IO Busy 5 (45.45%)

4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Time        PID: 0        PID: 1           CPU           IOs
1 RUN:io READY 1
2 WAITING RUN:cpu 1 1
3 WAITING RUN:cpu 1 1
4 WAITING RUN:cpu 1 1
5 WAITING RUN:cpu 1 1
6 WAITING DONE 1
7* RUN:io_done DONE 1

Stats: Total Time 7
Stats: CPU Busy 6 (85.71%)
Stats: IO Busy 5 (71.43%)


Time PID: 0 PID: 1 CPU IOs
1 RUN:cpu READY 1
2 RUN:cpu READY 1
3 RUN:cpu READY 1
4 RUN:cpu READY 1
5 DONE RUN:io 1
6 DONE WAITING 1
7 DONE WAITING 1
8 DONE WAITING 1
9 DONE WAITING 1
10 DONE WAITING 1
11* DONE RUN:io_done 1

Stats: Total Time 11
Stats: CPU Busy 6 (54.55%)
Stats: IO Busy 5 (45.45%)

一个重要的标志是-S,它决定了当进程发出 I/O 时系统如何反应。将标志设置为 SWITCH_ON_END,在进程进行 I/O 操作时,系统将不会切换到另一个进程,而是等待进程完成。

5

1
2
3
4
5
6
7
8
9
10
11
12
Time        PID: 0        PID: 1           CPU           IOs
1 RUN:io READY 1
2 WAITING RUN:cpu 1 1
3 WAITING RUN:cpu 1 1
4 WAITING RUN:cpu 1 1
5 WAITING RUN:cpu 1 1
6 WAITING DONE 1
7* RUN:io_done DONE 1

Stats: Total Time 7
Stats: CPU Busy 6 (85.71%)
Stats: IO Busy 5 (71.43%)

6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ ./process-run.py -l 3:0,5:100,5:100,5:100 -S SWITCH_ON_IO -I IO_RUN_LATER -c -p

Time PID: 0 PID: 1 PID: 2 PID: 3 CPU IOs
1 RUN:io READY READY READY 1
2 WAITING RUN:cpu READY READY 1 1
3 WAITING RUN:cpu READY READY 1 1
4 WAITING RUN:cpu READY READY 1 1
5 WAITING RUN:cpu READY READY 1 1
6 WAITING RUN:cpu READY READY 1 1
7* READY DONE RUN:cpu READY 1
8 READY DONE RUN:cpu READY 1
9 READY DONE RUN:cpu READY 1
10 READY DONE RUN:cpu READY 1
11 READY DONE RUN:cpu READY 1
12 READY DONE DONE RUN:cpu 1
13 READY DONE DONE RUN:cpu 1
14 READY DONE DONE RUN:cpu 1
15 READY DONE DONE RUN:cpu 1
16 READY DONE DONE RUN:cpu 1
17 RUN:io_done DONE DONE DONE 1
18 RUN:io DONE DONE DONE 1
19 WAITING DONE DONE DONE 1
20 WAITING DONE DONE DONE 1
21 WAITING DONE DONE DONE 1
22 WAITING DONE DONE DONE 1
23 WAITING DONE DONE DONE 1
24* RUN:io_done DONE DONE DONE 1
25 RUN:io DONE DONE DONE 1
26 WAITING DONE DONE DONE 1
27 WAITING DONE DONE DONE 1
28 WAITING DONE DONE DONE 1
29 WAITING DONE DONE DONE 1
30 WAITING DONE DONE DONE 1
31* RUN:io_done DONE DONE DONE 1

Stats: Total Time 31
Stats: CPU Busy 21 (67.74%)
Stats: IO Busy 15 (48.39%)


IO_RUN_LATER选项使得当 I/O 完成时,发出它的进程不一定马上运行,当时正在运行的进程一直运行;

使用-I IO_RUN_IMMEDIATE 设置,该设置立即运行发出I/O的进程

这种方式会让IO程序等待,IO程序如果优先使用CPU,当IO程序被阻塞的时候,其他进程被CPU调度,这种情况下的CPU利用率更高。

7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
./process-run.py -l 3:0,5:100,5:100,5:100 -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -c -p
Time PID: 0 PID: 1 PID: 2 PID: 3 CPU IOs
1 RUN:io READY READY READY 1
2 WAITING RUN:cpu READY READY 1 1
3 WAITING RUN:cpu READY READY 1 1
4 WAITING RUN:cpu READY READY 1 1
5 WAITING RUN:cpu READY READY 1 1
6 WAITING RUN:cpu READY READY 1 1
7* RUN:io_done DONE READY READY 1
8 RUN:io DONE READY READY 1
9 WAITING DONE RUN:cpu READY 1 1
10 WAITING DONE RUN:cpu READY 1 1
11 WAITING DONE RUN:cpu READY 1 1
12 WAITING DONE RUN:cpu READY 1 1
13 WAITING DONE RUN:cpu READY 1 1
14* RUN:io_done DONE DONE READY 1
15 RUN:io DONE DONE READY 1
16 WAITING DONE DONE RUN:cpu 1 1
17 WAITING DONE DONE RUN:cpu 1 1
18 WAITING DONE DONE RUN:cpu 1 1
19 WAITING DONE DONE RUN:cpu 1 1
20 WAITING DONE DONE RUN:cpu 1 1
21* RUN:io_done DONE DONE DONE 1

Stats: Total Time 21
Stats: CPU Busy 21 (100.00%)
Stats: IO Busy 15 (71.43%)


chapter 5 HW

1

父子进程的变量的值是独立的,一个进程对变量的修改并不会影响另一个变量的相同名字的值

2

父子进程具有相同的文件偏移量

3

fork之后父子进程的执行顺序是不固定的,但是vfork可以实现题意目的,vfork会使子进程先执行

vfork() differs from fork(2) in that the calling thread is suspended until the child terminates
(either normally, by calling _exit(2), or abnormally, after delivery of a fatal signal), or it
makes a call to execve(2).

4

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SYNOPSIS
#include <unistd.h>

extern char **environ;

int execl(const char *pathname, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *pathname, const char *arg, ...
/*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

引自https://stackoverflow.com/questions/5769734/what-are-the-different-versions-of-exec-used-for-in-c-and-c/5769803#5769803:

The differences are combinations of:

  1. L vs V: whether you want to pass the parameters to the exec’ed program as
    • L: individual parameters in the call (variable argument list): execl()execle()execlp(), and execlpe()
    • V: as an array of char* execv()execve()execvp(), and execvpe()
    The array format is useful when the number of parameters that are to be sent to the exec’ed process are variable – as in not known in advance, so you can’t put in a fixed number of parameters in a function call.
  2. E: The versions with an ‘e’ at the end let you additionally pass an array of char* that are a set of strings added to the spawned processes environment before the exec’ed program launches. Yet another way of passing parameters, really.
  3. P: The versions with ‘p’ in there use the environment variable PATH to search for the executable file named to execute. The versions without the ‘p’ require an absolute or relative file path to be prepended to the filename of the executable if it is not in the current working directory.

5

  • 父进程:返回子进程的进程号
  • 子进程:调用的进程wait()没有要等待的子进程,那么它会立即返回-1,指示错误。在这种情况下,errno将设置为ECHILD。可见man手册的return error内容

6

  • pid_t wait (int * status)
    • 子进程的结束状态值会由参数status返回,如果不在意结束状态值,则参数status可以设成NULL
  • pid_t waitpid(pid_t pid,int * status,int options)
    • 参数pid为欲等待的子进程识别码
      • pid<-1 等待进程组识别码为pid绝对值的任何子进程。
      • pid=-1 等待任何子进程,相当于wait()。
      • pid=0 等待进程组识别码与目前进程相同的任何子进程。
      • pid>0 等待任何子进程识别码为pid的子进程。
    • 子进程的结束状态返回后存于status

7

子进程在关闭标准输出后,printf无法打印到标准输出


OSTEP NOTES & HW:进程
http://gls.show/p/9e7f5052/
作者
郭佳明
发布于
2022年9月15日
许可协议