Linux进程概念系列(详细万字解读!!!!)

进程前的一些话

先了解一些操作系统的总体框架:
   操作系统(operator system):任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)简单来说: 操作系统就是一种来管理软硬件的程序

  • 为什么要有OS??
    更好地去管理协调软硬件资源,给予用户程序一个安全稳定快速的执行环境。
       但是操作系统本身是整体的:操作系统不相信任何的用户,但是依然要为用户提供服务,所以就出现了一些接口,用这些接口调用来供上层开发使用,叫做系统调用。可是由于系统调用在使用上难度较高,对于一般用户很不友好,所以有些开发者对系统进行了适度封装,从而形成了库,优化了用户的使用和开发体验。

  • 操作系统对进程的管理:同样也是先描述(描述为PCB结构)再组织(各种数据结构类型)

进程的基本概念

简单来说一个被载入内存的程序叫进程,或者一个正在运行的程序叫做进程。
进程 = 内核PCB数据结构对象 + 所写代码和数据

ls /proc 查看当前系统中所有的进程
使用命令ps axj也可以显示系统当中存在的进程。
在这里插入图片描述

  • 计算启动时加载操作系统首先加载到内存中(OS实际上也是一种程序),操作系统通过先描述,再管理这样的方式来管理内存。

那么到底什么是描述呢,什么又是管理呢?
举个例子,公司里最上层的管理者想要管理员工,实际上是不需要与员工见面的,上层的管理者只需要知道一个员工的属性,如出勤,项目贡献,活动参与度等等数据。管理者通过整合每一个人的的属性,就可以了解员工的状态来进行管理员工了。
在这里插入图片描述

  • 所以操作系统对进程的管理和组织也是同理
    当一个进程加载到内存的时候,OS会立刻描述这个进程,集合进程的信息,形成一个进程控制块PCB(Process Control Block),进程的所有信息都比放在PCB中。
  • PCB可以理解成: 是一个类似C语言中的结构体,或者c++中的类。上面我们说了,包含PCB和代码和数据,那么PCB之中就有记录进程代码的地址。
    在这里插入图片描述

刚刚说了PCB,那task_struct又是什么呢?

  • task_struct其实是PCB的其中一种: task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息。

task_struct的基本内容分类

  • PID:用来区别进程的标识。(PID是会变化的)
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器(pc): 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
  • 其他的各种信息。

进程的查看方式

1.ps axj 查询方式

ps axj | head -1 && ps axj | grep test
使用组合技查询可以现实首行信息标识和目标进程
在这里插入图片描述

2.ls  /proc方式
方法:ls /proc/对应进程pid
在这里插入图片描述
其中红圈中的目录名为数字,这些数字就是进程的PID。这些目录中记录着进程的详细信息

认识第一个系统调用

对于系统调用的使用: getpid(),getppid()
分别获得当前进程的PID和其父进程的PID。
在这里插入图片描述
在这里插入图片描述

系统调用中的创建子进程->fork( )

fork( )是一个系统调用的接口,可以帮助我们在运行代码时创建子进程。

  • fork函数的返回值:
    1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
    2、如果子进程创建失败,则在父进程中返回 -1。

例如,我们看下面的代码:
在这里插入图片描述
很明显,fork()之后原来的进程创建了一个子进程,但是这里还是会与一些疑问?

  • 为什么fork要给父进程返回子进程PID,给子进程返回0呢?
  • fork( )具体干了什么操作?
  • 一个函数怎么返回了两次值?

这里我们来一一解答:
首先,我们需要明白一个点:父进程的数据和代码是加载到内存当中取用的,那么fork之后创建之后的子进程数据和代码又来自哪里呢?很简单,既然是父子关系,老爸坑定要带一波儿子的,所以子进程的代码和数据与父进程共享
了解之后我们来回答第一个问题:为什么针对父进程和子进程返回不同的值?
1 - 返回不同的返回值,是为了区分不同的执行流,执行不同的代码块。(简单说:叫来子进程就是来帮忙的,让父和子执行不同的代码块。)
  fork( )具体干了什么操作?
2-
在这里插入图片描述
由上图可知:fork()完成了对子进程PCB的构建操作,所以在return 的前一刻已经创建号子进程了。那么return ret就分别被父子进程各调了一次。
 在回答第三个问题之前,我们需要明白如下情况:在这里插入图片描述
所以在理解函数能够返回两次的基础上,我们知道了父子数据实际上时分开存储的,所以id有两个值时是不是就可以接受了一些。

提示点 : 对于父子进程这两个进程,谁先运行时不确定的,这个有调度器中的算法具体实现。

一般操作系统的状态

运行状态
  • 对于已经载入内存的进程来说(已经形成PCB),在被CPU执行之前,会有一个运行队列,相当于queue一般,头部进入CPU中,然后不断地向尾部遍历进入。接着就是调度器按照自身内部的算法去控制进程进出CPU的过程。
    处于运行队列之中的进程就称为运行态(R状态),无论进程是否正在被CPU执行,即可以有同时有多个存在R状态的进程

  •   但是对于一个已经放到CPU上的进程,是要一直执行完毕,才自己放下来呢?
    当然不是:日常如果写出了死循环,若是要跑完当前进程才能调度,那么对于用户来说其他的后续进程就会卡住不动。
      所以进程有一个叫做时间片的概念:该时间片所限制的一段时间内,保证所有的进程都会被CPU执行,此过程在计算机叫做并发执行因此,会存在大量将进程放上CPU和拿下来的过程,这个过程叫做进程切换

阻塞态

对于硬件设备,操作系统对其的管理方式依旧是先描述,再组织。将硬件的数据属性形成集合PCB来管理。

  • 当进程处于等待队列的时候,此时进程的数据正在等待硬件的输入,此时进程就处于阻塞状态!
    在这里插入图片描述
挂起状态

当内存中资源严重不足的时候,操作系统会把进程的数据和代码换出到磁盘中以缓解内存的压力,此时的进程就被称为挂起状态,后续需要的时候再换入。

Linux中进程的状态

static const char task_state_array[] = {
“R (running)”, /
0*/
“S (sleeping)”, /* 1*/
“D (disk sleep)”, /* 2*/
“T (stopped)”, /* 4*/
“T (tracing stop)”, /* 8*/
“Z (zombie)”, /* 16*/
“X (dead)” /* 32*/
};
在这里插入图片描述

通过ps axj可以查看所有的进程信息情况:STAT那一列就是进程的状态显示。
在这里插入图片描述

运行状态R

当一个进程处在运行队列里的时候,进程就处于R状态。无论进程当前是否被载入CPU中,都是R状态,所以可能会有多个进程同时处于R的状态。
D深度睡眠不响应任何操作系统请求,所以在D状态的等待期间,该进程无法以任何被杀掉:以确保数据不会丢失。

浅度睡眠状态(S)

  一个进程处于浅度睡眠状态,意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。
  其实可以理解为阻塞状态,等待这键盘等外设写入数据,一但获取到数据就变成R状态。
例如,我们使用sleep(second)的时候,此时查看进程的状态就是阻塞的状态。在这里插入图片描述
处于浅度睡眠状态的进程是可以被杀掉的。kill -选项9 + 对应进程的PID

在这里插入图片描述

深度睡眠状态-D

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)

暂停状态-T

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。
在这里插入图片描述
我们再对该进程发送SIGCONT信号,该进程就继续运行了。
在这里插入图片描述

僵尸状态-Z

  当一个进程结束的时候,操作系统不会立刻释放其PCB的内存占用,而是保留下来,等着父进程或者bash回收结束信息,若是结束信息始终无法被回收,那么进程的一些相关数据会一直保留再内存当中。所以,当一个进程正在等待结束信息被回收,那么此时进程的状态就是僵尸状态。

  • 再主函数中,我们一直会写返回0,这个0其实就是返回给操作系统,让其来判断进程代码是否顺利的执行。
  • 但是进程结束后的退出信息保存再哪里呢?
      进程退出的信息(例如退出码),是暂时被保存在其进程控制块当中的,在Linux操作系统中也就是保存在该进程的task_struct当中。
僵尸进程

当一个进程的子进程结束后,夫进程没有回收子进程的退出信息,那么这个子进程就成为了僵尸进程。
下面的代码时我们自己编写的,父进程创建子进程,此时父进程中没有回收子进程退出信息的操作,所以子进程就会变成Z状态。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(count){
			printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){ //father
		while(1){
			printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else{ //fork error
	}
	return 0;
}

在这里插入图片描述

僵尸进程的危害
  • 僵尸进程的退出信息被保存在task_struct中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
  • 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
  • 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏
孤儿进程

  在Linux当中的进程关系大多数是父子关系。若是父进程先退出那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程
  若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。

  • 为什么要被领养呢?
    因为未来子进程也会结束,需要回收信息进行内存的释放。

进程的优先级

是什么:
进程对于资源的访问,谁先访问,谁后访问的权力。

为什么会有优先级?
内存资源时有限的,进程是多个的,所以注定了进程之间要竞争资源。操作系统必须出手保证良性的进程,否则会出现某些进程长时间得不到CPU资源,出现进程的饥饿问题。
ps -l
在这里插入图片描述
PRI代表着进程的优先级,整数越小, 优先级越高。
NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
PRI的范围是[60,99),所以NI的范围是[-20,19)共四十个级别

pri(新) = pri(旧) + nice
注意这个pri旧的值始终是80,与上一次调整后的数值无关。即操作系统当中默认的PRI值是80,且每次修改都是从80的基础上去加减。

调度器大体上是如何去通过pri去控制呢?
…待补充。
通过renice命令更改进程的nice值

  • 使用renice命令,后面跟上更改后的nice值和进程的PID即可。
  • 若是想使用renice命令将NI值调为负值,也需要使用sudo命令提升权限。

进程切换

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
进程切换必须要有进程上下文的保存和回复过程。
若是不保存,那么下次该进程在CPU中就无法接着上回退出时的地方继续运行了。

cpu内部的寄存器里面保存的进程的临时数据—>进程的上下文。

所以,进程在离开CPU的时候,会将当前的寄存器里内部数据保存到自身的PCB(理论理解,但是速度太慢,现实用的硬件方面的操作和软结合):
简单来说就是将自己的数据带走,方便下次运行,达到无缝衔接的进程切换过程

环境变量

什么是环境变量:环境变量是系统提供的一组name = value形式的变量,不同的环境变量有不同的用户,通常具有全局属性

认识常见的环境变量:
PATH: 指定命令的搜索路径。
HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。
SHELL: 当前Shell,它的值通常是/bin/bash。

查看环境变量的方法:
我们可以通过echo命令来查看环境变量,方式如下:

echo $NAME //NAME为待查看的环境变量名称(当环境变量被访问时,前面都要带dollar)

我们知道,系统提供的指令实际上是一个可执行程序,那为什么我输入指令的时候不需要带路劲?
就是由于shell内部维护了一个PATH的环境变量,作为一个输入指令时的一个搜索路劲。
有了PATH环境变量,我们输入对应指令的时候,系统就会自动在PATH内部所给的路劲里面寻找可执行文件的路劲去匹配,找到就停止,找不到提示找不到。

我们有两种不带路径去运行可之心程序的方法:
1.将可执行程序拷贝到环境变量PATH的某一路径下。

sudo cp proc /usr/bin

既然在未指定路径时,系统会根据环境变量PATH当中的路径进行查找对应可执行程序,那我们就可以将我们的可执行程序拷贝到PATH的某一路径下,此后我们的可执行程序不带路径系统也可以找到了

2.将可执行程序所在的目录导入到环境变量PATH当中。
在这里插入图片描述
本地变量 && 环境变量
本地变量只在当前bash中有效,不会被其他子进程继承。
esport 本地变量就可以使得转化为环境变量。
env :显示所有的环境变量。
unset 环境变量:删除环境变量
set:可以查看所有类型的变量(包括本地和环境变量)

环境变量的组织方式:在这里插入图片描述

命令行参数

main函数其实是带有参数的,int main(int argc,char *argv)
int argc:argc字符数组中的有效元素个数。
char *argv[]:字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项。
在这里插入图片描述
但是,其实main函数还有第三个参数:
main函数其实是带有参数的,int main(int argc,char *argv,char* env)
main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。
环境变量表实际上也就是上面的环境变量的组织方式:在这里插入图片描述
所以我们可以通过第三个参数来获取所有环境变量:
在这里插入图片描述

也可以通过我们还可以通过第三方变量environ来获取。

在这里插入图片描述
通过系统调用获取环境变量,getenv(); getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。

程序地址空间

再认识这个程序地址空间之前呢,我们先来看一个现象?
这个就是我们学习fork的时候留下的疑问,我们知道id发生了写时拷贝,单独开辟了空间,那为什么这两个id的地址确实一样的呢?
在这里插入图片描述

其实,我们见到的地址实质上都是虚拟地址,会通过页表映射去访问真正的物理地址。状况图如下:
由于子进程继承父进程的进程地址空间信息,所以子进程的进程地址空间也有一个0X40405c,但是两者映射到的物理地址不同 。100的物理地址是0X114514,200的物理地址是0X514411。
在这里插入图片描述

到这里之后,我们虽然知道两个相同地址id为什么会有不同的值了,但是,到底什么是进程地址空间呢?

  所谓进程地址空间,本质是一个描述进程可视内存范围的大小的数据结构对象。
地址空间内存在各种区域的划分,即对线性地址设有不同的start和end;
进程地址空间本质上是内存中的一种内核数据结构,在Linux当中在这里插入图片描述
进程地址空间具体由结构体mm_struct实现,类似PCB,地址空间也是要被操作系统管理的:先描述,再组织。
struct mm_struct的指针存在进程的PCB中。

在结构体mm_struct当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。

对进程的认识更新

所以我们可以更新一下我们对进程的认识了:
进程 = 内核数据结构(task_struct && mm_struct && 页表)+ 数据和程序代码。
所以在创建一个进程的时候,先创建进程的PCB内核数据结构(mm_struct和页表等),其程序代码和数据可以分批地加载进来-------惰性加载(要的时候再拿)。

那么我们为什么要有进程地址空间呢?

  • 让进程以统一的视角看待内存。
  • 有了进程空间虚拟地址,让我们在访问的时候,增加一个转换的过程,在这个过程中,可以对我们的寻求地址进行审查,所以一旦有异常访问,立刻拦截,该请求不会到达物理内存=》保护物理内存。

我们知道,进程是可以被挂起的,但是操作系统怎么知道代码数据在不在内存呢?

  • 其实这个页表的属性有关,页表有专门的一栏来做判断,0和1。若是发现没有数据和代码加载进入,就会出现缺页中断,此时虚拟地址就正在等待映射到物理内存,操作系统会自动去物理内存里面申请一份空间去构建映射关系。 例如:写时拷贝就是一类缺页中断。
  • 进程具有独立性,都有单独的进程地址空间和页表地址。进程进出CPU的时候会保存进程的上下文数据,所以进程切换的时候,PCB一变进程地址空间就跟着变动,上下文中保存着页表地址也会变成下一个进程的(页属于进程上下文)。
  • 对于物理内存如何加载,加载到哪个位置,对于页表映射关系的填写——>这一整套流程叫做Linux的内存管理模块。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值