《博学谷C++》四.1-5进程

1.学习目标

  • 掌握目录遍历相关的函数使用
  • 了解进程相关的概念
  • 掌握fork/getpid/getppid函数的使用
  • 熟练掌握ps/kill命令的使用
  • 熟练掌握execl/execlp函数的使用
  • 说出什么是孤儿进程和僵尸进程

2.进程和程序

进程是加载到内存后,运行的程序,可以理解为程序的执行过程。

进程是CPU分配资源的最小单位、

我们可以这么理解,公司相当于操作系统,部门相当于进程,公司通过部门来管理(系统通过进程管理),对于各个部门,每个部门有各自的资源,如人员、电脑设备、打印机等。(公司每个员工相当于线程。)

3.单道、多道程序设计

单道程序设计:

​ 多个程序排队执行。

多道程序设计:

​ 多个程序相互穿插运行,需要硬件支持。

4.并行和并发:

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

总结

并行是两个队列同时使用两台咖啡机;

并发是两个队列交替使用一台咖啡机。

5.MMU

内存管理单元(MMU,Memory Management Unit),是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,也负责也负责虚拟地址映射为物理地址等。

6.进程控制块PCB

每个进程有一个结构体task_struct,叫做进程控制块,保存进程的相关信息。

保存的文件路径:

/usr/src/kernels/4.18.0-305.3.1.el8.x86_64/include/linux/sched.h

PCB中需要掌握的部分:

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
  • 进程的状态,有就绪、运行、挂起、停止等状态。
  • 进程切换时需要保存和恢复的一些CPU寄存器。
  • 描述虚拟地址空间的信息。
  • 描述控制终端的信息。
  • 当前工作目录(Current Working Directory)。
  • umask掩码。
  • 文件描述符表,包含很多指向file结构体的指针。
  • 和信号相关的信息。
  • 用户id和组id。
  • 会话(Session)和进程组。
  • 进程可以使用的资源上限(Resource Limit)。

7.进程的状态

程序是静态的,而进程是动态的,因此进程才有各种运行过程中的各种状态。

三态模型:运行态,就绪态,阻塞态

五态模型:新建态、停止态,运行态,就绪态,阻塞态

就绪态:进程所有资源都满足,但还未被CPU调度;

运行态:被调度的就绪态进程;

僵尸态:子进程结束,但父进程还未释放进程描述符。僵尸态不可恢复为运行态。

①TASK_RUNNING:进程正在被CPU执行。当一个进程刚被创建时会处于TASK_RUNNABLE,表示己经准备就绪,正等待被调度。

 ②TASK_INTERRUPTIBLE(可中断):进程正在睡眠(也就是说它被阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒比如给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入TASK_RUNNABLE状态),然后再响应SIGKILL信号而退出(变为TASK_ZOMBIE状态),并不会从TASK_INTERRUPTIBLE状态直接退出。

  ③TASK_UNINTERRUPTIBLE(不可中断):处于等待中的进程,待资源满足时被唤醒,但不可以由其它进程通过信号或中断唤醒。由于不接受外来的任何信号,因此无法用kill杀掉这些处于该状态的进程。而TASK_UNINTERRUPTIBLE状态存在的意义就在于内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程,于是原有的流程就被中断了,这可能使某些设备陷入不可控的状态。处于TASK_UNINTERRUPTIBLE状态一般总是非常短暂的,通过ps命令基本上不可能捕捉到。

  ④TASK_ZOMBIE(僵死):表示进程已经结束了,但是其父进程还没有调用wait4或waitpid()来释放进程描述符。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。

  ⑤TASK_STOPPED(停止):进程停止执行。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。当接收到SIGCONT信号,会重新回到TASK_RUNNABLE

https://blog.csdn.net/zanda_/article/details/94784959

进程五态模型简明理解

7.1 ps命令

查看进程状态
ps -aux

选项 含义
-a 显示终端上的所有进程,包括其他用户的进程
-u 显示进程的详细状态
-x 显示没有控制终端的进程
-w 显示加宽,以便显示更多的信息
-r 只显示正在运行的进程

其中STAT是进程状态

7.2 top命令

动态显示运行中的进程,可以在使用top命令时加上-d 来指定显示信息更新的时间间隔。

7.3 kill命令

终止指定pid的进程,需要配合 ps 使用

kill [-signal] pid

信号值从0到15,其中9为绝对终止,可以处理一般信号无法终止的进程。

kill 8685

7.4 killall

可以根据进程名,杀死进程

1
2
3
4
5
6
7
8
9
10
11
[user@Mydesktop ~]$ sleep 3000&
[1] 2995
[user@Mydesktop ~]$ sleep 3000&`
[2] 3002`
[user@Mydesktop ~]$ sleep 3000&`
[3] 3009`

[user@Mydesktop ~]$ killall sleep`
[1] Terminated sleep 3000`
[2]- Terminated sleep 3000`
[3]+ Terminated sleep 3000`

7.5 pstree

显示进程树,方便可以看出父子关系,可以搭配killall使用

8.进程号和相关函数

进程号(PID)

标识进程的一个非负整型数。

父进程号(PPID)

任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。如,A 进程创建了 B 进程,A 的进程号就是 B 进程的父进程号。

进程组号(PGID)

进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID) 。这个过程有点类似于 QQ 群,组相当于 QQ 群,各个进程相当于各个好友,把各个好友都拉入这个 QQ 群里,主要是方便管理,特别是通知某些事时,只要在群里吼一声,所有人都收到,简单粗暴。但是,这个进程组号和 QQ 群号是有点区别的,默认的情况下,当前的进程号会当做当前的进程组号。

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main(void ){
pid_t pid = getpid();
printf("The process id:%d\n",pid);

printf("The parent process id:%d\n",getppid());
printf("The group process id:%d\n",getpgid(pid));
return 0;
}

9.子进程的创建

使用fork创建新的进程。

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

pid_t fork(void);
功能:
用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:

返回值:
成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
失败:返回-1
失败的两个主要原因是:
1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
2)系统内存不足,这时 errno 的值被设置为 ENOMEM。

以如下代码为例,说明进程的创建:

程序运行后输出两个”Hello World”,但每次的显示结果可能不一样,因为父子进程的执行顺序不确定(bash、和两个进程的关系是父、子、孙进程的关系)。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void ){
fork();
printf("Hello World\n");
return 0;
}

子进程几乎是父进程的复制品(代码段等等完全相同,进程号、计时器等少量信息除外)。但是子进程中,代码会从代码的fork函数之后开始执行(否则,子进程会再次通过fork创建孙进程,这样循环下去创建无数个进程)。

箭头表示父子进程的执行流程。

10.父子进程的数据

数据:

创建子进程时,系统会根据父进程的数据复制到子进程。为了减少费时的数据拷贝,Linux实行读时共享,写时拷贝

都在读取数据时,读取同一块数据;只要有一方写入,都会发生拷贝。

文件:

fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件文件偏移指针。

即,父子进程共享文件表项

11.区分父子进程

通过区分父子进程,让进程实现不同的功能。

为什么fork在子进程中返回0,却在父进程中返回子进程的id?

因为子进程可以分别通过getpid()和getppid()获得自己、父进程的id;而父进程无法获得子进程的id。

12.父子进程

地址空间

关于父子进程的数据,可以运行代码进行验证

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
int a = 10;     // 全局变量

int main()
{
int b = 20; //局部变量
pid_t pid;
pid = fork();
if (pid < 0)
{ // 没有创建成功
perror("fork");
}

if (0 == pid)
{ // 子进程
a = 111;
b = 222; // 子进程修改其值
printf("son: a = %d, b = %d\n", a, b); //结果:a= 111 b=222
}
else if (pid > 0)
{ // 父进程
sleep(1); // 保证子进程先运行
printf("father: a = %d, b = %d\n", a, b); //结果:a=10 b=20
}

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int a = 10; // 全局变量

int main()
{
int b = 20; //局部变量
int* p = NULL;
p = malloc(sizeof(int));
if(p == NULL){
printf("malloc failed...\n");
return 1;
}
memset(p,0,sizeof(int));
*p = 200;
pid_t pid;
pid = fork();
if (pid < 0)
{ // 没有创建成功
perror("fork");
}

if (0 == pid)
{ // 子进程
a = 111;
b = 222; // 子进程修改其值
printf("son before: *p = %d, a = %d, b = %d\n",*p, a, b); //结果:p=200, a=111 b=222
(*p)++;
printf("son after: *p = %d, a = %d, b = %d\n",*p, a, b); //结果:p=201,a= 111 b=222
//free(p); //子进程中释放指针
//p = NULL
}
else if (pid > 0)
{ // 父进程
sleep(1); // 保证子进程先运行
printf("father: *p = %d, a = %d, b = %d\n", *p, a, b); //结果:p=200, a=10 b=20
//free(p); //父进程中释放指针
//p = NULL
}

return 0;
}

可以看到,父、子进程均存在内存泄漏的问题。

执行free(p); 可以对堆空间进行释放,但是要注意:申请只有一次,但是要释放两次

13.gdb调试多进程

gdb默认调试父进程,通过set follow-fork-mode child调试子进程。

注意,一定要在fork函数之前设置。

14.进程退出函数

有四种退出方式:

return()、exit()、_exit()、_Exit()

1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>
void exit(int status);

#include <unistd.h>
void _exit(int status);
功能:
结束调用此函数的进程。
参数:
status:返回给父进程的参数(低 8 位有效),至于这个参数是多少根据需要来填写。
返回值:

区别在于,_exit()属于系统调用,而exit() 是库函数。并且存在缓冲区刷新的区别。

使用exit()和return()都会进行缓冲区刷新,但是_exit()不会,_Exit()等价于_exit()

15.清理子进程

wait()

阻塞父进程,等待子进程结束;如果没有子进程,立即返回。

waitpid()

通过设置可以自定义

16.孤儿进程

父进程结束,但子进程还在运行。

最后init进程作为”福利院“会”收养“接管孤儿进程,并在孤儿进程结束后释放资源,因此孤儿进程没有危害。

17.僵尸进程

一个进程已经终止,但是父进程没有回收其资源(”收尸“),其残留PCB资源还存放在内核中,就变成僵尸进程。

僵尸进程会造成资源不能被及时释放,会造成危害。

18.exec

exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段(进程替换)。因此进程号等不会变化,但是程序执行内容会被替换掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main (void){
printf("Hello world\n");

//int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
//execlp第一个参数是文件名,后面以此是各个选项、参数等,最后参数一定是空指针
//int execl(const char *path, const char *arg, ... /* (char *) NULL */);
//execl第一个参数是路径,绝对路径或相对路径

execlp("ls","ls","-l",NULL);//和下面几条等价
//execlp("/bin/ls","/bin/ls","-l",NULL);
//execl("/bin/ls","/bin/ls","-l",NULL);


printf("end\n");
return 0;
}

在执行execlp时,会载入指定的文件代码,并且从其main函数中开始执行(源文件execlp后面的代码就失效了)

其他四个类似的函数

1
2
3
4
5
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);