【Linux】进程基础知识

一. 认识操作系统

1. 什么是操作系统?

概念:操作系统是管理软硬件工作的软件

2. 为什么要有操作系统?

操作系统在计算机系统中处于一个承上启下的位置:

  • 向下通过驱动程序与硬件间接交互,充分发挥硬件的功能
  • 向上为用户提供接口,给用户提供稳定、高效、简易的使用环境

3. 计算机体系结构

自下而上分为:硬件部分 -> 系统软件部分 -> 用户部分

在这里插入图片描述

下面介绍和体系结构相关的几个概念

驱动程序

什么是驱动程序?
驱动程序全称:设备驱动程序(Device driver),是一种特殊的程序,它本质就是一段包含有硬件设备信息的代码。在 Linux 内核中 '驱动程序的代码 ',会占到 ”操作系统的内核源码“的 70%。

并不是说所有硬件设备都需要驱动程序才能使用,像 CPU、内存等就不需要,因为这些硬件对于一台电脑而言过于重要,他们都是 BIOS 所直接支持的硬件。

为什么要有驱动程序?
驱动程序完成硬件 ‘设备的电子信号’ 和 ‘计算机系统的代码指令’ 之间的翻译,是硬件和操作系统之间的"桥梁“。

我们的计算机使用的是代码指令,而外部硬件设备识别的是电子信号,这是两个完全不同的东西,所以我们的计算机与外设要通过’驱动程序’ 进行互动通信。

驱动程序如何工作?
比如我们要播放音乐:

  • OS 会发送指令给 ‘声卡-- 驱动程序’。
  • '声卡-- 驱动程序’收到指令后,将其’翻译’成 声卡能听懂的 ‘电子信号’。
  • 然后把这个’电子信号’给到声卡,让声卡播放音乐。

系统调用

操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用(接口)。系统调用是通向操作系统本身的接口,是面向底层硬件的。通过系统调用,可以使得用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互。

例如read() 接口:用于从打开的文件或设备中读取数据 ,在其实现中,内核将调用内核相关函数 sys_read() ;用户程序不能直接调用这些函数,这些函数运行在内核态,CPU 通过软中断切换到内核态开始执行内核系统调用函数。

库函数

系统调用函数的功能比较基础,对用户的要求相对也比较高,所以,有心的开发者就对部分系统调用进行适度封装,从而形成库。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。一般放在 .lib 文件中。有了库,就很有利于上层用户或者开发者进行二次开发。

例如 read() 函数根据传入参数,直接就能读文件,而背后隐藏的比如文件在硬盘的哪个磁道,哪个扇区,加载到内存的哪个位置等等这些操作,我们是不必关心的,这些操作里面当然包含了很多系统调用。

系统调用和库函数的关系

系统调用和库函数是上下级的关系,一般涉及到硬件的库函数都要经过系统调用,但比如算法相关的库函数就不用经过系统调用。

二. 进程

1. 进程概念

进程是被加载到内存中,正在运行或等待运行的程序,它由很多个部分组成,包括:PCB、页表、虚拟地址空间和缓冲区等。

编写一个死循环程序,并编译生产一个可执行程序 proc,最后运行起来这个死循环程序:
在这里插入图片描述

在另一个终端,使用 ps 指令可以查看到这个进程相关信息:
在这里插入图片描述

USERPID%CPU%MEMVSZRSSTTYSTATSTARTTIMECOMMAND
进程的所有者进程ID号运算器占用率内存占用率虚拟内存使用量(单位是KB)占用的固定内存量(单位是KB)所在终端进程状态被启动的时间实际使用CPU的时间命令名称与参数
ljj2161597.40.0132241288pts/0R+18:278:17./proc

进程和程序的关系

进程是动态的,程序是静态的,进程有创建,执行,消亡,所以进程实体是有生命周期的,而程序只是一组有序指令的集合。

2. 描述进程的实体 — PCB

为了描述和控制进程的运行,系统为每个进程定义了一个结构体——进程控制块PCB(Process Control Block),它里面描述了很多与进程有关的信息。

PCB 是进程存在的唯一标志。 Linux 系统中的 PCB 叫做 task_struct ,而在 Windows 操作系统中则使用一个执行体进程块 EPROCESS(全称 Execute Process)来表示描述进程。
在这里插入图片描述

2.1 进程标识符

  • PID:全称 Procss ID,操作系统给该进程的唯一标示符(编号),用来区别其他进程
  • PPID:全称 Parent Procss ID,父进程的标识符

在这里插入图片描述

2.2 状态

标识进程当前所存在的形态
在这里插入图片描述

①:R — 运行状态(running)
该状态不意味着进程一定在运行中,它表明该进程要么正在 CPU 里运行,要么处在运行队列里等待运行。

在这里插入图片描述

前台运行与后台运行

./ proc,运行可执行程序 proc,我们看到它的运行状态是 R+,说明该进程在前台运行,前台运行的进程只能有一个。
在这里插入图片描述
./proc &,在最后位置加上取地址符号来运行程序 proc,可以看到它的状态是 R,说明该进程在后台运行,后台运行的程序可以有无数个。
在这里插入图片描述

②:S — 睡眠状态(sleep)
也叫作可中断睡眠状态(interruptible sleep)。进程在等待事件发生而被放入对应事件的等待队列中,当这些事件发生时(由外部中断或由其他进程触发),对应等待队列中的一个或多个进程将被唤醒,S 状态将被解除。

通过 top 命令我们会看到,大多数进程都处于 S 状态,因为进程很多,而 CPU 一次只能运行一个,如果不是绝大多数进程都在睡眠,CPU 又怎么响应得过来。
在这里插入图片描述

③:D — 磁盘休眠状态(Disk sleep)
也叫作不可中断睡眠状态(uninterruptible sleep)。进程不响应异步信号,即不受任何信号影响,通常会等待 IO 的结束才唤醒。看这个名字就知道该状态与硬件有关,比如进程使用 read 这个系统调用接口对某个设备文件进行读操作,而 read 系统调用最终会执行到对应设备驱动的代码,并与对应的物理设备进行交互,这个时候通常进入这个状态以对进程进行保护,避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。

④:T — 停止状态(stopped)
向进程发送一个 SIGSTOP 信号,它就会因响应该信号而进 T 状态(除非该进程本身处于 D 状态而不响应信号),然后再向进程发送一个 SIGCONT 信号,就可以让其解除 T 状态。

当进程正在被跟踪时,它处于 T 这个特殊的状态。“正在被跟踪”指的是进程暂停下来,以等待跟踪它的进程对它进行进一步的操作。比如我们在调试到断点处时进程就处于 T 状态。

⑤:X — 死亡状态(dead)
预告进程即将被销毁。这个状态只是一个返回状态,销毁过程是非常短暂的,几乎不可能通过ps命令捕捉到。

⑥:Z — 僵尸状态(zombie)
子进程退出,父进程仍然在运行,但父进程没有调用 wait 或者 waitpid 函数完成对子进程资源的最后清理,这时子进程就处于僵尸状态。

成因

子进程在被销毁时操作系统会来清理该进程的资源,除了它的 task_struct,因为它要告诉关心它的父进程,你交给我的任务,我办的结果如何?子进程在退出的过程中,内核会给其父进程发送一个信号,通知父进程来“收尸”。这个信号默认是 SIGCHLD,父进程收到后可以通过 wait 系列的系统调用函数(如 wait、waitid)来等待某个或某些子进程的退出并获取它的退出码,顺带销毁子进程的 task_struct 结构

结果

只要父进程不退出,这个僵尸状态的子进程就会一直存在,进而导致内存泄漏。那么如果父进程在子进程之前先退出了呢?谁又来给子进程“收尸”?这种没有父亲的子进程称为:孤儿进程

这些孤儿进程会被托管给退出进程所在进程组的下一个进程(如果存在的话),或者是1号进程。

1号进程,即 pid 为1的进程,又称 init 进程。Linux 系统启动后,第一个被创建的用户态进程就是 init 进程。它有三项使命:

  • 执行系统初始化脚本,创建一系列的进程(其后创建的所有进程都是 init 的子孙进程)
  • 在一个死循环中等待其子进程的退出事件,并调用 waitid 函数来完成子进程的“收尸”工作。
  • 收养孤儿进程。如果有孤儿进程要退出,init 会调用 wait 系列的系统调用函数来帮助孤儿进程退出;如果孤儿进程不退出的话,init 就充当它的父进程。

2.3 优先级

优先级决定的是在已经可执行的前提下谁先执行的问题,权限决定能否被执行。PRI 越小,表示优先级越高,那该进程就可以优先执行,这就好像考试排名的数字一样,越小越靠前。

在 Linux 或者 Unix 系统中,用 ps –l 命令可以看到进程相关的优先级信息:
在这里插入图片描述

  • PRI(全称 priority)= PRI_old + NI(称为 NICE 值)
  • PRI 值越小其优先级会变高,就越快被执行
  • PRI_old 是常量 80,而 NI 的取值范围是 [-20, 19],一共40个级别
  • 一般是通过修改 NI 来改变一个进程的优先级的

用 top 命令更改已存在进程的 nice 值

首先输入 top,可以看到所有的进程:
在这里插入图片描述

输入 r,提示你输入要修改进程的 PID
在这里插入图片描述

最后输入你要修改的 NI 的值。输入如果超出 NI 的范围,就按边界的值算
在这里插入图片描述

2.4 程序计数器

程序中即将被执行的下一条指令的地址,当运行中的进程被其他优先级更高的进程占用或该进程执行的时间片到了,该进程 task_struct 里的程序计数器就会保存下一条执行指令的地址,这样下一次运行该进程时就可以接着上次中断的位置继续运行。

2.5 内存指针

包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针,进程运行时可以读取代码和相关数据。

2.6 上下文数据

进程执行时放在处理器的寄存器中的还没来得及写入目标区域的数据。这样下次进程在 CPU 运行时,可以从寄存器中读取到这些数据。

进程退出的两种情况

①时间片到了。进程的时间片到了就必须退出,等待下一次运行。

②抢占。当进程在运行时并且时间片还没到,来了个优先级更高的进程,它就会抢占这个运行中的进程。

进程的代码还没有全部跑完就退出,退出去它会把还没有来得及写入目标区域的数据都放到寄存器中,下次运行时可以读取并恢复这些数据。

3. 进程的组织方式

每个 task_struct 中都有一个 tasks 的域,它可以被添加到进程链表中(进程链表把一个个单独的进程以双向链表的形参式组织起来)
在这里插入图片描述

三. 进程的虚拟地址空间

1. 什么是虚拟地址空间

在回答这个问题之前我们,我们先来感受一下
在这里插入图片描述

我们发现,输出出来的变量的值和地址是一模一样的,很好理解呀,因为子进程复刻了父进程,且父子都没有对全局变量的值进行修改。

我们将代码稍加改动:在父进程的执行逻辑里增加一条修改全局变量的操作: g_val=200
在这里插入图片描述

我们发现,父子进程中输出全局变量的地址还是一样的,但是变量的值不同了!!!

推到得到得到如下结论:

  1. 变量内容不一样,说明父子进程输出的变量绝对不是同一个变量
  2. 但它们地址值是一样的,说明该地址不是真正的物理地址
  3. 在 Linux 中,这种地址叫做虚拟地址,其实它也是一种结构体叫做 mm_struct。以前我们在学习 C/C++ 时所看到的地址,也全都是虚拟地址!!真正的物理地址,用户是看不到的,这些操作都是由 OS 统一管理的。

在 32 位系统下的进程地址空间的布局,如下图所示:
在这里插入图片描述

其实我们看到的这个地址空间是虚拟的,目的是为了让进程以为自己拥有了一块连续的空间。实际这些连续的虚拟的地址都是通过页表映射到真实物理空间的,也就是说每个进程都有属于它自己的虚拟地址空间,虚拟地址空间再配合它们各自的页表就能够映射到同一块真实的物理空间上了。

进程地址空间本质上是内存中的一种内核数据结构,叫做 mm_struct,前面的 mm 是 memory简写。源代码里可以看到,它里面有对堆、栈、数据段和代码段等各个区域的划分,并记录它们各自的起始位置和终止位置。
在这里插入图片描述

前面说过,task_sturct 是进程存在的唯一表示,作为一个管理进程的数据结构,task_struct 里就应该存有指向进程虚拟地址空间的指针:
在这里插入图片描述

虚拟内存通过页表映射到真实的物理内存,当然页表也是一种数据结构,每个进程都有各自的页表:
在这里插入图片描述

2. 为什么要使用虚拟地址空间

①统一标准
对于进程:它们都认为自己看到的是相同的一段空间,统一了各个区段的名称、顺序。

对于开发者:虚拟地址空间展示给我们的是连续的地址,这有利于我们进行学习、开发和维护。

②保护内存
如果所有进程都直接到真实的物理内存中去读写数据,缺乏对内存的访问控制和保护,这样是很不安全的。而通过虚拟地址空间和页表的映射,我们就只能间接操作物理内存,一旦间接了,操作系统就可以在其中插一把手,这样可以更好的保护内存。

  • 页表中有标记物理内存上数据的读写权限。若发现你要修改只读空间的数据,那么就会报内存访问错误以阻止你的操作。
  • 从此以后,不会有任何系统级别的越界问题存在了,系统级别的意思是不会再错误的访问物理内存了。比如你去访问野指针,这个时候操作系统会去检查页表的映射关系,发现野指针地址对应到的物理内存并没有被申请,这时操作系统就会终止你的进程。
  • 当对空指针访问时,即访问编号为 0 的虚拟地址,此时程序发生段错误。虚拟内存的 0 号位置实际上是有空间的,不过不能读写,这是操作系统规定的。

③进程独立
页表可以映射到真实的物理内存,每一个进程都只能访问自己虚拟地址空间所映射的物理内存。这样多进程运行,独享各种资源,且在运行期间不会相互干扰,每个进程都认为自己是在独占内存。

3. 虚拟地址空间的使用

子进程的创建

fork() 后子进程拥有了自己的 pcb、虚拟地址空间和它自己的页表。但是起初父子进程共用代码(因为程序一旦运行了,代码是不能够被修改的)和相关的数据。

这是为了节省空间所以才共用的,不单独再为子进程在物理空间上另外开辟和父进程一样的数据和代码;只有当子进程或父进程单独对代码里的对象进行写操作时,它们的数据才会分离,操作系统在物理内存上单独开辟另外一块空间,并更改子(或父)进程页表的对应关系。

接下来就可以解释前面的现象了:子进程创建后,生成了一份和内容和父进程一模一样的虚拟地址空间(mm_struct)。因为父子都没有修改代码里对象的值,为了节省物理内存,各自的页表都映射到同一块物理空间(指向同一个 g_val)。

在这里插入图片描述

接下来我们在父进程里修改 g_val = 200,这时操作系统要在物理内存上重新开辟一块空间存父进程修改后的 g_val 的值,并更改父进程页表的映射关系,其它没修改的数据依然共享,这样做可以极致地节省内存空间;主打一个能一起用就一起用,如果其中一个要进行写操作的话就重新给这个要写的变量另外开辟一块空间存储它修改后的值,这种技术也叫作写时拷贝,顾名思义写的时候才拷贝。

在这里插入图片描述

子进程从哪开始运行?

我们是先运行父进程,然后父进程中才 fork() 创建的子进程,那么子进程从哪里开始运行呢?
在这里插入图片描述

要注意的是 printf 函数是行缓冲函数,满足下面条件之一才会将缓冲区内容刷到对应的文件(一般是 stdout 即显示器)中。

  1. 缓冲区被填满
  2. 写入的字符中有’\n’或’\r’
  3. 调用 fflush 手动刷新
  4. 程序结束

接下来我们给 printf 函数加上 ‘\n’,对比看看有什么效果:
在这里插入图片描述

4. 重新理解进程

进程就是代码 + 数据 + 一堆数据结构的集合,这些数据结构包括:PCB(进程控制块)、mm_struct(进程的虚拟地址空间)、页表、缓冲区等。

四. 环境变量

1. 什么是环境变量

环境变量一般是指操作系统中指定操作系统运行环境的一些参数。它相当于是一个字符串指针变量,想要查看环境变量的值,需要在前面加上 ‘$’,这类似于访问指针变量的值时,对指针变量进行解引用操作。

环境变量的查看方式有两种:

1. env (查看所有的环境变量以及他们的值)
2. echo $环境变量名 (查看特定环境变量的值)

在这里插入图片描述

2. 环境变量的作用

用来存储字符串信息,这些信息可以被系统访问,也可以被我们的应用程序所访问。

3. 环境变量的组织方式

每个程序都有一张环境表,环境表是一个字符串指针数组,每个指针变量指向一个以 ‘\0’ 结尾的字符串。
在这里插入图片描述

main 函数的第三个参数就是环境表:
在这里插入图片描述

运行上图的程序我们可以打印所有的环境变量和他们对应的值:
在这里插入图片描述

补充:关于 main 函数的前两个参数

在这里插入图片描述

执行可执行程序后,第一个命令行参数就是我们执行该程序的命令,即 argv[0] = 该程序的执行命令。
在这里插入图片描述

如果我们在输入执行程序的命令(./text)时加上选项,这些选项会按照先后顺序加入到命令行参数列表中:
在这里插入图片描述

我们可以在代码里判定命令行参数的值,根据值的不同,再去执行不同的操作:
在这里插入图片描述

4. 常见的环境变量介绍

4.1 PATH

指定命令的默认搜索路径

我们平时用的命令如:ls、pwd 等都是操作系统在 PATH 环境变量里的这些路径下去寻找并执行的。
在这里插入图片描述

这也就解释了为什么我们自己生成的可执行程序(也算是命令)在执行时必须加上路径(比如是 ./text,即执行当前目录下的 text 这个可执行程序),因为 PATH 下的路径里面没有存放我们自己写的可执行程序;而其他系统命令只要输入它们的名称就可以运行,这是因为 PATH 下的路径里有存放着这些命令对应的可执行程序。
在这里插入图片描述

我们也可以把自己的程序放到 PATH 下的路径里,这样不论在哪里,我们直接输入这个可执行程序名称都可以执行它。

举个例子下面是我们自己写的可执行程序:
在这里插入图片描述

方法一:永久的

把我们的可执行程序拷贝到 PATH 路径里。比如我们把我们的可执行程序 text 拷贝到 /use/bin 里。
在这里插入图片描述

添加完成后可以直接运行 text,不用在说明这个程序所在的路径
在这里插入图片描述

不过不推荐这样使用,因为这种方式会污染 Linux 自带的命名池。

删除的话就是用绝对路径把这个可执行程序删除就行。
在这里插入图片描述

方法二:临时的

这里的临时的意思是,只在当前登录时有效,下一登录后就失效了。方法是把这个可执行程序所在路径的绝对路径加到 PATH 环境变量里,这样你只要输入程序的名称,系统就会到 PATH 里找到你所添加的路径,并在这个路径下寻找你的可执行程序。

//中间以冒号隔开,没有空格
export PATH=$PATH:<PATH1>:<PATH2>:<PATH3>: .... :<PATHN> 

还是以我们的 text 程序为例
在这里插入图片描述

我们直接添加到 PATH 中的路径在退出登录后会自动清除。其实每一次登陆后,操作系统都会重新配置用户的环境变量,PATH 中默认的这些路径都是操作系统给我们安排好的。

4.2 HOME

指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)
在这里插入图片描述

  • 从结果上看 HOME 的值等价于执行指令执行 cd ~ + pwd 的结果。
  • cd ~ 这里的波浪号对应 HOME 的值。

4.3 SHELL

该变量的值为用户当前使用的解析器 ,就是当前 Shell,在 Linux 中它的值通常是 /bin/bash。
在这里插入图片描述

  • 13
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值