您的位置:首页 > 健康 > 美食 > h5网站源代码_微信小程序怎么创建店铺_百度网站怎么优化排名靠前_手机黄页怎么找

h5网站源代码_微信小程序怎么创建店铺_百度网站怎么优化排名靠前_手机黄页怎么找

2025/4/23 20:29:46 来源:https://blog.csdn.net/2402_86037826/article/details/146485877  浏览:    关键词:h5网站源代码_微信小程序怎么创建店铺_百度网站怎么优化排名靠前_手机黄页怎么找
h5网站源代码_微信小程序怎么创建店铺_百度网站怎么优化排名靠前_手机黄页怎么找

文章目录

  • 1. 冯诺依曼体系结构
    • 1.1 什么是冯诺依曼体系结构
    • 1.2 为什么选择冯诺依曼结构
  • 2. 操作系统
    • 2.1 操作系统是什么
    • 2.2 操作系统如何对硬件资源进行管理
    • 2.3 计算机的层状体系结构
  • 3. 进程
    • 3.1 进程是什么
    • 3.2 进程的相关属性
    • 3.3 在Linux中了解进程
      • 3.3.1 查看进程
      • 3.3.2 子进程由父进程创建
  • 4. 进程状态
    • 4.1 Linux中进程的双链表管理方式
    • 4. 2 进程具体状态
      • 4.2.1 新建状态
      • 4.2.1 就绪状态和运行状态
      • 4.2.2 阻塞状态
      • 4.2.3 挂起状态
    • 4.3 Linux中的进程状态
      • 4.3.1 R状态
      • 4.3.2 S状态
      • 4.3.3 D状态
      • 4.3.4 T状态
      • 4.3.5 t 状态
      • 4.3.6 x状态和Z状态
  • 5. 进程调度
    • 5.1 进程优先级
    • 5.2 进程切换
      • 5.2.1 进程相关概念补充
      • 5.2.2 进程切换
    • 5.3 进程调度
    • 5.4 分时操作系统与实时操作系统
  • 6. 命令行参数
    • 6.1 命令行参数如何获取
    • 6.2 命令行参数的意义
  • 7. 环境变量
    • 7.1 认识环境变量
    • 7.2 多方法获取环境变量
      • 7.2.1 main函数参数
      • 7.2.2 通过函数获取单个环境变量
      • 7.2.3 使用外部变量environ
      • 7.2.4 环境变量获取的本质
    • 7.3 环境变量的用途
    • 7.4 环境变量的属性
      • 7.4.1 全局属性
      • 7.4.2 内建命令

1. 冯诺依曼体系结构

当代计算机的体系架构普遍采用冯诺依曼体系结构

1.1 什么是冯诺依曼体系结构

在这里插入图片描述
冯诺依曼体系结构将计算机分为四大块:输入设备、存储器、输出设备以及中央处理器(即CPU,包括运算器和控制器)。

常用的输入设备有:鼠标、键盘、麦克风、硬盘等。
常见的输出设备有:屏幕、硬盘、打印机、音响等。
我们可以发现,有单纯的输入和输出设备,也有既是输入设备又是输出设备的,比如硬盘。

上图中的存储器实质是指计算机内存,像硬盘和U盘这类的存储器,不是内存,而是外存。

CPU分为运算器和控制器,运算器是用于进行算术和逻辑运算的,控制器则是用于进行逻辑控制。

在上图中,我们注意观察红色箭头代表的数据信号,我们可以发现,CPU仅直接与内存之间发生数据交流,而与外设(即输入输出设备)之间没有直接的数据交流。

因此,我们完全可以说,内存实质上是外设与CPU之间的一个缓存。而在冯诺依曼体系结构下,计算机的效率主要取决于内存。这是因为,在计算机硬件中,有这么一个规律,越快的数据传输或处理速度,意味着更高昂的造价,因此具备存储功能的硬件,通常都是速度效率与容量成反比的。比如,在容量上,CPU中的寄存器< 内存 < 硬盘;但在效率上,CPU > 内存 > 硬盘。而由于,内存直接与CPU进行数据交互,根据木桶的短板理论,实际CPU的运行速度要受到内存数据传输速度的限制,因此计算机的运行效率主要取决于内存。

1.2 为什么选择冯诺依曼结构

冯诺依曼结构的最大特点就在于引入内存充当外设与CPU之间的一个“缓存”。

试想,如果没有内存,而是外设直接与CPU进行数据交互,那么CPU的效率就会受到外设的直接限制,而外设由于较大的容量,必将导致较低的效率,这样就限制了高性能、低成本计算机的发展。在这样的情况下,想要提高效率,只有扩充CPU的容量,或者提高外设的效率,但CPU的高运算速度,以及外设的大容量,使得这样的改进成本极高。而且容量的增大,通常都会带来访问效率的降低,是很难在一个大容量的前提下,同时保证极高的访问效率的。

而冯诺依曼结构中内存的引入,就很好地解决了这个问题。内存充当外设与CPU之间的缓存,既提高了计算机的性能,又合理地限制了计算机的成本,这才使得计算机逐步走进千家万户,带来了互联网时代。

2. 操作系统

2.1 操作系统是什么

简单来说,操作系统是一款进行软硬件资源管理的软件。 实质上,我们使用计算机,就是通过软件去访问使用计算机的硬件资源,而硬件资源是有限的,因此需要把硬件资源管理好,因此就有了操作系统。所以,从这个角度来说,操作系统通过把软硬件资源管理好,进而会计算机用户提供一个安全、高效和稳定的使用环境。

操作系统有狭义与广义之分。狭义上的操作系统仅仅表示操作系统的kernel内核,如Linux内核,而内核主要进行线程管理,文件管理,内存管理和驱动管理;广义上的操作系统,包括内核的同时,还包括shell、原生库、一些预装的系统级软件和图形化界面等。

在这里插入图片描述

2.2 操作系统如何对硬件资源进行管理

操作系统本质也是软件,因此它也是用计算机语言编写而成的。具体来说,操作系统是用C语言构写的。

我们要想对硬件进行管理,首先就得对硬件的各种状态进行抽象描述,而操作系统中使用struct结构体来对硬件的各个属性进行描述。显卡、网卡、硬盘等等硬件都分别对应有一个结构体对象。操作系统驱动获取相应的硬件数据,然后再将这些数据存储到相应的结构体对象中,从而完成描述工作。

但仅仅是描述,并不能将硬件资源管理好,因为这就相当于获取了一堆数据,但是不将这些数据组织起来,是没有办法管理好这些数据的。因此,在操作系统中,还要将这一个个代表硬件的结构体对象,用一定的数据结构组织起来,这样操作系统对实际硬件的管理就变为了对相应数据结构的管理。所以,操作系统内部,一定是含有大量的结构体和数据结构的。

2.3 计算机的层状体系结构

计算机整体的软硬件体系结构是层状的。

在这里插入图片描述

计算机使用操作系统进行软硬件资源的管理,对于计算机的用户而言,是不允许直接通过操作系统去访问硬件资源的,要想访问硬件资源,必须使用相关的系统调用接口与操作系统进行交互。

但是系统调用接口的使用是高门槛的,为了计算机的普及,因此又提供了相关的用户操作接口,比如我们所熟知的C/C++标准库实质上就是用户操作接口。这样,就降低了计算机的使用门槛——只要学会命令行中的相关指令,懂得一门计算机语言和相关库的调用,就可以使用计算机,而不用去了解相关的系统调用接口。

但是,上述的用户,实质上针对的是计算机程序员等二次开发者,需要相关的计算机专业知识,但是不懂计算机的人并不会这些。因此,为了让不懂计算机语言和指令的人也能使用计算机,于是在用户操作接口上再封装一层图形化用户界面(GUI),而后就形成了我们现在普遍使用windows或mac的图形化界面。

所以,总的来说,计算机在用户层面的层层封装,上层调用下层,保证了用户对计算机安全与便捷的使用。

3. 进程

3.1 进程是什么

什么是进程呢?简单来说,运行起来,加载到内存中的可执行程序被称为进程。
在计算机运行过程中,内存中的进程数目是很多的,操作系统要对这些进程进行管理,与对硬件的管理相同,遵循先描述,再组织。

计算机中使用PCB(Process Control Block)对进程进行描述。PCB实质上就是一个结构体,里面包含进程的相关属性,以及相关代码和数据的地址等。实质上,内存中的进程:进程 = PCB + 相应的代码和数据。

但是在Linux中,相应描述进程的结构体并不叫PCB,而是名为task_struct。

已经有了对进程相应的描述,那么该如何组织呢?Linux中,使用双链表将各个task_struct对象串在一起进行管理——这样,操作系统对进程的管理,就转化成为对相应双链表的管理。

在这里插入图片描述

3.2 进程的相关属性

下面对进程的相关属性进行一个简单的介绍。

标识符:又称PID,每一个进程都有其唯一标识符,用以区别其它进程。
优先级:一个进程相较于其它进程的优先级,决定了进程执行的先后顺序。
程序计数器:英文简写为PC,存放即将要执行的下一条指令的地址。
内存指针:指向程序代码和进程相关数据的指针。
进程上下文:又称为进程的硬件上下文,是进程执行时,存储在CPU寄存器中的临时数据。

下面综合程序计数器,指令寄存器和进程上下文这三者,来简单解释一下内存中的进程是如何到CPU中运行的。

在这里插入图片描述
内存指针PC:存放即将执行的下一条指令的地址。
指令寄存器IR:存放当前正在执行的指令的地址。
一些寄存器:存放该进程相关指令被执行时,相关的一些临时数据,这些临时数据就是进程的硬件上下文。(包括PC和IR中的数据)

由上图可知,CPU执行内存中的进程,本质上就是一条条地执行该进程的相关指令,并在另外一些寄存器中存储指令执行时的相关数据。

简单了解进程是如何在CPU中执行后,我们来思考一个问题:一个进程可能被无限度地执行吗?显然,一个进程是不被允许无限度执行的,进程是需要被切换的,因此这又引出一个新的概念——进程的调度和切换。

进程的调度和切换体现出进程的动态属性。毫无疑问,当CPU中正在运行的一个进程被切换为另一个进程时,我们是需要此时CPU寄存器中的临时数据进行保存的,这样当进程再次被切换回时,可以从之前终止处继续往下执行,这就是进程硬件上下文的保存和恢复。在较老的Linux内核版本中,进程的硬件上下文是直接保存在相应的task_struct对象中的,但在之后的Liunx内核版本中,并未采取这样的做法。

3.3 在Linux中了解进程

3.3.1 查看进程

在Linux中,所有的进程都被放在根目录下的proc目录中,由于Linux中,一切皆文件,所以,每一个进程实质以目录的形式呈现。

在这里插入图片描述
上图中的各个数字,即对应进程的pid。

接下来,我们自己写一个程序用以执行,并对该进程进行观察。

在这里插入图片描述
上图中的getpid函数是用于获取相应进程的pid。

将该程序跑起来之后,我们通过ls /proc命令查看是否出现相应的pid。

在这里插入图片描述

在这里插入图片描述

既然进程在这里体现为一个目录,我们可以去查看这个目录中的具体内容。

在这里插入图片描述
在这里插入图片描述
这个目录中的内容很多,我们现在主要看两个:cwd exe
exe表示,当前进程所对应的正在运行的可执行文件是什么;cwd表示的是当前进程的工作目录,这个工作目录可以修改,默认为相应的可执行文件所在的目录。

我们还可以通过ps ajx | head -1 && ps ajx | grep "myporcess"命令进一步查看进程相关的信息。

在这里插入图片描述
其中,./myprocess即对应我们运行起来的二进制程序,但是我们注意到还有一个grep,这是为什么?
本质上,Linux命令行中的各个命令都是/usr/bin目录中的二进制文件,执行相应的命令,实质就是运行相应的二进制程序,因此也会形成相应的进程。

3.3.2 子进程由父进程创建

在上图第一行的进程相关的属性栏中,我们发现,除了PID,还有一个PPID,这又是什么?

在Linux中,一个进程是由其父进程产生的,PPID即为该进程的父进程。

那么,父进程是如何创建子进程的呢?父进程通过一个系统调用接口fork来创建子进程。
这里顺带讲一下fork函数的返回值:如果子进程创建成功,在父进程中返回相应子进程的pid,子进程中返回0;如果子进程创建失败,在原本进程中返回-1。

接下来,我们就在程序中,主动使用fork,来创建子进程。

在这里插入图片描述
在程序运行起来之后,我们来查看进程的相关信息。

在这里插入图片描述
在这里插入图片描述
通过上述例子,我们可以看到,确实是由父进程来创建子进程。
我们还可以发现,./myprocess也有一个父进程,其pid为14006,这又是什么呢?
我们进一步对其查看发现,实质上14006对应的进程即为-bash,即命令行解释器。

在这里插入图片描述
也就是说,Linux中,执行命令行中相应指令所形成的进程,都是由命令行解释器,即shell外壳生成的。

最后,我们来讲讲上述代码创建子进程后,为什么会有那样的运行结果。
本质上,父进程在创建子进程后,就会有两个执行流,一个是父进程的执行流,一个是子进程的执行流,这两个执行流在fork函数创建子进程的核心功能完成后,便会共享之后(包括主函数中的fork之后)的所有代码和数据(如果数据不被修改的话),因此就有了上述的输出结果。

所以,我们可以发现,在多进程的程序中,一个变量可以有两个值,一个函数可以有两个返回值,if-else语句中的 if-else if-else三者可以同时成立并执行。

4. 进程状态

什么是进程状态?简单来说,一个进程处在怎样的状态,就决定了这个进程接下来要做什么工作。

为什么要设计各种各样的进程状态呢?我觉得其中一个原因是:资源是有限的。对于计算机而言,资源分为两类:CPU资源和I/O资源。内存中的进程有很多,而CPU资源与I/O资源是有限的,所以必然存在进程竞争资源,而为了更好地进行资源分配,就需要对进程状态进行划分。

在操作系统相关的教科书上,应该都有这样一幅关于进程状态的图:

在这里插入图片描述
实际上,在具体的操作系统设计时,进程的状态不一定完全符合上图,更准确地说,上图仅为设计进程状态的指导思想,而并非实际的落地方案。

4.1 Linux中进程的双链表管理方式

在讲具体的进程状态前,我们要先弄清楚Linux中是如何进行双链表管理的。

与我们以往所认知的双链表不同,不同进程之间的链接,并不是直接在task_struct这个结构体中添加task_struct* next 和 task_struct* prev这两个指针,而是添入了一个list_node node这个成员变量,其中list_node是一个结构体,其中包含两个成员:list_node* next,list_node* prev

这样,不同进程的task_struct之间,就通过这个list_node串在一起,构成了双链表,如下所示:

在这里插入图片描述
这样管理双链表有何好处呢?好处在于,一个双链表的结点,即一个进程,可以同时链入多个链表中,而不用破坏任何一个链表的结构,只需要在task_struct增添list_node类型的成员即可。

除此之外,还有一个问题:不同的task_struct之间,通过list_node链接在一起,我们在不同的task_struct间切换时,实质上是在list_node间切换,那么我们该如何拿到task_struct中的其它数据呢?

实质上,我们有list_node的地址,同时知道相应结构体的类型,即task_struct,我们就可以拿到task_struct的地址,具体代码是:task_struct* ptr = (task_struct*)(&node - &((task_struct*)0 -> node))。这样,我们就拿到了相应进程task_struct的地址,也就可以任意访问其中的成员了。

4. 2 进程具体状态

4.2.1 新建状态

我们知道,操作系统在内存中使用双链表来管理所有进程,单纯处在这个全局双链表中的进程就为新建状态(创建状态).

4.2.1 就绪状态和运行状态

前面讲过,不同进程之间会竞争CPU资源,那么对于CPU而言,如何调度这些进程,显然需要一种调度算法进行调度,比如说用一个FIFO结构的调度队列(当然,这种调度算法过于简单,一般是不会采用的)。

一个进程如果正在被CPU调度,那么这个进程就处在运行状态;一个进程已经准备就绪,即将被CPU调度,那么就处在就绪状态。但是,有时并不对二者做区分,而都称为运行状态。

4.2.2 阻塞状态

前面讲过,不同进程之间除了竞争CPU资源,还会竞争I/O资源,即相关外设硬件资源。但是一个硬件不可能任何时候都是就绪的,比如说,一个可执行程序要从键盘中读取数据,那么这个进程首先处在运行状态,然后由于要从键盘中读取数据,所以操作系统会将这个进程从CPU的调度队列中拿出,转而放入键盘的相关等待队列中来获取键盘资源,此时进程的状态就为阻塞状态。

如果键盘中一直不输入数据,那么该进程就会阻塞,即一直处在键盘资源的等待队列中,而无法回到CPU的调度队列中。

4.2.3 挂起状态

挂起状态是指在内存中,一个进程只有相关PCB结构,而没有相应的代码和数据(放至硬盘上的swap分区中),此时该进程就处在挂起状态。

挂起状态分为就绪挂起和阻塞挂起,但不管是哪种挂起状态,其本质上都是因为当内存资源不足时,操作系统主动将一些不是正在运行的进程的代码和数据从内存中拿出,换至硬盘上的swap分区中,进而缓解内存资源紧张。
如果一个进程处在阻塞状态,其代码和数据被挪出,那么就称为阻塞挂起状态;如果一个进程处在就绪状态,其代码和数据被挪出,那么就成为就绪挂起状态。

4.3 Linux中的进程状态

在Linux内核源码中,进程状态被定义如下:

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 */
}

4.3.1 R状态

在Linux中,没有就绪状态,统一为R,即运行状态。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在程序运行起来,进入死循环后,我们可以发现,相关进程即为R运行状态。(这里对R后的+稍作解释,有+,表示该进程是前台进程,是可以用ctrl + c直接强行干掉的;没有+,表示是后台进程,是无法用ctrl + c直接干掉的)

4.3.2 S状态

S状态,是休眠状态,准确来说,是浅度休眠状态,或可中断睡眠状态隶属于之前所讲的阻塞状态

当一个进程在进行硬件资源的等待时,就会进入S状态。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
上述进程在等待键盘资源时,经过查看,处于S休眠状态。

4.3.3 D状态

D状态,为磁盘休眠状态,或者称为深度休眠状态,或不可中断睡眠状态,这种状态同样属于阻塞状态。

D状态与上述的S状态有何区别呢?
处在S状态下的进程会对外界事件进行响应,可以被ctrl + c直接干掉,同时,在内存资源严重不足时,整个进程可以被操作系统直接干掉。

处在D状态下的进程不会对外界事件进行相应,在内存资源严重不足时,也不允许操作系统将该进程干掉。处在这个状态下的进程,除非进程自身主动脱离该状态,否则只有关闭或重启计算机,方能除去该进程。

那么,什么情况下,进程会进入D状态呢?一般的,一个进程在进行磁盘级I/O时,为了防止数据被丢失,这个进程就会进入D状态,不对外界事件作任何相应,只有当磁盘级I/O结束时,进程方会脱离D状态。

4.3.4 T状态

T状态,即停止状态。当一个进程进入停止状态时,便不再运行了。

我们可以使用kill命令来使一个进程进入停止状态。

在这里插入图片描述
使用kill -l对相关kill指令的编号进行查看。
注意,9编号用以杀死进程,19编号用以停止进程,18编号用以使停止进程继续运行。

在这里插入图片描述
现在myfork这个进程正处于运行状态,我们使用kill +进程PID -19,来暂停这个进程。

在这里插入图片描述
在这里插入图片描述
不过有一点需要注意,使用kill暂停这个进程后,会使得进程变为后台进程,如上所示。

我们还可以进一步使用kill命令,将这个暂停的后台进程变为正在运行的后台进程。

在这里插入图片描述

4.3.5 t 状态

t 状态,又称为追溯停止状态

我们可以通过gdb调试加上断点来实现这个状态

在这里插入图片描述

在这里插入图片描述

本质上,gdb调试一个可执行程序就是在创建一个进程,而在gdb调试中,让被调试程序运行起来,本质上就是gdb再创建了一个子进程,然后让这个子进程去执行相应的代码。而由于在gdb调试中添加了断点,子进程执行到断点处便无法继续往下执行,此时该子进程便进入 t 状态。

4.3.6 x状态和Z状态

进程的创建是把相应的task_struct和代码数据加载到内存中的过程,进程的结束,则是进程的创建的反过程,即内存释放相应资源的过程。

x状态,即代表一个进程彻底结束时的状态,但是一个进程结束时,并不能立刻进入x状态,需要先进入Z状态,Z即Zombie,处于该状态的进程,又被我们称为僵尸进程

要弄清除为什么要有僵尸进程,我们首先要明确创建进程的作用。创建一个进程,本质上是为了完成相应的任务,而进程都是由另一个进程创建的,那子进程执行任务结束了,需不需要向父进程汇报任务完成的情况呢?毫无疑问,是绝对需要的。
因此,一个进程结束,不能立刻标记这个进程死亡,即进入x状态,然后将其相应资源全部释放,而是先进入僵尸状态,等待父进程获取子进程的任务完成情况后,再进入x状态,释放资源。

需要说明的是,由于进程执行的情况依旧是保存在相应的task_struct中,不同的数字标明不同的状态。因此,进程进入僵尸状态时,会将相应的代码数据释放掉,但是保留内存中的task_struct,父进程要从子进程的task_struct中,获取退出码。

正常情况下,僵尸进程由父进程读取退出码后,便进入x状态,彻底释放资源;但是,如果一个僵尸进程的父进程不对该进程做任何处理,那么这个子进程就会一直处在僵尸进程的状态,这就意味着,这个进程的task_struct会一直在内存中,进而引起内存泄漏。

下面我们在Linux下,来模拟实现一个僵尸进程。

在这里插入图片描述
将进程执行起来之后,查看进程状态。

在这里插入图片描述
可以看到,PID为15507的子进程处于Z状态,为僵尸进程。处在僵尸状态下的进程,是无法直接被kill命令杀死的。

在上述程序中,子进程结束后,父进程的代码为一个死循环,一直无法结束。而我们所创建的父进程,是不会主动读取子进程的退出码的(如果要主动读取退出码,可以在父进程的代码中使用wait函数),因此子进程就进入了僵尸状态。但在父进程正常结束后,会由操作系统调用wait来释放子进程中的资源。


上面的情况是父进程晚于子进程结束,但如果父进程早于子进程结束,又会出现什么情况呢?

如果一个父进程早于子进程结束,即父进程结束后,子进程仍然在运行,此时我们称这个子进程为孤儿进程

一个子进程成为孤儿进程后,会被1号进程领养,即1号进程成为该子进程的新父进程。

下面我们来模拟实现孤儿进程。
在这里插入图片描述

在这里插入图片描述
我们可以看到,在父进程结束后,相应的子进程便被1号进程领养,同时该子进程会转变为后台进程。

那1号进程又是谁呢?我们可以使用top命令,在Linux下查看。

在这里插入图片描述
1号进程在Linux中本质上就是一个systemd或init命令,我们可以将其理解为操作系统的一部分

可是,为什么一个孤儿进程要被领养呢?因为孤儿进程也需要被读取退出码,也需要释放内存中的相应资源。1号进程领养该进程,是为了在该子进程结束后,能够彻底回收该子进程的相关资源,避免内存泄漏。

上面所讲的孤儿进程情况,是父进程先于子进程结束,父进程的这个结束,可以是正常结束,也可以是意外终止,比如使用kill命令杀掉进程。

5. 进程调度

5.1 进程优先级

什么是进程的优先级呢?进程的优先级,即进程得到某资源的先后顺序,这个资源可以是CPU资源,也可以是I/O资源,但讲进程的优先级时,一般都是指获得CPU资源。

为什么进程要有优先级呢?本质上是因为资源有限,资源少,进程多。比如说,总共只有1个CPU,却有100个进程,任意时刻,1个CPU只能由一个进程使用,那么,肯定就要确定这些进程被CPU调度的顺序,即进程的优先级

那么在Linux中,是如何确定一个进程的优先级呢?

首先,我们要明确进程的优先级实际体现为task_struct中的一个数字,这个数字越小,代表进程的优先级越高,也就越先获得资源。

在Linux下,我们使用ps -l来查看当前终端会话中的进程信息。

在这里插入图片描述
UID:表示该进程的执行者身份,即启动该进程的用户。
PRI:代表该进程的优先级。
NI:表示该进程的nice值。

实质上,在Linux中,优先级的确定并不是由单一变量决定的,实际的计算公式为:PRI = 80 + nice。也就是说,在Linux下,固定了一个80的基准值,通过改变nice值进而改变优先级。nice值的取值范围为[-20,19],相应地,优先级的范围为[60,99],共40个优先级。

在Linux中,我们可以使用top命令来更改一个进程的nice值。一般情况下,降低一个进程的优先级,即调高nice值是被允许的;但是,提高一个进程的优先级,即调低nice值,普通用户是无法实现的,需要超级用户的权限。

在这里插入图片描述
使用top,调出如下窗口。
在这里插入图片描述

在这里插入图片描述
按下r,然后输入要更改nice值的进程的PID,再输入相应的nice值即可完成更改。

在这里插入图片描述
注意,nice的范围在[-20,19]之间,低于-20的值,会置为-20;高于19的值,会置为19。

下面解释一下关于nice值的两个问题。

为什么nice值要有范围?
nice的范围限定,实质上是限定了用户对优先级的调整,无论是超级用户,还是普通用户,都只能在一个优先级的范围内调整优先级,而不能任意调整优先级。这实际上是对系统的保护,如果用户能够任意调整进程的优先级,就可能导致一个资源被一个并不重要的进程长久占用,而其它重要的进程迟迟无法使用相应资源,造成进程饥饿问题**,严重时,甚至可能导致系统崩溃。**

为什么nice值的范围是[-20,19]?
这个问题,我们先抛出来,这个范围的确定与进程的调度算法有密切关系,等到讲进程调度算法时,再来解答。

5.2 进程切换

5.2.1 进程相关概念补充

竞争性:进程之间具有竞争性,其具体表现为进程的优先级。

独立性:进程之间具有独立性,一个进程的状态如何,并不影响另一个进程的状态。

并行:并行是指,多个进程在多个CPU中,分别且同时地运行。

并发:并发是指,任意一个时刻,一个CPU中只能运行一个进程,但是多个进程在一段时间内,在同一个CPU上进行进程切换,那么在这段时间内,这多个进程都得以同时推进。

5.2.2 进程切换

我们前面提到,CPU内有一套寄存器,用以临时存储在CPU上运行的进程的硬件上下文。进程切换,实际上就是CPU寄存器中进程的硬件上下文的切换。

当代的计算机系统,基本都是分时操作系统,即每个进程都有自己的时间片,这个时间片决定了该进程在操作系统上能够运行的时间,时间一到,就要将该进程从CPU上剥离,进而切换其它进程在CPU上运行。

这个剥离和切换,就是将时间片用完的进程的硬件上下文进行存储(在老的Linux内核版本中,是存储在task_struct中的tss_struct,即任务状态段这个结构体中的),然后将CPU寄存器中存储的数据更新为下一个要运行进程的硬件上下文,这样就完成了进程的剥离和切换。

注意,剥离进程的硬件上下文一定要进行存储,这样当再次轮到该进程被CPU调用时,能够进行硬件上下文的恢复,以便从上次中断处,继续向后运行该进程。

下图所示,即为在较老的Linux内核版本中的,tss_struct 的结构。

在这里插入图片描述

5.3 进程调度

在上面的介绍中,我们知道分时操作系统,每个进程都有其相应的时间片,时间片的时间用完了,就要进行进程切换,可是,在实际中,我们如何知道切换到哪个进程呢?这就涉及到进程调度的问题了。

那么在Linux中,进程是如何调度的呢?

每一个CPU,都有一个对应的runqueue结构,用以确定进程的调度顺序。

在这里插入图片描述

  • 在这个结构体中,有关于CPU要执行进程数量的负载因子,这个负载因子主要用于多CPU的情况,因为多个CPU要考虑进程的分配均衡问题。
  • 与进程调度直接相关的是图中的queue[140],其中存储值的类型为struct list_node*,也就是说,通过这个数组,可以拿到相应进程的task_struct,进而用以进程调度。这个数组共140个元素,下标为0~139,我们先不管0-99编号,而关注100-139。100-139刚好是40个编号,正好对应上文所讲的nice值跨度为40的范围。通过调整nice值,进而可调整进程优先级在**[60,99]**之间,然后再加上40,刚好是100-139,与数组的下标相对应。这样,进程的优先级就由其所处数组的位置来确定了。

进程进行调度时,就在queue中首先查找非空指针,找到非空指针后,按照其所指向的双链表顺序,依次调用相关进程。

但是,如果在queue中顺序遍历查找非空指针,效率就太低了,因此又引入了int bitmap[5]这个结构。这是位图的数据结构,一个int有32个比特位,5个int,共有160个比特位,而我们只需要用到其中140个即可。按照比特位从低到高,分别对应数组的下标从0到139。比特位为0,代表下应的数组下标处为空;比特位为1,代表不为空。通过位运算的相关操作,我们可以用O(1)的时间复杂度,快速查到queue中的非空指针后进行进程调度,而这就是进程调度的O(1)算法。

但是,我们在上图中发现,有两个queue[140],这是怎么回事?这就涉及到活跃进程和过期进程的区分。

实质上,runqueue中所含的成员是struct q arr[2],而struct q中就含有上图所标出的三个变量nr_active(该变量用于记录当前队列中可运行的进程数,用于计算负载因子),bitmap[5],queue[140],而这样的结构共有两个,一个对应过期进程,一个对应活跃进程。

似乎一个这样的结构就能实现进程的调度,那Linux下,为什么要给两个这样的结构呢?

我们先不着急解释这个问题,先来弄明白这两个结构是如何运作的。

在CPU进行进程调度时,会首先调用活跃进程。在活跃进程调度期间,新增的进程或时间片用完的活跃进程会被放入到过期队列中,变为过期进程。而当活跃队列中进程调度完毕后,Linux中的操作设计得很巧妙。

在这里插入图片描述
上图中,是两个指针,从变量名的命名上,一个是活跃,一个是过期,显然这两个指针就分别指向之前所讲的两个结构体struct q。在活跃进程调度完毕后,接着要调用过期进程,在此之前先swap(active,expired),
使得原本的过期队列与原本的活跃队列完成交换,然后继续重复上述过程,周而复始。

这样,保证了CPU调度的进程,始终是active指针所指向内容中的活跃进程;而新增进程或时间片用完的活跃进程始终被放到expired指针所指向内容中的过期队列中。

现在,我们可以理解为什么要有两个队列了。如果只有一个队列,那么在调用进程的过程中,对于新增进程或时间片用完的进程,会频繁在进程调度的过程中插队致使较低优先级的进程始终无法得到相应资源,进而引发进程饥饿问题,影响系统或相关任务的正常运行。

而使用两个队列,新增的进程或时间片用完的进程,始终是放到过期队列中的,而过期进程只有当当前活跃进程全部运行完后,才能转为活跃进程,进行运行,这样就避免了进程的频繁插队问题,从而避免了进程饥饿

所以,从宏观上看,在这样的设计结构下,对于单个队列内部,优先级会影响获得资源顺序;而在两个队列之间,永远是先活跃进程,后过期进程,优先级并不会影响获得资源顺序。

5.4 分时操作系统与实时操作系统

操作系统有分时与实时之分,在当代计算机中,操作系统主要是分时操作系统;而在工业领域中,实时操作系统却应用甚多。

此处对于这两种操作系统不作详细介绍,简而言之,分时操作系统是基于时间片轮转来调度进程的;实时操作系统是基于优先级来调度进程的。

而Linux操作系统兼具实时和分时两种功能,上述用于进程调度的queue[140],100-139用于分时操作系统,而0-99则专门用于实时操作系统。

6. 命令行参数

对于Linux下的命令行命令,我们之前是理解为:具体命令加上命令选项。现在,我们来重新理解一下命令行命令。

首先,我们来思考一个问题:main函数可以有参数吗?
main函数是可以有参数的,但是一般情况下,main函数的参数,我们都将其省略不写。
int main(int argc,char* argv[])
main函数的这两个参数,argc表示argv数组的大小,argv数组则是存储字符串。

我们看如下代码和其运行结果。

在这里插入图片描述
在这里插入图片描述
最终程序输出得到的结果是./test,正好是之前输入的命令,这是巧合吗?

实际上,命令行中的所有命令都是命令行参数,而命令行参数本质就是字符串,这些字符串可以被main函数获取,进而实现一定的功能。

那么,这些命令行参数是如何获取的,而这些命令行参数又有什么作用呢?

6.1 命令行参数如何获取

首先,我们一旦远程连接到相应的云服务器上,命令行解释器bash这个进程就会被运行起来,并且始终处在运行状态。
我们输入的命令,也就是命令行参数,即会在相应的显示器文件中,同时也会被bash获取,进而在内存中形成命令行参数表
而我们又知道,命令行中的命令本质上都是/usr/bin这个路径下的可执行文件,任何命令的运行本质上就是一个进程,一个由bash父进程创建的子进程。

所以,对于父进程bash的main函数,其相应的命令行参数是从用户输入中直接获得的,并且会在内存中创建命令行参数表,这是父进程的数据;而相应的命令本质上都是由bash父进程创建的子进程,而在不修改的情况下,子进程会共享父进程的代码和数据,这就意味着,父进程bash获得了命令行参数,相应的子进程也就获得了相应的命令行参数。

6.2 命令行参数的意义

我们来看如下代码和其运行情况。

在这里插入图片描述
在这里插入图片描述
上述程序和运行结果在一定程度上揭示了Linux中的命令以及各个命令运行是如何运行的。

首先,我们要明确char* argv[]中的这些字符串是怎样的。对于输入的命令,以空格为间隔,以从左到右为数组下标增长顺序,空格前后各为一个字符串,这就是argv[ ]中的内容,argv[ ]以空指针作为最后一个元素(不计算在argc内,argc表示argc中有效字符串的个数)。

考虑到我们书写命令的形式,例如ls -l,一般是先写对应命令,再明确相应选项。
而bash进程中的main函数,对其获得的命令行参数,即argv[ ]中的内容会进行判断:首先判断用户输入的是什么命令,根据具体的命令到相应的分支中,创建相应的子进程,并执行相应的功能;而在相应的子进程中,又会根据具体的命令选项,继续条件分支,进而执行不同的功能。

所以,我们所输入的命令,本质上都是命令行参数,本质上都是字符串,可以被相关进程的main函数获取,进而在main函数内部,根据字符串的大小以及不同内容,进行条件分支,进而实现不同的功能。

7. 环境变量

7.1 认识环境变量

环境变量是系统上的一些全局变量,不同的环境变量具备不同的用途。

那到底什么是环境变量呢?
我们知道,所有的命令都是/usr/bin目录下的可执行文件,但是正常情况下,我们运行一个可执行文件,都要给出这个可执行文件的具体路径,例如我们在自己家目录下编译生成的可执行文件,运行前,都要加上./,表示该文件在家目录下,但是,我们使用其它linux中的命令时,并没有指明路径,命令却能正常运行,这是为什么?

在这里插入图片描述
在环境变量中,有名为PATH的环境变量。

在这里插入图片描述
我们观察其中的各路径(不同路径之间使用冒号分隔),发现其中有一个路径**/usr/bin**,恰好就是我们linux中的各命令对应的可执行文件所在的路径。

所以,linux下的命令,如果不指定具体路径的话,默认都是到PATH这个环境变量中的各个路径中依次去找,找到了,就运行相应的功能;找不到,就什么都不执行。

这也就是为什么linux中的内置命令可以不指定相应路径直接运行,而自身在非PATH路径下创建的可执行文件,一定要指定具体路径。

但是,如果我们作如下改变,将家目录的路径添加到PATH中,那么我们在家目录下创建的可执行文件,就可以不用指定路径。

在这里插入图片描述
小提示,$PATH是指获取PATH的具体内容。

7.2 多方法获取环境变量

在Linux中,我们可以使用多种方法获取环境变量。

7.2.1 main函数参数

main函数中还有一个参数,可以用以获取环境变量。
int main(int argc,char* argv[],char* env[])

我们看如下代码及其运行结果。
在这里插入图片描述

在这里插入图片描述
上图中密密麻麻的这些信息,便是获取到的当前系统中的环境变量。 但是,这种方法,会获取到所有的环境变量,有没有方法能够只获取想要的单个环境变量呢?

7.2.2 通过函数获取单个环境变量

在这里插入图片描述
使用getenv函数,来获取单个环境变量,形参为相应环境变量的名称,返回值为环境变量的具体内容。

在这里插入图片描述
在这里插入图片描述

7.2.3 使用外部变量environ

在这里插入图片描述
该外部变量的使用方式与main函数参数env的使用方式相同,实质上,二者在值上是完全相同的,一般都用于查看所有环境变量。

7.2.4 环境变量获取的本质

同命令行参数一样,bash进程在内存中创建一张环境变量表,此表是bash进程的数据,因此bash进程创建的子进程会继承这张环境变量表,因此可以获得环境变量。

那么,父进程bash环境变量表中的环境变量又从哪里来呢?

先说结论:环境变量表中的环境变量,主要从系统的配置文件中来。

我们来看一下系统中的配置文件。

在这里插入图片描述
重点关注其中的.bash_profile和.bashrc这两个文件。

我们来看一下bash_profile中的内容。

在这里插入图片描述
可以看到,在这个系统配置文件中,恰好有一个PATH环境变量。实质上,当bash进程启动后,bash进程会自动运行该文件中的内容,从而获取相应的环境变量,并放入相应的环境变量表中。

现在,我们可以来回答一个问题。为什么,我们手动修改PATH路径后,只要重新启动xhell,PATH路径会自动复原。

这是因为,我们使用 PATH= 手动修改PATH路径,本质上是修改环境变量表中的内容,而bash的环境变量是内存级的,是在bash进程创建后,加载到内存当中的,但是bash_profile这个文件中的内容并没有改变。因此,关掉xshell,再重新登陆后,bash进程被重新创建,因此便会重新运行bash_profile中的内容,而只要在这个文件中,PATH的内容没有被改变,重启bash进程后,PATH路径就会复原。

下面,我们来验证一下在bash进程启动后,会自动运行bash_profile中的内容。

在这里插入图片描述

在这里插入图片描述

7.3 环境变量的用途

不同的环境变量有不同的用途,一下举几个例子帮助大家理解。

pwd:
pwd指令的作用是打印当前的工作目录,但是pwd指令是如何知道当前的工作目录呢?

在这里插入图片描述
在环境变量中,有两个环境变量,一个是PWD,一个是OLDPWD。显然,前者用于记录当前工作目录,后者由于记录之前一次的工作目录。pwd指令的实质功能,其实就是PWD这个环境变量的内容给打印了出来。

而我们之前在学习linux指令的时候,提到过cd指令的两种快捷操作,一个是cd -用于切换到前一个目录,另一个是cd ~,用于切换到家目录。现在,我们明白,cd - 实际就是 cd $OLDPWDcd ~ 实际就是 cd $HOME,本质上都使用了环境变量。

另外,如果我们想要实现一个只能由某个用户运行的程序,我们也可以借助环境变量USER

在这里插入图片描述

首先,我们在用户名为wnf的账号下,运行该程序,得到结果如下:

在这里插入图片描述

然后,我们切换到超级用户root中,运行该程序(注意,切换用户的过程中,要么重新使用root账号登陆,要么使用su - root命令,直接su root,很多环境变量无法跟随改变,包括USER)

在这里插入图片描述

7.4 环境变量的属性

7.4.1 全局属性

之前在定义的时候说过,环境变量是系统级别的全局变量,因此,环境变量具有全局属性

理解环境变量的全局属性,其实就是理解linux下全局变量和本地变量的区别。

Linux中,本地变量只在当前会话的bash进程中有效,在bash进程创建的子进程或是其他会话的bash进程中是无效的。

我们在当前bash进程中,创建一个本地变量。
在这里插入图片描述
这样创建的本地变量,不是环境变量,只能被bash识别,而不会被bash的子进程继承。

我们再使用export命令创建一个环境变量。

在这里插入图片描述
这样,就成功将haha添入了环境变量表中,而环境变量表本质上是父进程bash的数据,而这个数据是可以被bash进程创建的子进程继承的。

7.4.2 内建命令

但是,我们同样会发现一个非常奇怪的地方。

在这里插入图片描述
在上图中,我们明明将hello创建为一个本地变量,但是echo指令却能够打印出这个全局变量的具体内容,不是说本地变量只在bash中有效,不能被bash的子进程识别吗?

这就涉及到linux中内建命令的知识。
在linux中,有一部分命令是由bash进程直接执行的,而不是由bash进程创建的子进程完成的,而这样的命令,我们就称之为内建命令,而echo就是内建命令,因此上图中的现象就说得通了。

但是,我们查看echo命令的路径时,发现这样的情况,存储指令的目录中,也有这么一个echo,这是为什么?
在这里插入图片描述
本质上,我们在命令行中使用echo时,都会被bash识别为内建命令,并不会用到上述路径下的这个可执行文件,这个可执行文件是在别的场景下使用的。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com