聊聊进程(基本概念、进程的产生以及多进程编程)

//部分图片来源于网络,若侵联系删除

一、进程基本概念:
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()产生子进程,父子进程谁先执行的先后顺序由调度程序确定
//我们可以通过睡眠或者阻塞等手法控制父子进程执行的先后顺序
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值