Linux进程

获取子进程和父进程的PID

1
2
3
pid_t getpid(void);	// 当前进程 ID

pid_t getppid(void); // 父进程 ID

fork–创建一个子进程

1
2
3
4
pid_t fork(void);

// 成功:父进程返回子进程的 pid;子进程返回 0
// 失败:父进程返回 -1,不会创建子进程,并且设置 errno

当 fork 成功返回,父子进程就创建成功了。但是,关于 fork 这个方法要讲得东西还是蛮多的,下面逐一介绍:

(一)父子进程的创建遵循写时复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Demo(){
int public_var = 10;
pid_t pid = fork();
switch (pid) {
case -1:
perror("Fork failed");
exit(1);
case 0: // Child process
public_var = 20;
printf("Child process: public_var = %d\n", public_var);
break;
default: // Parent process
sleep(3);
printf("Parent process: public_var = %d\n", public_var);
break;
}
}
/*
Child process: public_var = 20
Parent process: public_var = 10
*/

在这个程序中,可以让父进程睡眠 3s ,让子进程有机会修改 public_var 变量。如果父进程输出结果为 20,说明父子进程共享变量;如果父进程输出结果为 10,说明父子进程不共享变量。

子进程创建成功,此时父子进程是共用物理内存页的,对应下图中左半部分。可是当子进程开始修改对应物理页的数据,就会触发这块被修改的物理页写时复制机制(没有修改的物理页继续共享,谁修改谁拥有一块新的物理页并脱离原来的物理页),对应下图中右半部分。这就合理解释上述变量变与不变的现象了。

父子进程.png

(二)fork 与 用户态缓冲区的数据

fork与缓冲区.png

代码 A 和 代码 B 唯一的代码不同是 第一行的输出一个有换行符,一个没有换行符。

我们前面学习到,printf 函数是行缓冲区,即遇到换行符才会清空缓冲区。由于代码 A 没有换行符,导致用户态文件缓冲区中留有数据,那么 fork() 时,这部分数据也会复制,并且父子进程各自拥有自己的副本。

用两道面试题检验你:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Demo(){
for(int i = 0; i < 3; i++){
pid_t pid = fork();
printf("a");
}
}
// 24 个 a,缓存到最后才会输出。缓存 3 个 a,最后有8个进程,所以 3 * 8 = 24

void Demo(){
for(int i = 0; i < 3; i++){
pid_t pid = fork();
printf("a\n");
}
}

// 14 个 a,直接打印。第一次有两个进程,所以打印 2 个 a;第一次有死个进程,所以打印 4 个 a;第一次有八个进程,所以打印 8 个 a;合起来,就是 14 个 a

(三)父子进程的文件描述符

fork() 时,子进程会复制父进程的 PCB ,其中 PCB 中含有打开的文件描述符列表。因此,父子进程拥有各自的打开文件描述符列表。但是,它们共享同一个打开文件。

所以,当你父进程修改文件偏移量,子进程同样受影响,尽管它们是不同的文件描述符,但是指向相同的文件。

文件描述符.png

下面看代码示例:

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
void Demo() {
int fd = open("demo.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
printf("public pos: %ld\n", lseek(fd, 0, SEEK_CUR));

pid_t pid = fork();

int newfd;
switch (pid) {
case -1:
perror("Fork failed");
exit(1);
case 0: // Child process
write(fd,"Hello world\n", 11);
close(STDERR_FILENO);
newfd = dup(fd);
printf("Child process: newfd = %d\n", newfd);
break;
default: // Parent process
sleep(2);
printf("parent pos: %ld\n", lseek(fd, 0, SEEK_CUR));
newfd = dup(fd);
printf("Parent process: newfd = %d\n", newfd);
break;
}
}
/*
public pos: 0
Child process: newfd = 2
parent pos: 11
Parent process: newfd = 4
*

让父进程休眠 2s,以让子进程修改偏移量。如果最后父进程文件描述符的偏移量为 0 表明不会被子进程影响,如果不是就代表子进程和父进程虽然是不同的文件描述符,但是指向同一个文件,互相影响。

终止进程

正常终止

(一)_exit

1
void _exit(int status);

status 表示程序的终止状态,父经常可以调用 wait() 获取该状态。

(二)exit

程序一般不会直接调用 _exit() ,而是调用库函数 exit() ,它会在调用 _exit() 前执行各种动作。

1
void exit(int status);

执行动作如下:

  1. 调用退出处理程序 (通过 atexit() 和 on_exit() 注册的函数),其执行顺序与注册顺序相反。
  2. 刷新 stdio 流缓冲区。
  3. 将 status 作为参数,调用 _exit() 系统调用。

退出处理函数由用户事先注册,当进程调用 exit() 正常终止时,会自动执行事先注册的退出处理函数。

1
2
3
int atexit(void (*function)(void));

int on_exit(void (*function)(int , void *), void *arg); // 可以传递进程 状态(status)

它们的第一个参数就是用来提交的回调函数,并且可以提交多个回调函数,但是函数列表被执行是按照注册的顺序的相反顺序来执行。

异常终止

1
void abort(void);

会给调用进程 (自己) 发送 SIGABRT 信号,该信号会导致进程终止,并产生 core 文件

监控子进程

孤儿进程是父进程先于子进程结束的一种状态,但是不会对操作系统造成危害,因为会有 init 进程托管。

僵尸进程是子进程结束但是附近成没有感知到的一种状态,这种状态的进程不会被回收,会对操作系统造成影响,因为操作系统可以创建的进程数量是有限的。我们得确保父进程调用 wait 或 waitpid。

wait–等待子进程终止

1
pid_t wait(int *status);

调用 wait 的进程,会阻塞在 wait 方法处,直到有一个进程终止才解除阻塞,返回值是子进程的 PID。

status 参数是一个传出参数,<sys/wait.h> 头文件中定义了一组宏用于解析 status。

WIFEXITED(status) 宏用于检查子进程是否正常结束。

WEXITSTATUS(status) 宏用于获取正常结束时的退出码。

WCOREDUMP(status) 宏用于检查子进程产生 core 文件。这个宏并没有在 POSIX.1- 2001 标准中规定。因此,使用的时候,应该用 #ifdef WCOREDUMP … #endif 包裹起来。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <error.h>

void print_wstatus(int status) {
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
printf("exit_code = %d", exit_code);
} else if (WIFSIGNALED(status)) {
int signo = WTERMSIG(status);
printf("term_sig = %d", signo);
#ifdef WCOREDUMP
if (WCOREDUMP(status)) {
printf(" (core dump)");
}
#endif
}
printf("\n");
}

int main(int argc, char* argv[]) {
pid_t pid = fork();
switch (pid) {
case -1:
error(1, errno, "fork");
case 0:
// 子进程
printf("CHILD: pid = %d\n", getpid());
for(;;); // 使子进程进入无限循环
default:
// 父进程
int status; // 保存子进程的终止状态信息
pid_t childPid = wait(&status); // 阻塞点:一直等待,直到有子进程终止
if (childPid > 0) {
printf("PARENT: %d terminated\n", childPid);
print_wstatus(status);
}
exit(0);
}
return 0;
}

wait 存在的一些限制:

  • 如果有多个子进程, wait() 是无法等待某个特定子进程终止的, 只能依次等待每一个子进程终止。
  • 如果没有子进程终止, wait() 会一直阻塞。有时候会希望非阻塞的等待:如果没有子进程终止,立刻返回。
  • 只能监控子进程是否终止。对于子进程因某个信号 (如 SIGSTOP 或 SIGTTIN ) 而停止,或是已停止子进程收到 SIGCONT 信号后恢复执行, wait() 是无法监控这些情况的。

waitpid–等待子进程状态发生改变

1
pid_t waitpid(pid_t pid, int *status, int options);

pid 参数:

  • pid > 0,表示等待进程 ID 为 pid 的子进程。
  • pid = 0,表示等待同进程组的所有子进程。
  • pid = -1,表示等待任意子进程。 wait(&status) 与 waitpid(-1, &status, 0) 等价。
  • pid < -1,表示等待进程组 ID 为 |pid| 的所有子进程。

options 参数:

  • WNOHANG 不阻塞。如果参数 pid 指定的子进程没有一个发生状态改变,则立即返回, waitpid()的返回值为 0。
  • WUNTRACED 监控子进程是否因为某个信号而停止。
  • WCONTINUED 监控已停止的子进程是否收到 SIGCONT 信号而恢复执行。

明显看到 waitpid 要比 wait 功能丰富,它可以突破 wait 存在的一些限制。

执行程序

execve()

可以将新程序加载到当前进程的内存空间。在这一过程中,会执行如下操作:

  1. 清楚当前进程的代码段、数据段、堆、栈、上下文…
  2. 加载新的可执行程序,设置代码段,数据段等
  3. 从新可执行程序 main() 的第一行开始执行
1
2
3
4
5
int execve(const char *pathname, char *const argv[], char *const envp[]);

pathname:可执行程序路径
argv:可执行程序的参数,记得以NULL结尾
envp:环境变量,记得以NULL结尾

exec函数簇

下面这些库函数都是建立在系统调用 execve() 之上的,它们为执行新可执行程序提供了多种 API 选择。这些函数只是在指定程序名、命令行参数列表以及环境变量的方式上有所不同。

execve.png

system–执行 shell 命令

1
int system(const char *command);