一、背景
1.1 冯诺依曼体系结构
我们常见的计算机、服务器等设备大部分遵循冯诺依曼体系。
当前,我们所认识的计算机,主要由各类硬件组成:
- 输入单元:包含键盘、鼠标、扫描仪、写板等;
- 中央处理器:含有运算器和控制器;
- 输出单元:显示器、打印机等。
关于冯诺依曼,必须强调几点:
- 图1的存储器指的是内存;
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备);
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或从内存中读取;
- 注意:所有设备只能直接和内存打交道。
1.2 操作系统(Operator System)
1.2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。操作系统包括:
- 内核:进程管理、内存管理、文件管理、驱动管理等
- 其他程序:函数库、shell程序等
1.2.2 设计OS的目的
- 与硬件交互,管理所有的软硬件资源;
- 为用户程序(应用程序)提供一个良好的执行环境
1.2.3 定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。
如何理解管理:
- 描述被管理的对象
- 组织被管理的对象
由上图可知:操作系统是一款进行软硬件资源管理的软件。且管理的本质:先描述,再组织。操作系统四大管理:内存管理、进程管理、文件管理、驱动管理。
1.2.4 操作系统总结
计算机硬件管理:
- 描述起来,用struct结构体;
- 组织起来,用链表或其他高效的数据结构
在数据层面:一般CPU不和外设直接沟通,而是直接只和内存打交道,外设亦只会与内存打交道。
系统调用和库函数概念:
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高。所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库就很有利于更上层用户或者开发者进行二次开发。
承上启下:
操作系统管理进程管理:先把进程描述起来,再把进程组织起来。
二、进程的概念与定位
2.1 什么是进程
相信各位读者对进程并不陌生,在我们日常使用的Windows操作系统下,进程图形化显示详见图3中。但进程具体是什么又说不出,进程可以理解为进程是程序的一个执行实例,正在执行的程序等。 内核观点:担当分配系统资源(CPU时间、内存)的实体。
在图3中,任何启动并运行程序的行为,都由操作系统帮助我们将程序转换为进程,完成特定的任务。
2.2 进程的PCB
图4中,在磁盘上的可执行程序,本质上就是一个普通二进制文件(文件包含文件内容和文件属性)。而可执行程序想要运行就必须将可执行程序的代码和数据加载到内存。进程信息被放在一个叫进程控制块的数据结构中,可以理解为进程属性的集合,一般称之为PCB
(Process Control Block)。
pcb/task_struct
提取了所有进程的属性;进程是由内核关于进程的相关数据结构(PCB/task_struct)与当前进程代码和数据组成。
task_struct-PCB的一种
在Linux中描述进程的结构体叫做task_struct
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程信息。
task_struct内容分类
标示符:描述本进程的唯一标示符,用来区别其他进程;
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级
程序计数器:程序中即将被执行的下一条指令的地址
内存指针:包含程序代码和进程相关数据的指针,还有和其他进程共享的内存模块的指针
上下文数据:进程执行时处理器的寄存器中的数据
I/O状态信息:包含显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
其他信息
PCB属性与文件(内容+属性)的属性关系不大,因为该属性仅仅包含文件的RWX权限,而PCB属性属于重起炉灶的数据结构。
2.3 组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
查看进程
进程的信息可以通过/proc
系统文件夹查看,详见图5:
Linux系统中,根目录下有一个proc目录,保存进程相关属性的目录。proc目录下保存的是一个内存级的文件系统,只有当Linux系统启动时它才会存在,磁盘上并不存在。图2中蓝色数字表示特定进程的PID。
- 大多数进程信息同样可以使用
top
和ps
这些用户级工具来获取
例如,在执行下列代码的时候,生成可执行程序后,通过./
将其转化为进程,详见图6:
#include<stdio.h>
#include<unistd.h>
int main(){
while(1){
printf("hello process\n");
sleep(1);
}
return 0;
}
此时,可以通过ps
命令查看进程的相关信息,详见图7:
注意:去除grep进程,可以通过grep -v grep
。
通过图8及图9可以看出,将进程20110中断后(或该进程终止后),系统目录proc下的20110文件亦会删除:
进程关闭时,操作系统会在/proc路径下将PID命名的文件夹20110的内容全部删除。
2.4 通过系统调用获取进程标示符
- 进程ID(PID)
- 父进程ID(PPID)
#include<stdio.h>
#include<unistd.h>
int main(){
while(1){
printf("hello proccess, 我已经是一个进程了,我的pid是:%d,我的父进程是:%d\n",getpid(),getppid());
}
return 0;
}
图10中,频繁启动上述代码生成的可执行程序myprocess
时,PPID值不变,PID数值在myprocess
重启时会更改:
且通过图11可知,进程myproccess的父进程是bash。bash是命令行解释器,本质上它也是一个进程,因为它有独立的PID;命令行启动的所有的程序,最终都会变成进程,而该进程对应的父进程都是bash。
进程的删除除了通过ctrl+c
命令还可以通过kill -9 "PID"
,根据进程PID来删除进程,结果图9所示:
2.5 通过系统调用创建进程
2.5.1 如何创建子进程?
要想创建子进程,必须使用系统调用函数fork()
。例如,执行下述代码,生成对应的可执行程序myprocess
后,经过./
进程执行后,图13中产生了2个返回值。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
printf("AAAAAAAAAAAAAAAAAAAAAAA:pid->%d,ppid->%d\n",getpid(),getppid());
fork();
printf("BBBBBBBBBBBBBBBBBBBBBBB:pid->%d,ppid->%d\n",getpid(),getppid());
sleep(1);
return 0;
}
//fork()后有两个执行流
2.5.2 fork的两个返回值
通过系统调用创建进程-fork(系统调用函数,操作系统提供的)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
printf("AAAAAAAAAAAAAAAAAAAAAAA:pid->%d, ppid->%d\n",getpid(),getppid());
pid_t ret = fork();
printf("BBBBBBBBBBBBBBBBBBBBBBB:pid->%d, ppid->%d, ret:%d, &ret:%p\n",getpid(),getppid(),ret,&ret);
sleep(1);
return 0;
}
上图中,ret
给子进程返回0,给父进程返回的是子进程的PID。具体原因将在fork原理说明。此外,一般fork常与if一起使用,就能返回一个结果:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<assert.h>
int main(){
pid_t ret = fork();
assert(ret != -1);
if(ret == 0){
//子进程
while(1){
printf("我是一个子进程了,我的pid是:%d,我的父进程是:%d\n",getpid(),getppid());
sleep(1);
}
}else if(ret > 0){
//父进程
while(1){
printf("我是一个父进程了,我的pid是:%d,我的父进程是:%d\n",getpid(),getppid());
sleep(2);
}
}
return 0;
}
2.5.3 fork原理
- fork做了什么?
- fork如何看待 —— 代码和数据
- fork如何理解两个返回值问题
fork原理主要从上述3个方面来讲述。在图16中,fork创建了子进程,该子进程继承了父进程大部分PCB属性(PID、PPID等除外),且子进程和父进程共享内存处的数据和代码(红框处)。
进程在运行的时候具有独立性的,父子进程在运行的时候也是一样
进程PCB指向的内存块中的代码是只读的;对于数据,当一个执行流尝试修改数据的时候,OS会自动给我们当前触发写时拷贝。父子进程会共享指向的内存处的代码和数据,且独立运行,如图16中,子进程被killed,父进程依然正常运行。若子进程对数据进行更改,则在内存中申请空间存储更改的数据,并返回,不会对父进程产生影响。
一般而言,当我们函数内部准备执行return的时候,我们的主体功能已经完成。fork具有两个返回值,注意,fork本质上是OS提供的一个函数;fork之前的代码父进程执行,而fork之后的代码子进程、父进程都执行。当fork函数内部准备执行return时,我们的主体功能已经完成。父子进程都执行,因而产生两个返回值。
2.5.4 fork小结:
- fork之后,执行流会变成2个执行流;
- fork之后,谁先运行由调度器决定;
- fork之后,fork之后的代码共享,通常通过if和else if来进行执行流分流
- 进程在运行的时候,是具有独立性的!父子进程在运行的时候也是一样具有独立性。
- 代码:代码是只读的
- 数据:当有一个执行流尝试修改数据时侯,OS会自动给我们当前进程触发写时拷贝。