一、什么是进程
只要是计算机科班出身的,肯定是对《操作系统》中的进程了解的相当清楚。但如果让大家把进程真正的掌握并说清楚,其实很不容易做到。进程是计算机操作系统中资源分配和独立运行的基本单位。但进程为什么是资源分配的基本单位?为什么又是独立运行的基本单位呢?可能大家就说得不太清楚了。
二、程序、进程、线程和协程
程序很好理解,最简单的认知就是开发者编写的代码及其相关依赖(如图片等),其实程序更准确的描述应该是编译后的二进制的代码和相关依赖数据。而进程则是加载到内存中的程序,程序是静态的存在于电脑硬盘中的二进制数据集合,进程是动态运行的机器码。在现代的操作系统中,进程是抽象的、虚拟的,而进程是一个静态的文件集合。程序没有生命周期,而进程是有生命周期控制的。一个进程,无论如何,总有创建和退出的情况。而程序,则没有这一说法。
明白了程序和进程的不同,再理解线程和协程就非常好理解了。假使一个工人(人无法再拆分了)只作一个工作,映射计算机,就是一个进程(当然,这不太严谨)。如果在一个工作中,工人一会干这个,一会干那个,或者两只手同时处理两个以上的操作,就是线程。如果在工人干具体的工作任务时,可以通过某种手段让多个行为同时进行(比如把电路板推到焊接机器上,多个机器手同时焊接完成不同的芯片焊接),这就是协程。
通过上面的说明,大家就明白了,多线程需要工人自己不断协调和处理任务,而协程就不用了,把任务安排好,就直接完成了。也就是说,协程的应用比线程相对要简单多了(不过,映射到计算机上,由于支持的还不够,协程开发还是比较复杂的)。
三、操作系统中的进程及其本质
要想真正的掌握操作系统中的进程,也就是系统中的本质,就需要从计算机的发展来理解。在操作系统的相关书籍中,可以知道计算机最初根本就没有什么进程这种概念之说。主要以硬件电子管切换各种逻辑和操作。之后,随着计算机技术发展,计算机的能力越来越强大,就出现了多道程技术。说白了就是计算机硬件资源变得强大后,可以同时处理多个作业(任务)。而每个作业就可以从逻辑上认为是一个进程。再到后来,随着计算机技术发展的更为强大先进,进程就出现了。
举一个简单例子,一个手工作坊,可能一个人就能支持。可如果换成机器生产,可能就需要十个工人来支持。而每个工人完成的其实是一个相同的任务。他可以独立的负责机器产出的接收、检测、整理以及最终的包装。每个人都有自己的一整套的工作工具(资源)如手套、测量的尺子、包装盒等等。如果缺少工人,就会出现生产出来的产品无人处理,只能降低机器生产的速度,从而浪费了机器的资源。那么进一步推广呢?进入了现代化的工业大生产,产品产出更多,工人需要也更多,那么如何管理这些工作和产品的对应协调就是一项专门的工作即管理者的日常工作。
回到进程,也就是不管计算机技术如何发展(在可预见的未来),进程就类似一个普通的工人,它可以利用计算机分配的资源独立的完成一个工作任务。可计算机太强大了,就如同现代化的大工业生产,如何管理工人的生产呢?如果有一点点管理经验的可能就知道,可以用表格啊,先把整体的任务分解,然后把时间、资源、班次等等与分解的子任务对齐,每个工人都有自己的任务计划和排期。不出意外(员工不到岗、停电等)的情况下,整个工厂就可以在计划中自然推进。计算机是人设计出来的,它的处理任务的方式自然与现实处理任务的思想是一致的。
在操作系统中,有一个PCB( Process Control Block)表,来操作着多个PCB,PCB代表着一个进程的信息(可以理解成工人的等级、年龄等等),操作系统通过PCB表来操作每个PCB来与具体的进程打交道,每个PCB指向一个进程,而指向的这个进程包含着加载到内存中的程序段及由其操作的相关数据结构集合。这也是人们经常提到的程序就是算法+数据结构的来由。现在就明白了,进程是由PCB+加载到内存中的程序段+其操作的数据结构集合来组成。如同现实世界工人可能会更换位置、岗位,可能会由于身体原因操作速度变慢等等情况,计算机中进程也可能会出现资源不足、优先级升降等等情况,PCB表中都要表现出来。此处只简单说明一下PCB的定义,不进行扩展。
struct task_struct {
// 进程状态 --是否在工作
volatile long state; // TASK_RUNNING, TASK_INTERRUPTIBLE 等
// 调度信息
struct sched_entity se; // CFS 调度实体
int prio; // 动态优先级
int static_prio; // 静态优先级
// 进程标识 --身份标识
pid_t pid; // 进程 ID
pid_t tgid; // 线程组 ID(主线程 pid)
struct task_struct __rcu *parent; // 父进程
struct list_head children; // 子进程链表
struct list_head sibling; // 兄弟进程(同父)
// 内存管理 --最典型的资源
struct mm_struct *mm; // 用户地址空间(堆、栈、代码段等)
struct mm_struct *active_mm; // 内核线程使用
// 文件系统
struct fs_struct *fs; // 根目录、当前工作目录
// 打开的文件
struct files_struct *files; // 文件描述符表(fd table)
// 信号处理
struct signal_struct *signal; // 共享信号处理(线程组共享)
sigset_t blocked; // 被阻塞的信号集
// CPU 上下文--任务切换是需要保存
struct thread_struct thread; // CPU 寄存器、内核栈指针等
// 内核栈
void *stack; // 指向内核栈(通常 16KB)
// 命名空间--容器的支持
struct nsproxy *nsproxy; // UTS, PID, network, mount 等 namespace
// ... 其它省略
};
PCB中主要包括四大类定义,即标识类、资源类和状态(含调度)类以及上下文类,操作系统通过它们即可把一个进程控制的明明白白。当然,以后可能随着技术的进步,一定会扩展PCB的内容的。
通过上面的分析就可以得出,进程的本质就是运行的程序段,它一种动态的虚拟抽象。为了能被控制,就需要有一个标识即PCB。而为了能够有效的工作就需要处理数据,而数据的描述就可以用数据结构来进行。明白了这一点,就知道在进程或线程开发中,为什么始终要控制进程ID,为什么要处理上下文,为什么要传递数据参数。这也就明白了本文一开始提出的两个问题。
当我们从不同的角度来看进程时,可以发现,进程的理解其实是不同的。从一个普通的开发者看来,程序就是一个被创建出来的执行单元;从内核看就是一个任务及其控制的资源的集合;从虚拟化角度来看就是一个服务,一个容器。诸如此类。
但最终进程是服务到用户的,从用户看来,进程就是一个软件应用。比如浏览器,比如Word等等。
四、Linux系统中如何创建进程
在Linux系统中,大家可以通过ps命令来查看进程的相关信息,如果想更详细的了解进程的信息可以到/proc/进程ID文件夹下查看。但更重要的是,大家需要理解一个进程是如何在Linux系统中创建并运行的。其主要的步骤如下:
- 程序的二进制代码存储在硬盘上
- 通过命令或其它方式启动硬盘上的程序并通过内核加载到内存中,同时加载相关的资源数据
- 内核创建PCB并填充相关的数据信息,如PID,状态信息等。后续会进行父进程资源复制、状态设备和上下文的处理等
- 将PCB添加到PCB列表中,供操作系统调用处理
- 操作系统根据情况执行进程的调度和执行
体现在代码上就是下面的过程:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("create process error!");
return 1;
}
else if (pid == 0) {
// son process
printf("son PID: %d, parent PID: %d\n", getpid(), getppid());
execl("/bin/df", "df", "-h", NULL);
perror("execl start error!");
return 1;
}
else {
//parent process
printf("parent PID: %d, son PID: %d\n", getpid(), pid);
int status = -1;
waitpid(pid, &status, 0); // wait sub process finish
printf("son exited, status %d\n", WEXITSTATUS(status));
}
return 0;
}
说明:只要注意waitpid的作用即可。
在Linux中创建进程一般是通过fork(),exec()和clone()来实现。fork()用来快速复制父进程;exec()用来加载新程序到进程空间;而clone()则提供精细粒度的控制,用于创建线程或共享资源的进程。
五、总结
本文作为对计算机基础知识点进行回望的篇章,不是对进程进行重复说明,而是从有一定开发经验后,从另外一个角度回看进程。对于进程这种最基础的技术点,可能很多同学都不以为然。认为有什么可聊的?其实不然,进程在开发中虽然不如线程常见,但其实线程的问题往往最终回馈到进程。如果在开发实践中无法真正的理解进程,那么就无法真正掌握线程的开发,只能在线程开发的上层徘徊。进程这个技术点,要从应知应会到活学活用,就是靠从底层真正掌握进程的本质开始。与诸君共勉!

1108

被折叠的 条评论
为什么被折叠?



