程序和进程

程序是二进制文件,在磁盘上,不占用系统资源(CPU,内存,打开的文件,设备,锁….)

进程,比较抽象的概念,活跃的程序,占用系统资源

可以通俗理解为:程序是剧本,进程是上演的戏剧

需要知道的前置知识

并发

单道程序设计:DOS系统,程序只能排队占用CPU执行,

多道程序设计:想要实现多开进程,而CPU如果只有一个,需要划分时间分片设置一个时间中断,硬件手段,给打开的多个程序时间中断,给进程们进行分配CPU的使用时限,由于CPU计算相当快,人眼看不到停止轮次,就会认为是并行运行,实际上是并发

CPU和MMU

CPU:硅,去沙滩抓把沙子做一做就弄明白了
请移步机组文章

这里单独拿出MMU

MMU基本工作原理

MMU是一个内存管理单元,在CPU中

虚拟内存只是个概念,需要MMU将虚拟内存的虚拟地址映射在对应的物理地址上。除了映射功能,还能设置内存访问的级别,一般英特尔cpu设定的内存访问级别都有四个级别

linux只使用了3级与0级两种级别

0级的内核空间,3级就是用户空间

在运行的两个进程,即使程序相同,但是用户区的映射的不是同一块的物理进程,内核区指向的物理内存实在同一块,但是PCB是不一样的,具体还是请移步机组文章

进程控制块

进程描述符(linux系统下的通用称呼)

PCB本质是一个结构体组

结构体成员

  • 进程id,系统中每个进程有唯一的id,
  • 进程的状态
    1. 初始化
    2. 就绪:等待CPU分配时间片
    3. 运行
    4. 挂起:等待除了CPU以外的其他资源,主动放弃CPU,可以理解为阻塞状态
    5. 终止
  • 进程切换时需要进行保存的和恢复的一些CPU资源和寄存器的值。
  • 描述虚拟地址空间的信息
    • MMU维护这一个虚拟地址映射到物理地址的表
  • 描述控制终端的信息
    • 运行该进程的终端信息呗
  • 当前工作目录
    • 当前进程的工作目录,具体请看chdir函数
  • umask掩码
    • 文件的保护控制权限用
  • 文件描述符表
    • 包含很多指向文件结构体的指针,就是fd,
  • 信号相关的信息
  • 用户id和组id
  • 会话(Session)和进程组
  • 进程可以使用的资源上限(Resource Limit)
    • 使用ulimit -a命令查看linux中的所有的进程组使用的资源上限

环境变量

linux是什么系统,多用户多任务的开源操作系统

每个用户的配置都不一样,每个人都有不同的启动桌面,不同的默认工具设置,等等,实际上就是环境变量不一样

我们所常见的全局环境变量,用户环境变量可以这么理解

几个常用环境变量

  • PATH:二进制的执行文件路径
  • SHELL:当前的命令解析器
  • LANG:当前的语言

环境变量的打印

1
2
3
4
5
6
7
8
9
#include <stdio.h>
extern char** environ;
int main(){
for (int i = 0; environ[i] != NULL; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}

几个环境变量的函数

  • setenv()
  • getenv()
  • clearenv()

使用示例

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(){
char* val;
const char* name = "ABCDE";

val = getenv(name);
printf("1,%s = %s\n", name, val);

setenv(name, "自定义一个环境变量", 1);
val = getenv(name);
printf("2,%s = %s\n", name, val);

# if 1
int ret = unsetenv("ABCDE");
printf("ret: %d\n", ret);
val = getenv(name);
printf("3,%s = %s\n", name, val);

#else
int ret = unsetenv("ABCDE");
printf("ret: %d\n", ret);
val = getenv(name);
printf("3,%s = %s\n", name, val);

#endif
return 0;
}

进程控制

fork函数

通过函数fork创建一个子进程

fork函数有两个返回值,一个是是子进程的id,一个是flag(是创建进程成功)

父进程的fork返回的是子进程的id,子进程fork返回的是flag

创建一个子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(){

//主进程
printf("主进程测试");
__pid_t pid;
pid = fork();
if (pid == -1)
{
perror("fork error");
_exit(1);
}else if(pid == 0){
printf("我是子进程,pid= %u,我的父进程的 ppid= %u\n", getpid(), getppid());
}else{
printf("我是主进程,pid= %u,我的父进程的 ppid= %u\n", getpid(), getppid());
sleep(1);
//主进程睡眠1秒保证子进程先运行一会
}
printf("测试子进程会不会也执行\n");
return 0;
}

循环创建五个子进程

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
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
int i = 0;
__pid_t pid;
printf("主进程测试\n");

for (; i < 5; i++)
{
pid = fork();
if (pid == -1)
{
perror("fork error");
exit(1);
}else if (pid == 0){
break;
}
}
if(i < 5){
sleep(i);
//第一个进程睡眠一秒依次类推
printf("我是第%d子进程,pid是:%u, 父进程ppid是:%u\n", i+1, getpid(),getppid());
}else{
sleep(i);
printf("主进程结束\n");
}
return 0;
}

以上所有的进程实际上都在同时争夺CPU,需要做进程状态的控制才能保证到底是哪个进程先使用CPU

如果将所有的sleep休眠函数都删除,那么结果其实也是一样的,只是显示上有问题

父子进程的异同

主要是值的相同

相同:

  • 全局变量,
  • .data
  • .text
  • 环境变量
  • 用户ID
  • 宿主目录
  • 进程工作目录
  • 信号处理方式等

不同

  • 进程ID
  • fork返回值
  • 父进程ID
  • 进程运行时间
  • 定时器
  • 未决信号集

父子进程间遵循

读时共享,写时复制

共享的

  1. 文件描述符
  2. mmap建立的映射区

父子进程的调试

gdb调试默认父进程

在调用子进程函数之前
设置跟踪子进程,就可以走子进程的逻辑

1
set follow-fork-mode child

如果有多个子进程,那就需要设置好条件断点,通过进程的索引值比如你设定的id,或者直接使用pid判断来进入不同的子进程

exec函数族

除了子父进程,子进程还可以执行别的程序

换核不换壳,pid ppid都不变,改变的其实是代码区和等等。

利用的是exec函数族

execlp

exec函数族之一的函数

l 代表的是list(具体可以理解为就是指令参数0~n),而p 表示的是PATH环境变量

利用execlp函数实现子进程执行 环境变量中指定的地址二进制文件ls文件,打印当前的进程的目录下的文件
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
##include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
__pid_t pid;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(1);
printf("这里是父进程");
}else {
execlp("ls", "ls", "-a", "-l", NULL);
}
return 0;
}

这里解析一下execlp的使用:

第一个参数表示的是执行的是ls这个文件

第二个参数表示argv[0],第三个就是argv[1]依次类推

直到选项参数结束为NULL

假设我们要执行我们自己的程序

可以使用execl (不用指定环境变量)

除了以上的execlp和execl

还有execv和execvp, execvpe, execle

参数:

  • l(list) 参数列表
  • p(path) 参数的环境变量path
  • v(vector) 参数的数组
  • e(environment) 参数的所有的环境变量
    • 在使用e时,需要先导入环境变量表extren

回收子进程

孤儿进程

没爹没妈的进程

没有父进程回收,系统会将其父进程设置为init进程,可以理解为孤儿院领养了

1
让父进程先死,后子进程打印ID,打印的父进程ID是init进程ID

僵尸进程

父进程忘记给子进程结束后收尸了

子进程会残留在PCB中,成为僵尸进程,kill无法终止

代码示例

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
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
__pid_t pid;

pid = fork();
if(pid < 0){
perror("fork error");
exit(1);
}
if(pid == 0){
printf("我是子进程,pid: %u,父进程ppid: %u\n", getpid(), getppid());
sleep(5);
printf("子进程:啊,我死了\n");
}else{
//主进程
//循环运行没有给结束的子进程进行收尸
//循环打印
while (1){
printf("我是父进程,pid: %u,我的子进程ppid: %u\n", getpid(), pid);
printf("主进程发呆中呀~\n");
sleep(1);
}
}
return 0;
}

在终端查看进程可以看见

看见一个状态为Z+ 进程名字使用中括号括起(一般买书的时候发现作者名字被中括号扩起说明作者已经过世了)
那么该怎么将上面这两种子进程回收呢

我们需要使用以下函数

wait函数

  • 阻塞主进程,等待子进程退出,然后回收

  • 获取子进程的结束状态

    根据wait的传出参数是一个整形数int* status,该怎么得知对应状态呢?

    需要系统提供的宏函数来进一步判断进程终止的具体原因,重点掌握

    • 正常结束:
    • WIFEXITED(status) 为非0 进程正常结束
    • WEXITSATUS(status) 如果是进程正常结束,返回进程的退出的值
    • 异常结束:
    • WIFSIGNALED(status) 为非0 进程异常结束
    • WTERMSIG(status) 如果进程异常结束,返回收到的是第几种信号造成的 结束
      • kill默认15号信号杀死进程
      • 我们常见的访问不可访问的内存空间会返回的是11号信号,表示的是段错误信号

代码示例

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
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
__pid_t pid;
__pid_t wpid;
int status;

pid = fork();
if(pid < 0){
perror("fork error");
exit(1);
}
if(pid == 0){
execl("02app", "02app", NULL);
printf("我是子进程,pid: %u,父进程ppid: %u\n", getpid(), getppid());
sleep(20);
printf("子进程:我死了\n");
exit(121);
}else{
printf("我是父进程,pid: %u,我的子进程ppid: %u\n", getpid(), pid);
sleep(1);
wpid = wait(&status);
if(wpid < 0){
perror("wait error");
exit(1);
}
if(WIFEXITED(status)){
printf("正常关闭,关闭的返回值为:%d\n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("异常关闭,受到的是信号为 %d 号\n", WTERMSIG(status));
}
}
return 0;
}

实际上可以这么说,几乎所有进程的异常终止,都是收到信号的影响

问题来了

一次wait函数调用能回收几个子进程?一个,想要回收多个子进程,需要多次调用

但是自动默认选择的是先结束的子进程。

显然这是有些局限的,不能指定回收哪一个子进程

问题解决:

waitpid函数

作用与wait大体相同,不同点,也是关键点

  • 可以选择不阻塞主进程,使用轮询方式判断子进程是否结束,需要设置第三个参数为WNOHANG(wait not hang)主进程不挂起

    回收指定的子进程并且不挂起主进程代码示例:

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
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
int i = 0;
__pid_t pid, wpid;
int count = 5;
__pid_t wpid3;
for ( i = 0; i < count; i++)
{
pid = fork();
if(pid == 0){
break;
}else if(pid == -1){
perror("fork error");
exit(1);
}else if(i == 2){
wpid3 = pid;
}
}
if(i < count){
sleep(i);
printf("我是第%d子进程,我的pid是: %u,我的父进程id是: %u\n", i+1, getpid(), getppid());
}else{
do{
wpid = waitpid(wpid3, NULL, WNOHANG);
}
while (wpid != wpid3);//当没有子进程回收跳出轮询
//这里有一个BUG,会极大的占用cpu资源,在下方的代码有修正
sleep(20);
printf("第三个进程结束成功回收\n");
}
printf("所有进程都回收成功");
return 0;

}

如果要回收所有的子进程并且不挂起,将waitpid的第一个参数和跳出轮询的判断设置为 -1(-1表示没有一个进程需要回收,如果收成功,返回的是回收成功的pid)

第一个参数有四个特殊的值

  • 大于0,表示回收指定的PID的进程
  • 0 ,回收本组的任意一个子进程
  • -1,表示回收任意一个子进程,等价于 wait(NULL);
  • 小于-1的值,表示回收指定的进程组GPID的任意子进程

总结

假设需要定义一个进程任务,任务内容要求主进程有三个子进程

第一个子进程实现ps_dup2,第二个子进程实现自定义的程序,第三个出段信号引发错误的程序。

最后主进程再不结束运行的情况下对所有进程进行回收

实现代码如下:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>

//子进程调用
void w1(){
printf("进程1");
int fd;
fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0777);
if(fd < 0){
perror("open");
exit(121);
}
dup2(fd, STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
close(fd);
}
void w2(){
printf("进程2");
execl("w2", "w2", NULL);
}
void w3(){
printf("进程3");
execl("w3", "w3", NULL);
}

int main(){
int i = 0;
pid_t pid, wpid;
int status;
int count = 3;
pid_t w3pid;//第三个进程
int n=0;//设定一个轮询次数,否则极大浪费cpu资源

for (; i < count; i++)
{
n += 1;
pid = fork();
if(pid == 0){
break;
}else if(pid == -1){
perror("fork error");
exit(1);
}else if(i == 2){
w3pid = pid;
//记录测试用
}
}

if(i == 0){
w1();
}else if(i == 1){
w2();
}else if(i == 2){
w3();
}else{
printf("轮询次数为:%d\n", n);
//回收所有的子进程
//轮询方式,不挂起主程序
do{
wpid = waitpid(-1, &status, WNOHANG);
if(wpid > 0){
n--;
if(WIFEXITED(status)){
printf("回收值:%d\n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("异常信号为:%d\n", WTERMSIG(status));
}
}
}while(wpid != -1 || n > 0 );
sleep(10);
}

printf("主程序时间已过,再次自动回收所有的子进程\n");
printf("轮询次数为:%d\n", n);
return 0;

}