写在前面:本节我们学习了进程的相关基础内容,主要包括进程的概述,进程与程序的区别,进程的基本状态,父子进程,以及进程的GDB调试等相关内容;
一、进程概述
1.1程序与进程
程序是包含一系列信息的文件;
进程是正在运行的程序的实例;
程序是怎么产生的呢?我们可以简单认为,将代码写入一个文件,然后再经过编译、链接之后便可以生成一个可执行程序;但是这个可执行程序的本质还是文件,放在磁盘当中;
当我们将可执行程序进行运行时,本质上是将程序加载到内存当中,CPU会对程序进行执行,而一旦程序被加载到内存中,我们称为进程,也就是正在运行的程序;
进程我们认为它是由内核定义的抽象实体,操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
一个程序可以创建多个进程,一旦进程产生,与之对应的系统就会为进程分配用来执行程序的各类资源。
1.2单道、多道程序设计
单道程序:即在计算机内存中只允许一个的程序运行。
多道程序设计技术:是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。
我们需要注意的是,无论是单道还是多道,在任意某一时刻CPU只能执行一个进程,即使是多道程序,也是多个进程轮流使用 CPU;
时间片:是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。即便是一台计算机通常可能有多个 CPU,但是同一个 CPU 永远不可能真正地同时运行多个任务,只是轮番穿插地运行,由于时间片通常很短,感觉是多个进程同时在运行;
时间片由操作系统内核的调度程序分配给每个进程。
1.3并行与并发
并行:指在同一时刻,有多条指令在多个处理器上同时执行;
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行;
目前来说,我们还是对于并发的研究更加深入,因为并行的缺陷明显,需要高数量的多处理器,在一个系统中很难完成多处理器的集成;
并发是两个队列交替使用一台咖啡机。
并行是两个队列同时使用两台咖啡机。
1.4进程控制块(PCB)
为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(进程控制块,维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。主要包括的内容有:
二、进程状态转换
2.1进程的状态
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换;
进程的状态分为:三态模型与五态模型;
三态模型:
三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型 中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
运行态:进程占有处理器正在运行;
就绪态:进程具备运行条件,等待系统分配处理器以便运 行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进 程可能有多个,通常将它们排成一个队列,称为就绪队列。
阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程 不具备运行条件,正在等待某个事件的完成
五态模型:
在三态模型的基础上,增加一个新建态于终止态;
新建态:进程刚被创建时的状态,尚未进入就绪队列;
终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及 有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系 统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
2.2进程的相关指令与函数
查看进程:
ps aux / ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息 x:显示没有控制终端的进程
j:列出与作业控制相关的信息
ps aux:
USER :用户;PID:进程的ID; %CPU:CPU的使用率; %MEM :内存的使用率;TTY :当前进程所属的一个终端; STAT:状态;START:开始的时间,COMMAND:执行的命令;
ps ajx:
PPID:父进程ID;PID:进程的ID;PGID:进程组的ID;SID:会话的ID;
实时显示进程动态
top
可以在使用 top 命令时加上-d 来指定显示信息更新的时间间隔,在 top 命令 执行后,可以按以下按键对显示的结果进行排序;
动态进程信息显示
杀死进程
kill [-signal] pid
kill –l 列出所有信号
kill –SIGKILL 进程ID
kill -9 进程ID
killall name 根据进程名杀死进程
当前终端的进程无法杀死,kill+id不能杀死,需要用到kill -9 +id;直接杀死ID信号;
2.3进程号与相关函数
1、每个进程都由进程号来标识,其类型为 pid_t(整型),进程号的范围:0~32767。 进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
2、任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程, 对应的进程号称为父进程号(PPID)。
3、进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各 种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当 前的进程组号。
三、 父子进程
3.1fork函数
操作系统允许在一个进程中创建新的进程,新的进程称之为子进程,原进程称之为父进程;子进程还可以在创建新的子进程,从而形成进程树模型;
fork函数——一个用于创建子进程的函数;
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
作用:用于创建子进程;
返回值:fork的返回值会返回两次,一次是在父进程中,一次是在子进程中;
如果成功, 在父进程中返回子进程的ID,在子进程返回0;
如果返回失败,则在父进程中返回-1,子进程没有被创建,在errno中返回错误的值;
如何判断是父进程还是子进程?通过fork的返回值。
测试代码:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
//创建子进程
pid_t pid = fork();
//判断是父进程还是子进程;
if(pid>0)
{
printf("pid:%d\n",pid);
//如果大于0,返回的是创建的子进程的进程号;当前是父进程;
printf("i am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
}
else if(pid==0)
{
//当前是子进程;
printf("i am child process,pid:%d,ppid:%d\n",getpid(),getppid());
}
for(int i =0;i<3;i++)
{
printf("i=%d,pid:%d\n",i,getpid());
sleep(1);
}
return 0;
}
测试结果:
由上面的测试结果我们可知,此代码文件已运行,产生一个进程,该进程的进程号为:6600,此进程是由终端产生,那么该进程的父进程(终端)的进程号为:6330;
该代码进程中有一个fork函数,会产生一个子进程,产生的子进程的进程号为:6601;该子进程的父进程也就是由中断产生的,父进程号为:6600;
3.2父子进程虚拟地址空间
父进程与子进程在程序中是如何执行的?,父进程与子进程的相关虚拟地址空间问题;
在执行了fork函数后,在代码去父进程和子进程的代码是一样的,只是两者执行的是不同的(由于pid的不同);
父进程执行的代码:
子进程执行的代码:
当调用fork()函数时会复制一份父进程的虚拟地址空间 ,即子进程的用户数据和父进程一样,内核区也会被拷贝过来,但是pid会略有不同;
如果在父进程中存在变量的定义,那么克隆的子进程中的变量与父进程的变量有什么关系呢?
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int num=10;
//创建子进程
pid_t pid = fork();
//判断是父进程还是子进程;
if(pid>0)
{
printf("pid:%d\n",pid);
//如果大于0,返回的是创建的子进程的进程号;当前是父进程;
printf("i am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
printf("parent num :%d\n",num);
num+=10;
printf("parent num +=10:%d\n",num);
}
else if(pid==0)
{
//当前是子进程;
printf("i am child process,pid:%d,ppid:%d\n",getpid(),getppid());
printf("child num :%d\n",num);
num+=100;
printf("child num +=100:%d\n",num);
}
for(int i =0;i<3;i++)
{
printf("i=%d,pid:%d\n",i,getpid());
sleep(1);
}
return 0;
}
如上图所示, 克隆后的子进程中的变量不受父进程的变量变化影响;
实际上,fork函数的使用是写时拷贝,(写时拷贝是一种推迟甚至避免拷贝数据的技术)也就是说资源的复制是在需要写入的时候才会进行,在此之前,只是以读的方式进行共享;
内核此时并不复制整个进程的地址空间,而是让父子进程共享一个地址空间;只有在需要写入的时候才会复制地址空间,从而使各个进程有各自的地址空间。(父子进程进行写入的时候,都会重新创建一个地址空间);
父子进程之间的关系:
区别:
1、fork()函数的返回值不同
父进程中:>0,返回子进程的ID
子进程中:=0;
2、PCB(进程控制块)中的一些数据:
当前进程的ID不同(PID),当前进程的父进程的ID也不同(PPID);
共同点:
在某些状态下,在子进程刚被创建还没有执行写入操作时;
用户区数据、文件描述符表;
父子进程对于变量是不是共享的呢?
刚开始是共享的,如果修改了数据,就不进行共享了;
读时共享,写时拷贝;
3.3GDB多进程调试
设置调试对象
使用 GDB 调试的时候,GDB 默认只能跟踪一个进程,可以在 fork 函数调用之前,通
过指令设置 GDB 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程。
测试代码:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("begin\n");
if(fork() > 0) {
printf("我是父进程:pid = %d, ppid = %d\n", getpid(), getppid());
int i;
for(i = 0; i < 10; i++) {
printf("i = %d\n", i);
sleep(1);
}
} else {
printf("我是子进程:pid = %d, ppid = %d\n", getpid(), getppid());
int j;
for(j = 0; j < 10; j++) {
printf("j = %d\n", j);
sleep(1);
}
}
return 0;
}
GDB默认情况下,调试的是父进程的代码,
设置调试父进程或者子进程:set follow-fork-mode [parent(默认)| child];
设置调试模式
设置调试模式:set detach-on-fork [on | off]
默认为 on,表示调试当前进程的时候,其它的进程继续运行,如果为 off,调试当前进
程的时候,其它进程被 GDB 挂起。挂起就不会被执行;
其他调式
查看调试的进程:info inferiors
切换当前调试的进程:inferior id
使进程脱离 GDB 调试:detach inferiors id
本节我们学习的内容基本如上,大家多多练习;
创作不易,还请大家多多点赞支持!!!