进程

我们平时写的 C 语言代码,通过编译器编译,最终它会成为一个可执行程序,当这个可执行程序运行起来后(没有结束之前),它就成为了一个进程

【Linux系统编程】进程介绍_进程


程序是存放在存储介质上的一个可执行文件,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建、调度和消亡。程序是静态的,进程是动态的


在 Linux 系统中,操作系统是通过进程去完成一个一个的任务,进程是管理事务的基本单元。进程拥有自己独立的处理环境(如:当前需要用到哪些环境变量,程序运行的目录在哪,当前是哪个用户在运行此程序等)和系统资源(如:处理器 CPU 占用率、存储器、I/O设备、数据、程序)。我们可以这么理解,公司相当于操作系统,部门相当于进程,公司通过部门来管理(系统通过进程管理),对于各个部门,每个部门有各自的资源,如人员、电脑设备、打印机等。


进程状态

我们现在的电脑基本上都是多任务,我们聊着 QQ 的时候,同时可以看着视频,这里相当于 QQ 和视频两个程序同时运行着(两个进程)。早期的时候,电脑的 CPU 是单核的(单核理论上只运行操作一个任务),那它是如何做到多任务的呢?这就涉及到进程的调度策略。现在给大家举这么一个例子,有 A,B,C 三个进程,在我们单 CPU 的情况下,每一个时刻只有一个进程在运行,如果 A 运行完,B 运行,B 运行完,C 运行,C 运行完,A 运行,而 CPU 的运算速度足够快,A 两次运行时间间隔足够短,从宏观上就我们就看到 A,B,C 好像同时运行,这就是实现单 CPU 运行多个任务的核心原理,通过时间片轮询调度策略实现多任务更多详情,请看《Linux 进程调度浅析》


从上面的例子,我们可以得知,对于 A 进程而言,有时候在运行,有时候没有运行,两个状态不一样,所以,进程是有状态的,同时,状态是可以相互进行转换的,从执行的状态转换为不执行的状态,这里,我们可以把进程运行的整个生命周期简单划分为三种状态(实际上不指这三种状态):就绪态、执行态、等待态

就绪态

进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间。

执行态

该进程正在占用 CPU 运行。

等待态

进程因不具备某些执行条件而暂时无法继续执行的状态。


这里需要注意,就绪态和等待态都是不执行,但它们是有区别的,就绪态是指满足条件,时间没到,等待态是不满足条件。


同样的,进程的这三种状态可以相互转换:

【Linux系统编程】进程介绍_父进程_02


为了让大家更好地这三种状态的转换,给大家举一个买火车票的例子。


Mike 匆忙地赶去火车站买火车票,太着急了,到了售票厅才发现忘记带身份证,这时候,就算 Mike 排队也没用,因为 Mike 不具备买票的条件(没带身份证),这时候的 Mike 属于等待态


Mike 给它对象打电话,让她把身份证带过来,等会,身份证送到了,这时候,Mike 可以去排队买票了,只是时间到,Mike 就可以买票了,这时,Mike 属于就绪态。而这过程是等待态转换到就绪态


等了 10 分钟,终于到 Mike 了,Mike 开始买票,这时候, Mike 属于执行态。而这过程是由就绪态转换为执行态


而在买票的过程中,Mike 的对象打电话给他,让 Mike 也帮她买一张火车票,但是, Mike 没有她对象的身份证,接着,Mike 继续等他对象送身份证,这时候,Mike由执行态转换为等待态


假如是这么一种情况,Mike 买火车票是给公司的同事买的(需要买 100 多张票),在买着票的过程中(执行态),后面还有很多人在排队,后面排队的人肯定不爽,这时售票员就说,20分钟后,如果你还没处理完,请你到后面排队。结果,Mike 花了 20 分钟还是没有处理完,于是,乖乖地到后面重新排队,这时候,Mike由执行态转换为就绪态


进程控制块

对于操作系统而言,它需要控制很多进程,同时,每个进程都有不同的状态,系统如何知道 A 执行完到 B 执行而不是 C?系统如何协调控制进程呢?


当我们运行一个程序使它成为一个进程时,系统会开辟一段内存空间存放与此进程相关的数据信息,而这个数据信息是通过结构体( task_struct,打开 /usr/include/linux/sched.h 可以找到 task_struct 的定义 )来存放,我们把这个存放进程相关数据信息的结构体称为进程控制块操作系统就是通过这个进程控制块来操作控制进程更多详情,请看《 Linux 进程管理》


进程控制块是操作系统中最重要的记录型数据结构。进程控制块记录了用于描述进程进展情况及控制进程运行所需的全部信息,它是进程存在的唯一标志。进程控制块里有很多信息,其中比较重要的是进程号,至于其他的一些信息我们不在这详细讨论。


进程号

每个进程都由一个进程号来标识,其类型为 pid_t(无符号整型),进程号的范围:0~32767。进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用。

【Linux系统编程】进程介绍_进程介绍_03


系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个 Linux 系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的 0 号进程,它是所有进程的祖先。进程号为 0 的进程通常是调度进程,常被称为交换进程( swapper )。由 0 号进程创建 1 号进程(内核态),1 号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1 号进程调用 execve() 运行可执行程序 init,并演变成用户态 1 号进程,即 init 进程


所以,在 Linux 下面所有的进程都由 init 进程直接或者间接创建。


接下来,再给大家介绍三个不同的进程号。


进程号(PID)

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

父进程号(PPID)

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

进程组号(PGID)

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


Linux 操作系统提供了三个获得进程号的函数 getpid()、getppid()、getpgid()


所需头文件:

#include <sys/types.h>

#include <unistd.h>


pid_t getpid(void);

功能:

获取本进程号(PID)

参数:

返回值:

本进程号



pid_t getppid(void);

功能:

获取调用此函数的进程的父进程号(PPID)

参数:

返回值:

调用此函数的进程的父进程号(PPID)


pid_t getpgid(pid_t pid);

功能:

获取进程组号(PGID)

参数:

pid:进程号

返回值:

参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号


示例代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	pid_t pid, ppid, pgid;

	pid = getpid();
	printf("pid = %d\n", pid);

	ppid = getppid();
	printf("ppid = %d\n", ppid);

	pgid = getpgid(pid);
	printf("pgid = %d\n", pgid);
	
	return 0;
}


运行结果如下:

【Linux系统编程】进程介绍_linux_04