//部分图片来源于网络,若侵联系删除
一、进程基本概念:
1. 我们经常是写程序,那你知道程序究竟是什么:
程序 = 指令+数据,是一个可执行文件,是一个指令序列,是静态的。
2.进程的定义:
实际上,一个程序运行起来我们就称为一个进程,进程是动态的,
是系统资源和调度的独立单位。
3.进程控制块:PCB
1.是操作系统用于管理和控制进程的额一个专门的数据结构;
2.记录进程的各种属性,描述进程动态变化的过程;
3.PCB是操作系统的感知进程存在的唯一标志,创建进程就是创建PCB;
4.操作系统为了统一管理,将进程存放在一起,形成进程表,进程表的大小,体现的是并发量的理论值。
4.PCB的内容:
1.进程基本的描述信息:
进程标识符PID(类似人的身份证),进程名,用户标识符UserID,进程组关系等
2.进程控制信息:
当前的状态、优先级、代码执行入口地址、程序在磁盘上的文件
进程间同步与通信、进程的消息队列指针、进程的队列指针
3.所拥有的资源和使用的情况:
虚拟地址空间的状况、打开文件列表等
4.CPU的现场信息:
指的是程序不运行的状态下:寄存器值(通用寄存器、PSW、PC等),指向该进程页表的指针。
我在这里将linux下的PCB内容给大家展示一下,其实也没多高大上,实际上就是一个task_struct 结构体:
//linux-2.6.27.10 PCB在linux下其实就是task_struct这样的数据结构;
//这里我截出来一部分,有需要的可以去linux官网下载源码看一下
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
struct list_head tasks;
//进程虚拟内存管理的结构体
struct mm_struct *mm, *active_mm;
struct linux_binfmt *binfmt;
int exit_state;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
unsigned int personality;
unsigned did_exec:1;
pid_t pid;
pid_t tgid;
....................
//以下描述父子进程
struct task_struct *real_parent;
struct task_struct *parent;
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
................
5.进程的三种基本状态:
5.1.运行态:占用CPU,并在CPU上运行;
5.2.就绪态:已经具备了运行的条件,只不过CPU不空闲,需要等一下;
5.3.等待态:因某一个条件没有完成,需要等待完成,比如等待IO等
(也称阻塞态、封锁态、睡眠态等)
进程三种状态的转换图:
1.就绪 与 运行之间的相互转换:
就绪-->运行:程序被调度程序选中
运行-->就绪: 时间片用完、更高优先级的程序需要运行
2.
运行-->等待(阻塞):请求OS服务、对资源的请求不到位、等待IO结果、等待另一个进程提供信息
3.
阻塞-->就绪:造成阻塞的事件消失或者被执行
还可以给进程设置其他的一些状态:
1.创建态:创建了PID,PCB,但是os不同意该进程执行,可能因为资源有限;
2.终止态:执行结束了,进行一些资源的回收、数据统计工作等
3.挂起态:OS用于负载调节,比如进程太多资源严重不足,OS就将进程清出内存,直到进程被激活。
看一下linux下的进程状态转换图:
linux下状态特点:
1.有浅度睡眠、深度睡眠之分,区别在于浅度可以被信号唤起,
深度不可,深度只有等到资源到位;
2.linux下进程运行结束后,处于僵尸态,关于僵尸进程我将在其他文章中叙述。
schedule()调度程序
不论在哪种操作系统,不论设置哪些状态,都必须有状态之间互相转换的条件。
6.进程队列:(状态的切换过程)
操作系统根据进程的状态(运行、阻塞…),为进程创建一个或者多个队列;
队列中的元素实际上是进程PCB;
进程的状态转换:实际上是PCB从一类状态的队列出,进入另一类状态的队列中的过程。
实际上,下图能更好的理解进程队列:
7.进程控制:完成进程的各个状态的转换,由原语完成
原语:也是原子操作,不可再分的执行单位,完成原语需要OS屏蔽中断等措施来完成;
1.进程创建原语;
2.进程阻塞原语;
3.进程挂起原语;
…
8.进程的创建过程:重点
1.给新进程创建一个唯一标识以及进程控制块
2.为进程分配地址空间,这里指的是虚拟地址空间,不会分配物理内存,直到真正运行时
体现操作系统的虚拟特性
3.对进程控制块进行一些初始化工作,如设置状态为new(创建)
4.将PCB插入到相应的队列,如(就绪队列等)
linux下:创建进程的系统调用 fork() + exec() 配合使用
9.进程的撤销:
1.回收进程所占用的资源
如打开的文件、网络连接、回收分配的内存
2.最关键的是撤销调进程的PCB
linux中调用: exit()
10.进程的阻塞:
运行过程中期待某一事件发生,如等待键盘输入、等待其他进程发送的数据。
(进程自己执行一个 阻塞原语 将自己变为阻塞态,linux下是wait())
11.进程的分类以及进程的层次结构
1.系统进程和用户进程:
2.前台进程和后台进程:用户与前台进程打交道,后台程序默默提供服务
3.CPU密集型进程和IO密集型进程:需要大量计算的为CPU密集,读写磁盘较多的为IO密集
linux进程家族树根:init进程
init进程是所有进程的起源,一些情况下,一个进程结束,这个进程的子孙进程也必须结束
二、linux下的进程创建的过程:
1.linux下与进程有关系统调用:
fork():通过复制调用fork的进程(父进程)的操作来创建新进程;
exec():通过用新的程序代码覆盖原来的地址空间,实现 执行代码的转换;
wait():提供初级进程同步操作, 使一个进程等待另外一个进程的结束;
exit():终止一个程序的运行
2.fork()的实现:
先给大家看一下Linux中的sys_fork的函数原型:
asmlinkage int sys_fork(struct pt_regs *regs)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return(-EINVAL);
#endif
}
实际上,sys_fork()调用了do_fork(),以下是do_fork()函数原型:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
具体的实现不在这里啰嗦,有兴趣的可以自己看看源码
接下来,说一下linux创建进程的基本过程:
1.为子进程分配一个空的PCB;
2.为子进程分配PID;
3.父进程将地址空间的指针传递给子进程,子进程拿来引用,需要注意的是,子进程拿到父进程的指针
但是只能用指针去读取,当子进程要给指针指向的文件写入时,操作系统给子进程才真正的开辟内存,
然后进行存储。(写时拷贝技术 copy on write)
4.从父进程继承共享资源,如打开的文件和当前的工作目录;
5.将子进程的状态设置为就绪,插入就绪的进程队列;
6.给 子进程 返回0;
7.给父进程返回子进程的pid
(可以看到fork()返回了两个值)
我们需要注意的是:
UNIX调用fork()复制进程到上述的 第三步 时,实际上真正的给进程一页一页的复制数据,这样效率慢
linux采用了写时拷贝技术,写入的时候才真正分配存储空间,大大提高了效率
系统中的所有的进程来源于Init进程,从init复制而来
进程虚拟地址空间:
每个进程运行起来,操作系统的都会给分配一个进程地址空间,逻辑上的
进程映像:
很多时候,我们需要收集一些进程的信息,我们该去哪里找呢?
1.用户相关的信息:进程的地址空间
2.寄存器相关
3.内核相关:
静态部分:PCB以及各种资源数据结构
动态部分:内核栈(不同进程进入内核后使用不同的内核栈)
三、多进程编程:
//ubuntu 18.04
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("这是产生自进程前的执行的指令\n");
pid_t pid;
pid = fork();
//从这里产生一个子进程,fork给子进程返回0
//给父进程返回的是子进程的PID
if(pid == 0)
{
printf("这是子进程打印出来的\n");
}
else
{
printf("这是父进程打印出来的\n");
wait(NULL);
//父进程获取子进程的退出状态,阻塞子进程退出,回收子进程的资源
exit(0);
}
}
分析以上程序的执行过程:
1.pid = fork(); 语句之前都是父进程执行的;
2.pid = fork();执行完以后,子进程就产生了,地址空间也分配好了
3.pid = fork();之后又的if(pid == 0)判断,如果是子进程的地址空间,那么就执行子进程的代码
同理,如果不是子进程那么就是父进程的空间,自然要执行父进程的地址空间中指令
父子进程之间执行的是同样的代码,因为父子进程代码是共享的
只不过最终我们用if else语句使得父子进程执行不同的 分支 。
2.我们在bash中 ./ 执行一个可执行文件,产生进程的过程:
1.首先启动的bash也是一个进程;
2.先调用fork()产生一个子进程;
3.然后调用exec()系列函数来替换掉子进程,这个新的进程,我们想要的进程就产生了
3.父子进程谁先执行:
这个不我们能决定的,最终由内核的调用算法决定,谁使用CPU
补充:循环创建多进程
错误示例:这样创建的进程不止五个
int i = 0;
for(;i < 5; ++i)
{
pid = fork();
}
正确的做法:
int i = 0;
pid_t pid;
for(;i<5;++i)
{
pid = fork(); //这里父进程产生一个子进程
if (pid == 0 )//如果子进程抢到CPU,跳出循环
break; //子进程从这里跳出循环,子进程不应该再去 ++i fork()
}
//父进程只有执行完for才能执行后面的代码
if( i<5 ) //i==5之前,父进程还在循环中,子进程已经跳出
{
//child 的代码分支部分
}
else
{
//父进程的代码分支部分
}
//调用fork()产生子进程,父子进程谁先执行的先后顺序由调度程序确定
//我们可以通过睡眠或者阻塞等手法控制父子进程执行的先后顺序