【Linux】Linux进程概念

一.冯诺依曼体系结构

1.概念

  • 冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同,如英特尔公司的8086中央处理器的程序指令和数据都是16位宽。
  • 数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
冯诺依曼的基本结构如下所示:
在这里插入图片描述
冯诺依曼体系结构可以分为三个基本单元:

  • 存储器(内存)
  • 中央处理器(CPU):含有运算器和控制器等
  • 输入/输出设备:输入(键盘、鼠标、扫描仪、写板等),输出(显示器、打印机等)

关于冯诺依曼,必须强调几点:

  • 这里的存储器指的是内存
  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
  • 一句话,所有设备都只能直接和内存打交道。

2.小思考

为什么在磁盘中编写好的可执行程序(文件),运行的时候必须先加载到内存中?

因为冯诺依曼体系规定!可执行程序是二进制指令,CPU 要执行这些指令,必须先将磁盘中的可执行程序加载到内存中,CPU 才能访问执行这些指令。
存储器的层次结构 中,越往上速度越快,CPU离寄存器最近,离高速缓存也很近,主存(存储器)次之,所以 CPU 间接从主存中访问数据,效率更高。
而让 CPU 直接访问外设(输入或输出设备)肯定是不行的,因为 CPU 特别快,输入输出设备特别慢,导致效率低。
在这里插入图片描述
当一个快的设备和一个慢的设备协同工作的时候,整个体系最终的运算效率肯定以慢的为主。类似木桶效应,当我们让 CPU 直接访问磁盘时,那么木桶的短板的就在磁盘上,整个计算机体系的效率就会被磁盘拖累,这显然不是我们想看到的,所以我们必须把数据写入到存储器中,再让 CPU 一级一级的去访问,而且 CPU 运算的同时,输入 / 输出设备还可以继续将数据写入内存或从内存中读出,这样就可以将 IO 的时间和运算的时间重合,从而提升效率。
在这里插入图片描述

3.实例理解

对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?

  • 在qq上发送消息,数据的流动过程:
    电脑联网后,我用键盘敲下要发送的消息 “在吗?”,此时输入设备是键盘,键盘将该消息写入到内存中,CPU间接从内存中读取到消息,对其进行运算处理后,再写回内存,此时输出设备网卡从内存中读取消息,并经过网络发送到对方网卡,同时输出设备显示器从内存中读取消息并刷新出来,显示在我的电脑上。
    我朋友的电脑,输入设备是网卡,接收到消息后,网卡将该消息写入到内存中,CPU间接从内存中读取到消息,对其进行运算处理后,再写回内存,此时输出设备显示器从内存中读取消息并刷新出来,显示在我朋友的电脑上。
    这样我们就知道了硬件层面的数据流:
    键盘 → 内存 → CPU → 内存 → 网卡 → 网卡经过网络到对方网卡 → 内存 → CPU → 内存 → 显示器
    在这里插入图片描述

二.操作系统(Operator System)

1.概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(例如函数库,shell程序等等)

设计OS的目的:

  • 与硬件交互,管理所有的软硬件资源
  • 为用户程序(应用程序)提供一个良好的执行环境

定位:

  • 在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

2.如何理解 “管理”

计算机管理硬件

  1. 描述起来,用struct结构体
  2. 组织起来,用链表或其他高效的数据结构
  3. 管理的本质是对「数据」进行管理!

管理的例子:
我们在学校里面很少见到校长,说明管理者和被管理者,一般不见面和直接打交道(就像阿里的员工和马云并不见面和直接打交道),那么校长如何进行管理呢?校方是如何知道你是该学校的学生呢?因为你所有的数据早已在学校的系统中,并且一直在更新。
管理的本质是对「数据」进行管理!

计算机软硬件体系结构:
在这里插入图片描述

  • 总结
  • 计算机管理硬件
    1. 描述起来,用struct结构体
    2. 组织起来,用链表或其他高效的数据结构

3.系统调用和库函数概念

  • 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
  • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

三.进程

1.基本概念

进程=内核数据结构(task_struct)+进程对应的磁盘代码和数据
程序的本质就是在磁盘上放着的文件,进程是一个运行起来(加载到内存中)的程序。
进程与程序相比,进程具有动态属性

2.如何管理进程

当有太多的加载进来的程序,操作系统需要将这些加载进来的多个程序管理起来,那么怎么管理这些进程呢?
先描述,再组织

2.1描述进程-PCB

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,通常称之为PCB(process control block)
  • 在Linux中描述进程的结构体叫做task_struct,是PCB的一种
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

PCB概念:

//进程控制块
struct task_struct {
	//该进程的所有属性
	//该进程对应的代码和属性地址
	struct task_struct* next;
};

task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

2.2管理进程

所谓的对进程的管理,变成了对进程对应的PCB进行相关的管理。
对进程管理->转化成了对链表的增删查改
在这里插入图片描述

3.查看进程

3.1通过 /proc 系统文件夹查看

在根目录下有一个名为proc的系统文件夹。
在这里插入图片描述
其中包含大量进程信息
在这里插入图片描述
如果要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹
在这里插入图片描述

3.2使用ps命令查看进程信息

ps命令有很多选项,其中最简单的使用格式如下所示
在这里插入图片描述

这个例子的输出结果列出了两个进程:进程19377和进程23098,它们分别对应bash命令和ps命令。我们可以发现,默认情况下,ps命令输出的信息 并不是很多,只是输出和当前终端会话相关的进程信息。为了获得更多的信息,我们需要添加一些选项,但是在介绍这个之前,让我们先看看ps命令输出的其他字段信息。TTY是teletype(电传打字机)的缩写,代表了进程的控制终端(controllingterminal)。TIME字段表示了进程消耗的CPU时间总和。可以看出,这两个进程都没有使计算机变得忙碌。

如果在ps命令后添加一个选项,那么我们将得到反映系统运行情况的更大视图界面,ps常用选项如下所示:

常用选项

  • a:显示现行终端机下的所有程序,包括其他用户的程序
  • c:列出程序时,显示每个程序真正的指令名称,而不包含路径,选项或常驻服务的标示
  • e:列出程序时,显示每个程序所使用的环境变量
  • f:用ASCII字符显示树状结构,表达程序间的相互关系
  • g:显示现行终端机下的所有程序,包括群组领导者的程序
  • h:不显示标题列
  • u:以用户为主的格式来显示程序状况
  • x:显示所有程序,不以终端机来区分
  • r:只列出现行终端机正在执行中的程序
  • v:采用虚拟内存的格式显示程序状况
  • -a:显示所有终端机下执行的程序,除了阶段作业领导者之外
  • -c:显示CLS和PRI栏位
  • -d:显示所有程序,但不包括阶段作业领导者的程序
  • -e:显示所有程序
  • -f:显示UID,PPIP,C与STIME栏位
  • -H:显示树状结构,表示程序间的相互关系
  • -u<用户识别码>:列出属于该用户的程序的状况,也可使用用户名称来指定
  • -j:采用工作控制的格式显示程序状况
  • -ll:采用详细的格式来显示程序状况
  • -N:显示所有的程序,除了执行ps指令终端机下的程序之外

常用组合选项:
ps aux

在这里插入图片描述
该选项组合将会显示属于每个用户的进程信息,使用这些选项时不带前置连字符-将使得命令以“BSD模式(BSD-style)”运行。ps aux命令显示的信息具体如表所示:

标题含义
USER用户ID。表示该进程的所有者
PID该进程的ID
%CPUCPU使用百分比
%MEM内存使用百分比
VSZ虚拟耗用内存大小
RSS实际使用的内存大小。进程使用的物理内存(RAM)大小(以KB为单位)
TTYteletype(电传打字机)的缩写,代表了进程的控制终端
STAT进程状态
START进程开启的时间。如果数值超过24个小时,那么将使用日期来显示
TIME进程消耗的CPU时间总和
COMMAND进程名称

另一个常用的选项组合是ps ajx,显示进程组id和状态
在这里插入图片描述
ps命令也可以与grep命令搭配使用,具体显示某个指定进程的信息
在这里插入图片描述

3.3使用top命令动态查看进程信息

虽然ps命令可以显示有关机器运行情况的很多信息,但是它提供的只是在ps命令被执行时刻机器状态的一个快照。要查看机器运行情况的动态视图,我们可以使用top命令,如下所示:

[hutao@hecs-414761 lesson9]$ top

top程序将按照进程活动的顺序,以列表的形式持续更新显示系统进程的 当前信息(默认每3秒更新一次)。它主要用于查看系统“最高(top)”进程的运行情况,其名字也来源于此。top命令显示的内容包含两个部分,顶部显示的是系统总体状态信息,下面显示的是一张按CPU活动时间排序的进程情况表:

在这里插入图片描述
系统总体状态信息包含很多有用的内容,具体解释如下:

字段含义
1top程序名
122:55:46一天当中的时间
1up 49days, 8:01正常运行时间(uptime)。从机器最后一次启动开始计算的时间总数。 在这个例子中,系统已经运行了49天8小时01分
12 users有两个用户已登录
1load average:负载均值(load average)指的是等待运行的进程数;即共享CPU资源的 处于可运行状态的进程数。显示的三个值分别对应不同的时间段第一个对应的是前60秒的均值,下一个对应的是前5分钟的均值,最后一个对应 的是前15分钟的均值。该值小于1.0表示该机器并不忙
2Tasks:统计进程数及各个进程的状态信息
20.0 us0.0%的CPU时间被用户进程占用,这里指的是处于内核外的进程
20.2 sy0.2%的CPU时间被系统进程(内核进程)占用
20.0 ni0.0%的CPU时间被友好进程(nice)(低优先级进程)占用
299.8 id99.8%的CPU时间是空闲的
20.0 wa0.0%的CPU时间用来等待I/O操作
4Mem:显示物理RAM(随机存取内存)的使用情况
5Swap:显示交换空间(虚拟内存)的使用情况

4.进程标识符

  • 进程id(PID)
  • 父进程id(PPID)

通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID。
通过一段代码演示一下:

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>

 int main()
 {
      while(1)
      {
          printf("pid: %d\n", getpid());
          printf("ppid: %d\n", getppid());
          sleep(1);
      }
  
      return 0;
 }     

在这里插入图片描述
通过ps命令查看该进程的信息,即可发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同
在这里插入图片描述

命令行上启动的进程,一般它的父进程没有特殊情况的话都是bash

比如我们在命令行上运行的test程序,通过ps命令可以看到它的PPID是28982,而通过ps命令查阅,bash的PID就是28982,因此bash就是./test的父进程

在这里插入图片描述

5.创建进程-fork初识

运行 man fork 认识fork

5.1fork的功能

fork的功能是创建一个新的进程,我们称之为子进程,而原来的进程我们称为父进程,执行完fork之后,父子进程共存,一起执行后续的代码。父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
在这里插入图片描述

5.2fork的返回值

  1. 新的进程创建成功时,在父进程中,fork返回子进程的PID;在子进程中,fork返回0.
  2. 新的进程创建失败时fork返回一个负值
  3. 通过不同的返回值,让父子进程执行后续共享代码的一部分
    在这里插入图片描述

5.3实例

执行下面的代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	int ret = fork();
	if(ret < 0)  //error
	{
    	perror("fork");
        return 1;
    }
    else if(ret == 0)  //child
    {
        printf("I am child : %d!, ret: %d\n", getpid(), ret);
    }
    else   //father                                                                                                                                                    
    {
        printf("I am father : %d!, ret: %d\n", getpid(), ret);
    }
    sleep(1);

    return 0;
}

在这里插入图片描述
运行结果:
在这里插入图片描述
可以看到,fork之后父子进程一起执行后续的代码,由于fork的返回值不同,子进程会进入到 else if 语句当中打印,而父进程会进入到 else 语句中打印。这与我们之前所认识的if-else分支语句只能执行其中的一个完全不同。

小贴士: 使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。

6.进程状态

在Linux操作系统中,每个进程都有不同的状态,一个进程在一个时间段内可能有多个状态了解进程状态的时候,我们需要先了解几个知识:

  1. 进程的信息状态存放地方: 在task_strcut(PCB)中
  2. 进程状态的意义:方便OS(操作系统)快速判断进程,完成特定的功能,满足不同的运行场景

进程在不同的队列中,表示不同的状态。
什么是运行队列

  • 1个CPU只有一个运行队列,进程的执行需要排队。让进程入队列,本质上是将task_struct结构体对象放入运行队列中。
  • 这个进程队列中的指针可以找到需要加载的进程,将其PCB对象的信息加载到CPU中,CPU可以根据PCB对象的信息找到进程对应的代码进行执行。
  • 在CPU的运行队列中的进程状态就叫做运行状态。
  • 状态,是进程的内部属性,存放于task_struct中。这个状态可以理解为一个整数,例如这个整数为1代表运行;2代表停止,3代表死亡状态等······

对于CPU和硬件的速度差异,系统如何调度?

  • 进程不仅仅会占用CPU资源,也会占用硬件资源。对于CPU,它可以很快的处理进程的请求;但是对于硬件,速度很慢,例如网卡,可能同时有迅雷、百度网盘、QQ等进程需要获取网卡的资源,所以每一个描述硬件的结构体中也有一个task_struct* queue运行队列指针,指向排队中的PCB对象的头结点。
  • 那么CPU和硬件的速度差异巨大,系统该怎么平衡这种速度?当CPU发现运行状态的进程需要访问硬件资源时,会让该进程去所需访问的硬件的运行队列中排队,CPU继续执行下一个进程。
  • 那么这个被CPU剥离至硬件运行队列中的进程状态被称为阻塞状态。当进程对硬件的访问结束后,进程的状态将会被修改为运行状态,即该进程重新回到CPU的运行队列。

对于过多的阻塞进程,内存占用如何处理?

  • 硬件的速度较慢,但是大量的进程需要访问硬件,势必会产生较多的阻塞进程,这些阻塞进程的代码和数据在短期内不会被执行,如果全部存在于内存中将会导致内存占用。
  • 对于这个问题,如果内存中有过多的阻塞状态的进程导致内存不足,操作系统会将其的代码和数据先挪动至磁盘,仅留PCB结构体,以节省内存空间,这种进程状态被称为挂起状态。将进程相关数据,加载或保存至磁盘的过程,称为内存数据的换入和换出。
  • 进程的阻塞状态不一定是挂起状态,部分操作系统可能会存在新建状态挂起或运行状态挂起等。

下面是进程状态在Linux内核源代码里的定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

6.1 R运行状态(running)

一个进程处于运行状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里(进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中)

执行下面的代码:

#include <stdio.h>   
 
int main()    
{    
    while(1); 
                              
    return 0;    
}

在这里插入图片描述

注意
状态后面有+号的表示前台进程,没有+号表示后台进程。前台进程在执行时,用户无法继续输入指令除非ctrl+c终止程序;后台进程在执行的过程中,用户可以输入指令且ctrl+c无法杀掉该进程。可以使用kill-9 PID的指令杀掉该进程。

6.2 S睡眠状态(sleeping)

一个进程处于睡眠状态意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep),处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉,是阻塞状态的一种。

执行下面的代码:

#include <stdio.h>    

int main()    
{      
    while(1)    
    {    
        printf("hello!\n");               
    }            
                          
    return 0;                          
} 

在这里插入图片描述
虽然数值一直在打印,但是printf函数需要访问显示器,对硬件设备的访问速度很慢,大部分时间在等显示器IO就绪,只有小部分时间在执行打印代码。所以该代码呈现睡眠状态。
需要访问外设的,一般属于睡眠状态。

6.3 D磁盘休眠状态(Disk sleep)

有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的
进程通常会等待IO的结束。
在该状态下的进程,无法被操作系统杀掉,即使使用kill -9也无法杀死处于D状态的进程,只能通过断电或者进程自己醒来的方式中断深度睡眠状态。

6.4 T停止状态(stopped)

可以通过发送 SIGSTOP 信号或者kill -19 PID给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号或者kill -18 PID让进程继续运行。

执行下面的代码:

#include <stdio.h>   
 
int main()    
{    
    while(1); 
                              
    return 0;    
}

在这里插入图片描述

tips:使用kill命令可以列出当前系统所支持的信号集
在这里插入图片描述

6.5 Z(zombie)-僵尸进程

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

来一个创建维持30秒的僵死进程例子

#include <stdio.h>
#include <stdlib.h>

int main()
{
	pid_t id = fork();
	if(id < 0){
		perror("fork");
		return 1;
	}
	else if(id > 0){ //parent
		printf("parent[%d] is sleeping...\n", getpid());
		sleep(30);
	}
	else{  //child
		printf("child[%d] is begin Z...\n", getpid());
		sleep(5);
		exit(EXIT_SUCCESS);
	}
	
	return 0;
}

运行该代码后,我们可以通过以下监控脚本,对该进程的信息进行检测

while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;echo "######################";sleep 1;done

检测后即可发现,当子进程退出后,子进程的状态就变成了僵尸状态。
在这里插入图片描述

僵尸进程的危害:

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说, Z状态一直不退出,PCB一直都要维护?是的!
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏!

6.6 X死亡状态(dead)

在Z状态的进程被回收后,进程状态变为X死亡状态(dead):父进程读取完子进程的返回信息后,收尸速度太快了,我们看不到,进程死亡状态立马被它的父进程回收。这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

6.7 孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程”
  • 孤儿进程被1号init进程领养,当然要由init进程回收喽
  • 如果是前台进程创建的子进程,如果孤儿了,就会自动变成后台进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
	pid_t id = fork();
	if(id < 0){
		perror("fork");
		return 1;
	}
	else if(id == 0){  //child
		printf("I am child, pid : %d\n", getpid());
		sleep(10);
	}
	else{  //parent
		printf("I am parent, pid: %d\n", getpid());
		sleep(3);
		exit(0);
	}
	
	return 0;
}

在这里插入图片描述

7.进程优先级

7.1 基本概念

  • 由于资源太少了,资源的分配具有先后顺序。cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
  • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整 体性能
  • 进程的优先级本质就是PCB里面的一个整数数字(也可能是几个)

在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
在这里插入图片描述
我们很容易注意到其中的几个重要信息,有下:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI:代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值

7.12 PRI和NI

PRI and NI

  • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 进程的优先级别越高
  • 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice,其中PRI(old)=80
  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 所以,调整进程优先级,在Linux下,就是调整进程nice值 ,nice其取值范围是-20至19,一共40个级别
  • 因此,优先级的范围是[60,99]

PRI vs NI

  • 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进 程的优先级变化。
  • 可以理解nice值是进程优先级的修正修正数据

7.3 进程优先级相关的命令

查看进程优先级:
当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息
在这里插入图片描述
注意: 在Linux操作系统中,初始进程一般优先级PRI默认为80,NI默认为0

更改已存在进程的nice:

  • top
  • 进入top后按“r”–>输入进程PID–>输入nice值
  • 设置的时候,调优先级本来就会影响调度器的效率,所以在设置的时候可能要进行权限的设置。要使用sudo top

先使用ps -al查看进程的NI值,可以看到PID为8840的进程NI值为0
在这里插入图片描述
输入top,进入该界面
在这里插入图片描述
按下r后,会提示你输入要改变NI值得进程得PID,这里我们输入8840,输入完毕后按回车键
在这里插入图片描述
然后会提示你输入新的NI值,我们输入10,然后按下回车键就修改完成了
在这里插入图片描述
输入q退出top
使用ps -al查看是否修改NI值成功,可以看到NI值变成了10,PRI变成了90,修改成功
在这里插入图片描述

8.进程切换

8.1 进程重要概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

8.2 进程的切换

在CPU中有一套寄存器,由于存储数据
在这里插入图片描述

  • cpu中有一个eip寄存器(PC指针),指向下一条指令的地址
  • 在任何时刻,CPU里面的寄存器里面的数据,看起来是在大家能看到的寄存器上的,但是,寄存器内的数据,只属于当前运行的进程!
  • 寄存器被所有的进程共享,寄存器内的数据,是每个进程各自私有的–上下文数据。
  • 寄存器硬件!=寄存器内的数据
  • 当我们的进程运行的时候,一定会产生非常多的临时数据,这份数据属于当前进程
  • 进程在运行的时候,占有CPU,但并不是一直占有到进程结束。进程在运行的时候,都有自己的时间片,这个时间一到,即使进程还没有被执行完毕,CPU也会切换下一个进程运行,每个进程依次轮流运行,直至结束
  • 那么这个进程下次再回到CPU继续运行时,操作系统是如何知道这个进程的代码被执行到哪里了?
  • 进程在切换的时候,要进行进程的上下文保护,将数据保存到其他地方;进程在恢复运行的时候,要进行上下文的恢复,后续该进程回到CPU运时,将加载这些数据。通过PC指针继续运行下一行代码。

9. 环境变量

9.1 基本概念

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,操作系统通过环境变量来找到运行时的一些资源
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

9.2 常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL :当前Shell,它的值通常是/bin/bash

9.3 查看环境变量方法

echo $NAME //NAME:你的环境变量名称
在这里插入图片描述

9.4 测试PATH

功能和作用

  • 可执行程序的搜索目录,可执行程序包括Linux系统命令和用户的应用程序。如果可执行程序的目录不在PATH指定的目录中,执行时需要指定目录。

环境变量PATH中存在/usr/bin目录(这个目录其实是指令存放的目录)。当要执行一个程序的时候,在程序名字前面加上./表示在才能运行起来,比如./test;而当我们在执行ls,pwd等指令的时候,不用加上./就能直接运行起来,如果我们运行自己创建的mycmd程序的时候不加./的话,系统就会报错,找不到这个可执行文件
在这里插入图片描述
这是因为当我们要执行一个程序的时候,先要找到这个程序,不带./就可以执行ls命令,bash会根据PATH记录的这些路径依次在这些路径去检索,从而找到ls存放的目录;而系统无法找到我们自己创建的可执行程序存放的目录没有在环境变量PATH中,bash找不到,所以我们必须带上./,以此告诉系统该可执行程序位于当前目录下
查看环境变量PATH我们可以看到如下内容:
在这里插入图片描述
可以看到环境变量PATH当中存放着一系列由’:'所分割的路径。当我们执行ls这个命令时,bash会根据PATH记录的这些路径依次在这些路径去搜索,找到了就执行;全部路径都找不到就会显示找不到该命令。ls这个命令存放在usr/bin这个目录下,可以在PATH找到,因此可以执行成功。

让自己写的可执行程序无需添加./也可执行的方法:
Ⅰ.将可执行程序复制到/usr/bin中

sudo cp myprocess /usr/bin/

在这里插入图片描述
但是不建议将我们自己写的程序放进这个目录下,这样会污染系统的环境,因此需要删除刚刚加进去的程序

sudo rm /usr/bin/myprocess

Ⅱ.将程序所在的路径添加至环境变量PATH当中

export PATH=$PATH:程序所在路径

在这里插入图片描述
查看是否添加成功
在这里插入图片描述
程序也可以直接运行
在这里插入图片描述

9.5 测试HOME

环境变量HOME中保存了该用户的主工作目录,也就是家目录~

用root和普通用户,分别执行 echo $HOME ,对比差异
普通用户
在这里插入图片描述
root
在这里插入图片描述

执行 cd ~; pwd ,对应 ~HOME 的关系
普通用户
在这里插入图片描述
root
在这里插入图片描述
可以看到root和普通用户中的环境变量HOME存储着它们各自的家目录

9.7 和环境变量相关的命令

1.echo: 显示某个环境变量值(注意在环境变量前面加上$)
在这里插入图片描述
2. env: 显示所有环境变量
在这里插入图片描述
3. export: 设置一个新的环境变量
我们可以自己设置一些本地变量

[hutao@hecs-414761 ~]$ MYVAL="123456"

注意:如果环境变量的值有空格等特殊符号,必须用双引号包含

但是设置完后使用env查看环境变量并不能找到我们自己设置的变量,这是因为它现在还只是一个本地变量,只会在当前进程内有效
在这里插入图片描述
环境变量具有全局属性,会被子进程继承下去,这是为了不同的应用场景
使用export可以将本地变量设置成环境变量

[hutao@hecs-414761 ~]$ export MYVAL

现在使用env就可以查看到我们自己设置的环境变量
在这里插入图片描述
使用export设置一个新的环境变量还有另一种格式

[hutao@hecs-414761 lesson14]$ export MYVAL=123456

4. unset: 清除环境变量
比如清除我们刚刚设置的环境变量MYVAL
在这里插入图片描述
5. set: 显示本地定义的shell变量和环境变量

9.8 环境变量的组织方式

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
在这里插入图片描述
通过代码获取环境变量的三种方式:
1. getenv函数(推荐使用)
使用man查看getenv这个函数
在这里插入图片描述
可以看到getenv的头文件是<stdlib.h>,返回值是一个字符串,因为环境变量本身就是一个字符串
编译并运行下面的代码:

#include <stdio.h>
#include <stdlib.h>

#define USER "USER"

int main()
{	
	char *who = getenv(USER);  
	printf("user: %s\n", who);  

	return 0;
}

在这里插入图片描述
切换到root执行上面的代码:
在这里插入图片描述
所以USER环境变量的最大意义:可以表示当前使用Linux的用户
操作系统也是根据USER来判断你是否具有某个操作的权限
来看下面这段代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define USER "USER"

int main()
{
	if(strcmp(who, "root") == 0)
    {
		printf("user: %s\n",who);
    }
	else
	{
		printf("权限不足\n");
	}

	 return 0;
}

编译并运行
在这里插入图片描述
当你以root身份运行的时候,可以成功;而当你以普通用户运行的时候,会提示你“权限不足”。操作系统就是这样来判断你是否具有权限的

2.命令行第三个参数char *env[]
main()其实可以带3个参数,我们先来看前两个参数

#include <stdio.h>

int main(int argc, char *argv[])
{
	for(int i = 0; i < argc; ++i)                                                                                                                                      
	{                            
		printf("argv[%d]->%s\n", i, argv[i]);
 	}   
 	
	return 0;
}

在这里插入图片描述
命令行参数本质上是把程序名和选项依次传递给argv的,程序名加选项一个有多少个,我们就需要定义多大的argv数组,argc传递的就是数组的大小
我们输入的命令行操作整体是一个长字符串

 "ls -a -b -c -d -e" ----> 长字符串

当我们做命令行解析的时候,以空格为单位把它拆成一个一个的子字符串,然后传递给argv数组

"ls" "-a" "-b" "-c" "-d" "-e"

在这里插入图片描述

例子:
比如我们想自己实现一个带有一个选项的程序

./mycmd -a/-b/-c/-ab/-ac/-bc/-abc

可以看如下代码:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
	if(argc != 2)
    {
        printf("Usage: \n\t%s [-a/-b/-c/-ab/-ac/-bc/-abc]\n", argv[0]);
        return 1;
    }
    if(strcmp("-a", argv[1]) == 0)
    {
    	printf("功能a\n");
    }
    if(strcmp("-b", argv[1]) == 0)
    {
        printf("功能b\n");                                                                                                                                             
    }
    if(strcmp("-c", argv[1]) == 0)
    {
        printf("功能c\n");
    }
    if(strcmp("-ab", argv[1]) == 0)
    {
        printf("功能ab\n");
    }
    if(strcmp("-ac", argv[1]) == 0)
    {
        printf("功能ac\n");
    }
    if(strcmp("-bc", argv[1]) == 0)
    {
        printf("功能bc\n");
    }
 	if(strcmp("-abc", argv[1]) == 0)
    {
        printf("功能abc\n");
    }

	return 0;
}

在这里插入图片描述
这和我们使用的ls -als -lls -al的原理是一样的

main函数还有第三个参数char *env[],里面存放着一个一个的环境变量,以NULL结尾
运行下面代码:

#include <stdio.h>

int main(int argc, char *argv[], char *env[])
{
	for(int i = 0; env[i]; ++i)                                                                                                                                      
	{                            
		printf("env[%d]:%s\n", i, env[i]);
 	}   
 	
	return 0;
}

运行结果就是各个环境变量的值:
在这里插入图片描述
3. 通过第三方变量char **environ获取
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明

#include <stdio.h>

int main()
{
	extern char **environ;
	for(int i = 0; environ[i]; i++)
	{
		printf("%s\n", environ[i]);
	}
	return 0;
}

编译并运行:
在这里插入图片描述
也能打印出所有的环境变量

10. 进程地址空间

10.1 程序地址空间回顾

在学习C语言的时候,大家都看到过这样的空间布局图
在这里插入图片描述
可是我们对他并不理解!

来段代码感受一下

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_val = 0;

int main()
{
	pid_t id = fork();
	if(id < 0){
		perror("fork");
		return 0;
	}
	else if(id == 0){ //child
		printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	else{ //parent
		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	
	sleep(1);
	return 0;
}

输出:

//与环境相关,观察现象即可
parent[22087]: 0 : 0x601058
child[22088]: 0 : 0x601058

我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
	pid_t id = fork();
	if(id < 0){
		perror("fork");
		return 0;
	}
	else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
		g_val=100;
		printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	else{ //parent
		sleep(3);
		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	
	sleep(1);
	return 0;
}

输出结果:

//与环境相关,观察现象即可
child[22374]: 100 : 0x601058
parent[22373]: 0 : 0x601058

我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  • 但地址值是一样的,说明,该地址绝对不是物理地址!
  • 在Linux地址下,这种地址叫做虚拟地址
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
  • 进程地址空间,会在进程的整个生命周期内一直存在,直到进程退出!
  • 这也就解释了为什么全局/静态变量的生命周期是整个程序,因为全局/静态变量是随着进程一直存在的
  • OS必须负责将虚拟地址转化 物理地址
  • 同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址

在这里插入图片描述

10.2 进程地址空间

  1. 地址空间描述的基本空间大小是字节
  2. 32位的平台有2^32个地址,也就是虚拟地址
  3. 2^32 * 1byte = 4GB 的空间范围
  4. 每一个字节都要有唯一的地址

每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。
例如,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图:
在这里插入图片描述

而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
例如,子进程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并且改变子进程当中g_val的虚拟地址通过页表映射后得到的物理地址即可。
在这里插入图片描述
这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术,将不同进程的数据进行分离。任何一方尝试写入,OS先进行数据拷贝,更改页表映射,然后再让进程进行修改。

1. 为什们要进行写时拷贝?
因为进程具有独立性,一个进程对被共享的数据做修改,如果影响了其他进程,不能称之为独立性
2. 为什么不在创建子进程的时候就进行数据的拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。
3. 代码会不会进行写时拷贝?
90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

为什么存在地址空间?

  1. 如果让进程之间访问物理内存,万一进程越界非法操作,就会非常不安全
  2. 地址空间的存在,可以更方便地进行进程和进程的数据代码的解耦,保证了进程独立性这样的特征
  3. 让进程以统一的视角,来看待进程对应的代码和数据各个区域,方便使用
  4. 编译器也以统一的视角来进行代码编译,编译器在对代码进行编译的时候,就是按照虚拟地址空间的方式进行对我们的代码和数据进行编址的

对于创建进程的现阶段理解:
一个进程的创建实际上伴随着其进程控制块(task_struct)进程地址空间(mm_struct)以及页表的创建。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值