作者前言
🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂
🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言、C++和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂
Linux进程
冯诺依曼体系结构

cpu:运算器和控制器组成
内存储器:内存
输入设备:话筒、摄像头、键盘、鼠标、磁盘、网卡
输出设备:声卡、显卡、网卡、磁盘、显示器、打印机,也可能两者都是
目的:通过设备连接进行设备之间数据流动
数据流动:本质就是设备之间会进行数据的来回拷贝,而拷贝的整体速度是决定计算金效率的重要指标
内存金字塔
可以看出,接近顶的存储单元效率高,但是造价贵,容量小
为啥要有内存呢?
如果没有内存,cpu的处理速度是很快的,当输入的速度很慢,以至于导致cpu存在很大的时间是空闲的,
内存的引入可以使计算机的效率还不错,造价便宜-
例子:
(1)程序在运行的时候,必须加载到内存里面去
我们知道,程序是一个文件,文件存储在磁盘,而磁盘属于外设,程序最终是要在cpu上运行的,在数据层面,cpu只和内存打交道,而外设只和内存打交道,所以程序要运行,必须加载到内存里面去
操作系统
操作系统是一款进行软硬件管理的软件
当我们打开电脑的时候被加载的就是操作系统

每一款硬件,都有属于自己的驱动程序,而操作系统里面 的软件只有负责调动对应的驱动程序就可以调动对应的硬件
操作系统是管理者,硬件是被管理者,两者不需要见面,本质是 对相对应的数据进行管理就行
管理者的核心是做决策,根据数据做决策
对于管理的一个计算机的建模过程:先描述,再组织
计算机管理:
1 .先描述(struct描述)
2.再组织(数据结构)
为啥要有操作系统?
它让计算机运行更为稳定,同时也减少了软件开发者的工作量,因为程序猿只需要考虑操作系统的标准接口,而不需要考虑硬件系统的底层差异
为啥要有操作系统管理:
对下管理好软硬件资源-------手段
对上提供一个良好的运行环境-----目的
系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
进程

我们可以看到
-
我们可以同时启动多个进程
-
操作系统要管理多个加载到内存的程序
-
操作系统管理加载到内存的程序是要先描述,再组织
可以看出,可执行文件加载到内存中会形成一个个的PCB对象,所以,进程 = PCB对象+可执行程序
如果是cpu调用一个进程,只需要调用对应的PCB就可以了,
所以,未来对进程的控制和操作,都只和进程的PCB有关,和进程的可执行程序无关
描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
在Linux中描述进程的结构体叫做task_struct 。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。(就是cpu的pc指针)
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针(找到进程)
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
查看进程
powershell
ps axj

下面我们可以写一个c的程序进行运行
然后运行
powershell
ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep

如果我们不断的运行这个程序就会发现该程序的PID是不断变化的,而父进程的PID是不变的,
当我们查看进程PID为父进程的PID的时候
我们可以通过查看系统文件/proc来进行,查找对应的父进程的PID或者普通进程的PID
powershell
/proc

当我们进入到普通进程的文件夹里面
的时候
powershell
ls /proc/3546 -la ## 3546是一个进程文件夹

当我们把这个进程运行结束,这个文件夹也就没有了,
图中:
这个就是该程序的所在目录,前面我们学过C语言的fopen函数,就会知道,如果我们往文件写入,如果文件不存在就会在当前目录创建该文件.计算机是怎么知道当前目录的,就是靠这个进程的cwd指向的地方
我们先写一段代码进行实现
然后运行出来,分别查看更改前和更改后的cwd有没有改变,
更改前:
更改后:
可以发现是更改了,如果取查看对应的目录就会发现,文件也创建好了
创建进程(fork)
一个进程的创建,先创建出来的是PCB,然后再把可执行程序进行加载到内存
认识fork
powershell
man 2 fork
然后我们写一个,如图的代码
当我们打印出来的时候就是
和我们预期的不一样,为啥呢?
因为fork之后的代码父进程和子进程共享,上图中的子进程的父进程的PID就可以大致猜到一二了,
fork函数有两个返回值,如果是父进程就会读取到子进程的PID,如果是子进程就会读取到0,否则返回失败就返回小于0的数
为啥会这样呢?
因为父进程可以拥有多个子进程,而子进程只有一个父进程,子进程找到父进程很容易,但是父进程找到子进程必须有PID,也就是父进程要知道子进程的PID
为啥fork函数会返回两个值呢?
因为父进程在fork函数返回一次,子进程在fork函数也返回一次,而我们的变量是一个虚拟地址,当我们把C语言代码,变成二进制指令,也就是映射到了物理地址,在Linux中可以使用相同的同名的变量,来表示不同的内存,也就是说,
在操作系统中,fork() 是创建一个新进程的系统调用。在调用 fork() 之后,操作系统会复制父进程的地址空间,并将它分配给子进程。这种机制被称为写时复制(Copy-on-Write)。
在父进程和子进程之间,初始时它们共享相同的物理内存页面。这意味着它们的虚拟地址是相同的,但实际上指向的是相同的物理内存页。这样,父子进程之间可以共享数据,减少内存的开销。
然而,当其中一个进程尝试修改共享的内存页时,操作系统会采取一定的措施来保证数据的一致性。具体来说,当有进程要修改一个被共享的内存页时,操作系统首先会为该进程分配一个新的物理内存页,然后将原来的内存页的内容拷贝到新的内存页中。这样一来,父进程和子进程分别拥有各自的内存页副本,它们的虚拟地址对应的物理内存页就不再相同。
因此,通过 fork() 创建的子进程在修改共享变量时会拥有自己的副本,这就解释了为什么父子进程的公共变量在相同的地址上具有不同的值。
需要注意的是,这种写时复制机制只适用于父进程和子进程之间的直接修改。如果父进程创建了子进程,并且子进程继续创建了更多的子进程,那么它们之间的共享内存仍然是相同的,因为它们共享同一个物理内存页。只有当有进程尝试修改这些共享内存页时,才会进行拷贝操作。
此外,每个操作系统在实现 fork() 和写时复制时可能有所不同,但基本原理是相似的。以上解释是一般情况下对 fork() 和写时复制机制的描述,具体细节可能因操作系统的不同而有所差异。
进程之间是不影响,一个进程结束,不会影响另外一个进程
杀死进程
kill -9 8854
进程
进程排队
我们知道进程不会一直运行下去,哪怕放在cpu上,进程排队一定是为了等待某种资源,是进程的PCB进行排队的,进程 = stack_struct + 可执行程序;
PCB可以被多种数据结构连入,就拿我们熟悉的链表形式
在我们平时练习的链表中
链表的next指向下一个节点的地址
而在Linux中有点不一样
是stack_struct里面一个listnode来进行链接的,
如果要拿到stack_struct的首地址,可以listnode减去偏移量,定义一个listnode结构体变量为n
偏移量
powershell
&(((stack_struct*)0)->n)
进程状态
运行
进程处于要么是在运行中要么在运行队列里
所谓的状态就是一个整形变量
powershell
#define NEW 1
#define READY 2
#define RUNNING 3
#define BLOCK 4
而状态在stack_struct中,是一个整形变量,这个整形变量的值决定这个进程的状态
进程的状态决定了后续行为
一个cpu一个运行队列
例如
powershell
struct runqueue
{
int num;
stack_struct *q;
}
在运行队列 中的进程称为运行状态
阻塞
指的是一个进程在执行过程中,由于某些原因无法继续执行,需要等待某个事件的发生或者资源变得可用后才能继续执行
前面我们讲过,
操作系统要管理软硬件,要先描述,在组织 ,
然后连入数据结构中进行各种操作
当我们在Linux中写一个简单的输入输出代码
powershell
int a = 0;
scanf("%d", &a);
printf("%d\n", a);
运行起来就会发现,程序等待我们输入,为啥会等待,
我们假设一下,这个进程被放入在cpu的运行队列中,运行到scanf中,由于无法进行运行,就会把该进程的状态改为阻塞状态,踢出cpu运行队列中,然后把该进程链入到键盘设备队列(等待队列)中,等到输入数据,就会把这个进程在放到cpu的运行队列里,并把阻塞状态改为运行状态,等待调度
需要注意的是,每个设备都可以有自己的队列,就跟cpu的运行队列是一样的,只是这个队列是等待队列,cpu的是运行队列
总结: 状态的变迁,会引起的是PCB会被OS(操作系统)变迁到不同的队列中
挂起
前提:计算机资源吃紧,
我们知道,一个进程要先运行,必须加载到内存里面去,进程 = PCB+可执行程序 ,只要我们对对应的PCB进行管理,就可以对对应的代码和数据进行管理,内存拥有很多进程,在运行一些进程时,如果发生内存不足,就会使进程崩溃,为了解决这个问题, 内存就有一个机制,就是把一些目前未在就绪状态的进程(或者没有现阶段不需要运行的进程)中的可执行程序(代码和数据)和磁盘的swap分区 进行换入和换出,把内存的空间腾出来、
此时这些进程就称为挂起状态
注意:PCB不能唤出,因为唤出,就无法知道这个进程是啥,无法管理,
一个进程的创建,先创建出来的是PCB,然后再把可执行程序进行加载到内存
进程状态的代码
powershell
/*
* 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 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列
里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
运行状态(R)
我们写一个简单的代码,来运行看看
powershell
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("pid: %d\n", getpid());
sleep(2);
}
return 0;
}
运行起来
查看对应的进程
powershell
ps ajx | head -1 && ps ajx | grep myprocess|grep -v grep

可以看到进程的pid, STAT表示的就是状态 ,可以看出,这个程序是S+状态,为啥会是这个状态,
因为printf是要访问外设的,还有sleep是在等待,,所以说这个进程处于一种等待状态,printf执行完,sleep执行完,如果要想该进程是R状态,我们可以删除while里面的代码,只进行循环,
这里我选择注释掉,我们运行看看
可以看出,这个程序是一直运行, 运行状态,我们添加scanf等这些函数,本身这些函数需要调用到对应的外设就得需要运行对应的外设进程,这些外设进程不一定是就绪状态,需要等待, 虽然我们看到很快,但是cpu的运行速度比外设快很多的
后台进程
需要注意的是: R+中的+表示该进程是前台, 如果要把该进程放在后台
可以在直接在在后面加个 &,表示后台进程
powershell
./myprocess &
后台进程需要用kill命令来杀死才行
运行状态(S)
是多种阻塞状态的一种,这种状态是可以终断的,ctrl+c就可以终断
例如:
如图,简单代码,然后运行,然后中断
运行状态(D)
是阻塞状态的一种,这种状态是不可中断 的,不能被杀死
我们可以想像一下,
内存很吃紧的状况下, 进程吃着瓜子等数据唤入,操作系统为了内存能腾出空间,就会杀死进程(发生在内存严重不足的情况下),如果刚好在写入swap分区 的数据刚好还差一些数据没有写入,该进程又刚刚杀死了。swap分区就会回收该进程的在swap占据的全部空间,刚好这个进程是一个很重要的进程,就会导致了这部分的数据的丢失,所以为了解决这个问题, 就增加了这个运行状态,
运行状态(T)
也是阻塞状态的一种
这里为了让我们清楚的看到,需要用到kill命令
kill有多种的命令,
查看kill命令相关发
powershell
kill -l

这里我们需要用到18和19,
19命令可以使一个正在运行的进程暂停,
powershell
kill -19 17934

可以看到进程停止了,我们查看进程的状态
变成了T状态,并且进程由前台变成在后台上,
我们再把该进程运行起来
powershell
kill -18 17934
结果:
会发现,进程重新运行起来,但是ctrl +c 没有把进程终止掉,查看进程的状态
发现进程在后台上,这里后面讲解
运行状态(t)
也是阻塞状态的一种
这也是一种暂停状态,只是这种状态和T状态不一样,有一种属性:被追踪的状态
这里我使用gdb演示一下:
进入gdb:
状态:
调试进程状态是前台睡眠状态(阻塞状态的一种),这个进程不是我们关注的
当我们打个断点运行起来,运行到断点处,我们查看myprocess的进程(该进程是状态为t的进程,不是第一个进程),就会发现状态是t
运行状态(Z)
僵尸状态
简单的理解就是,该进程执行结束了,但是不能马上进行销毁,只有等到父进程或者其他进程获取到该进程的状态,该进程 的PCB就会被释放掉,,如果该进程的状态一直没有被获取,PCB就会一直存在, 但是代码和数据可以释放掉
我们可以写一个程序
powershell
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<unistd.h>
6 int main()
7 {
8 int a = fork();
9 if(a > 0)
10 {
11 printf("我是父进程,我知道子进程的pid:%d\n", a);
12 }
13 else if(a == 0){
14
15 printf("我是子进程,我获取到的for值是:%d\n", a);
16 exit(0);
17 }
18 else{
19 printf("我是失败了\n");
20 exit(0);
21 }
22
23 sleep(30);
24
25 return 0;
26 }
~
运行起来的结果如下:
然后我们查看对应的子进程的就会发现,子进程的状态是僵尸状态,如图:
所以我们可以理解为: Z状态是希望这个进程结束后,需要给上层读取, 如果该状态下的进程没有被获取,就会一直占用内存,(内存泄漏), 所有的进程运行结束后,先是Z状态哦
孤儿进程
前面我们知道了。子进程结束后会等待上层获取到,然后子进程才能被释放掉。如果父进程比子进程先结束,则该子进程被称为孤儿进程,父进程会被bash进程回收掉
孤儿进程被1号init进程领养,当然要有init进程回收喽。
例子:
我们可以一个例子,代码如下
powershell
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 int main()
5 {
6 pid_t it = fork();
7
8 if(it)
9 {
10 int i = 0;
11 printf("I am is father, pid: %d ppid:%d\n", getpid(), getppid());
12 while(i <5)
13 {
14
15 printf("I am is father, pid: %d ppid:%d\n", getpid(), getppid());
16 i++;
17 sleep(1);
18 }
19
20 }
21 else
22 {
23 int i =0;
24 printf("I am is chlid, pid: %d ppid:%d\n", getpid(), getppid());
25
26 while(i <20)
27 {
28 printf("I am is chlid, pid: %d ppid:%d\n", getpid(), getppid());
29 i++;
30 sleep(1);
31 }
32 }
33 return 0;
34 }
~
运行结果:截图主要分为两部分,左部分是运行程序,右部分是查看进程的
从图中可以看出,父进程比子进程先释放掉,子进程被1号进程领养了,子进程也跟着变成后台进程了
需要被kill -9 来杀死
进程优先级
注意: 优先级和权限是两个不一样的概念,优先级是先后顺序,权限是可不可以做
显示当前用户运行的所有进程
powershell
ps -al
如图,
图中的PRI就是优先级,
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
相当于一个PCB结构体里面的一个整数
linux里面默认的优先级就是80, linux的优先级的大小是[60,99],优先级是可以修改的
数值越小,优先级越高
优先级修改
进入任务管理器
powershell
top
如图:
然后输入r键之后就出现下面的提示:
然后输入你要更改哪个进程的pid,然后会有一个提示
然后输入你要增大的数值
.然后查看对应的PRI就行了
其实Linux系统允许用户调整优先级。但是不能直接更改PRI,而是修改nice值,
PRI = PRI(old)+ nice ,所以我们在这里更改的就是nice
如图:
其中这个PRI(old) = 80(这个是定的,不能更改),nice值可正可负,会进行极值的判断,nice在[-20, 19]范围,设置这个范围主要是约束程序员的过度把自己的进程调整过高 ,让一些进程迟迟等不到资源,进而让 造成进程饥饿问题
如果要进行更改nice值,建议 使用
powershell
sudo top
这样可以预防无法进程nice负值操作
Linux的调度与切换
在进程运行的时候,不一定是要把代码运行完,因为现代的操作系统中,都是基于时间片进行轮转执行的
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高
- 效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
进程切换
我们前面知道,进程加载到内存中,然后由cpu调度,其中cpu有一个属于自己的运行队列,把进程安照队列的形式进行排列,cpu调度进程,调用对应的PCB就可以了,PCB会找到对应的可执行程序(代码)
在cpu中拥有许多品质的寄存器,比如:eax/ebx/ecx/,eds.ecs/fg...等寄存器,
进程在cpu运行中,会产生许多的临时数据,这些数据需要使用寄存器来存储,没有存储到寄存器的数据,我们可以默认是进程的代码。
我们知道,进程的运行是基于时间片 来进行轮转的,当一个进程运行的时间,和时间片相等,就会跳到下一个进程中,跳到下一个进程之前,会把在cpu的寄存器(进程运行产生的临时数据都保存在寄存器中, 这些进程的所有临时数据叫做进程的硬件上下文)中的数据保存到对应的PCB(我们目前理解成这个样子),

当该进程第二次被执行时,会找出曾经的硬件上下文,进行恢复,然后延续上次运行程度,继续进行
寄存器VS寄存器的内容
cpu内的寄存器只有一套,但是寄存器内部保存的数据可以有多套,因为寄存器存放在一个共享的cpu设备里面,但是所有数据是进程私有的,一个进程有自己的一套私有数据,
总结
一个进程的运行完成,基于时间片, 通常是在不断的进行进程切换,硬件上下文转移的过程,
Linux进程切换的调度算法
要解决进程的优先级、饥饿、效率问题
优先级的解决
这里我来讲解一下,
图中runqueue是一个cpu的运行队列,这里我讲解红色和蓝色这两个
首先, 红色这个框内中,queue[140],是一个内存指针数组,指向的是进程 task_struct* queue[140],
数组下标0~99是不会使用的,剩下的100~139是使用的,对应的是PRI的范围[60,99],刚好四十个,相当于一个哈希表一样,把优先级相同的进程链入到对应的位置中,100对应的是优先级为60的进程队列,依次往上,
效率解决
在有bitmap[5] ,是一个整形数组, int bitmap[5],设置出这个,主要是为了解决访问queue[140]中的访问问题,
一个整形的大小是32bit,所以这个数组有160个bit,有140个bit一一对应queue的0~139的下标,如果该比特位的值为1,代表对应的queue下标指向的不是NULL,,所以每判断一个bitmap[i],就会决定queue对应的位置的值是否是NULL.不为NULL就运行,
饥饿问题解决
有一些小伙伴可能想到,假如在运行优先级为60的进程的时候,有其他的优先级为60的进程加入,就会导致后面的优先级较低的进程无法得到cpu的运行,造成饥饿问题,为了解决这个问题,就创建出来两个一摸一样的也就是蓝红框的内容,
过渡队列主要是解决进程链入,造成饥饿的问题,后面进来的进程直接链入对应的位置,当活跃队列里面的内容都为NULL,活跃队列和过渡队列的身份就会调换过来
还有一些其他的
图中的array[0]代表的是活跃进程,所以当活跃进程里面的queue的所有元素都是NULL或者时间片到了,活跃进程就变成了过渡进程,这个是下图中的两个指针来控制的
tack_struct * active = &array[0];
tack_struct * expired = &array[1];
当活跃进程要变成过渡进程,只需active和expired互相交换就行了
环境变量
main参数 ----命令行参数
前面我们知道,一个源文件只有一个主函数main,很多时候,我们写这个main函数的时候,是不写参数的,其实这个main函数是有参数的,
如图:
一个是整形参数argc,一个是字符指针数组,然后我们运行一下图中的代码就会发现
每当我们运行图中代码,就会把命令行的字符全部输出出来,
主要是类似这样
由一个字符数组接收,然后以空格进行切割,分别放入到argv数组中,最后一个元素是NULL, argc的大小就是数组argv的大小减去1, argc = argv长度 - 1,
我们可以利用这个特性去写一个特色的main函数
如图:
结果:
所以我们可以理解 命令+参数可以实现不同的功能了
环境变量
环境变量不是一个,是一堆,彼此之间其实没有关系, 一般是系统内具体特殊用途的变量
我们知道,定义变量的本质就是开辟空间,所以说系统的+环境变量就是系统开辟的一个空间,里面存储着内容
见见环境变量
前面我们写过很多的C语言程序,可以发现,我们可执行文件在运行是必须如图所示:
当我们运行ls这些命令时,不用使用当前路径或者绝对路径就可以运行,如图
这是是因为有一个环境变量PATH,
查看PATH的内容,注意一定要有$
powershell
echo $PATH

输出出来的是一个个路径,以:为分隔符进行分割 ,每一个路径就是默认搜索的路径,
所以说,环境变量里面存放的是程序的位置路径,
添加路径
powershell
PATH=/home/laoqin/6-6/:$PATH
$PATH是之前的老的内容,这就是一个赋值操作
如果被覆盖了也没问题,直接重启shell就可以了,
为啥启动shell就可以了呢,原因如下:
当我们登入进行的使用bash(当前用户)就会读取.bash_profile文件里面的内容,这个文件在用户的家目录下:
内容如下:
然后我们模拟一下PATH的写法,写一个自己定义的环境变量
MYVAR=aaaaaaa
export MYVAR
效果如下:
保存退出,然后重启Xshell,进行环境变量的查找如下:(我这里定义的是VAR)
环境变量的查看
还要一些其他的环境变量
我们可以通过下面命令进行查看
powershell
env
如图:
定义环境变量
powershell
export 变量名=值
如图
环境变量的特性
这里再来引入一个main函数的参数 char* env[];
写一个类似的代码,然后输出env数组里面的内容,其中env数组是以NULL结尾的,(最后一个元素是NULL),里面存储的是所有的环境变量,
如同:
所以main函数控制着两张表,一张是命令行的表格,一张是环境变量表
-
环境变量具有全局属性,可以被子进程和孙子进程继承
下面代码,父进程和子进程都可以输出#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int main(int argc, char * argv[], char* env[])
{printf("I am is fatherproess, pid:%d, ppid: %d", getpid(), getppid()); for(int i = 0; env[i];i++) { printf("------env[%d]-->%s\n", i, env[i]); } pid_t id = fork(); if(id == 0) { printf("I am is childproess, pid: %d, ppid: %d", getpid(), getppid()); for(int i = 0; env[i];i++) { printf("------env[%d]-->%s\n", i, env[i]); } } return 0; }
获取环境变量的方式
除了使用env这个数组还可以使用environ这个变量获取
这个变量是一个二级指针,使用之前需要声明这个变量
#include<stdio.h>
#include<unistd.h>
int main()
{
extern char** environ;//声明变量
for(int i = 0;environ[i];i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
但是这两种方式使用情况很少,一般使用如下:
-
获取无参 的main的环境变量
这里要用到函数getenv(const char *name)#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
int main()
{
const char* username = getenv("USER");
if(strcmp(username, "laoqin")==0)
{
printf("恭喜主人回家\n");} else{ printf("有外部人进入\n"); }
}
这个代码就是获取当前的用户名,如果用户名相等就输出结果
本地变量
写法:
变量名=值
如图:
注意:使用env方法是查找不到的,需要使用set
如下:
本地变量不是环境变量。不会被子进程继承
删除环境变量
写法:
unset 变量名
本地变量也是一样的
环境变量命令总结
和环境变量相关的命令
1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量
程序地址
前面我们使用fork创建进程的时候,在不同的进程输出PID是不一样,为啥不一样呢,下面先介绍一下:
程序地址空间图
然后我们一一写一下对应的代码看看是否图中的一样
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<vector>
using namespace std;
int variable_One;
int variable_Two = 1;
int main()
{
vector<int>* var = new vector<int>(4,1);
printf("正文内容地址: %p\n", main);
printf("初始化地址: %p\n", &variable_Two);
printf("未初始化地址: %p\n", &variable_One);
printf("堆地址: %p\n", var);
printf("栈的地址:%p\n", &var);
return 0;
}
效果图:
可以看到不同类型的地址创建,是先使用低地址,再使用高地址,
我们再写一个同为栈或者堆的地址的创建是先使用啥的,如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<vector>
using namespace std;
int variable_One;
int variable_Two = 1;
int main()
{
vector<int>* var = new vector<int>(4,1);
vector<int>* var_1 = new vector<int>(4,1);
vector<int>* var_2 = new vector<int>(4,1);
vector<int>* var_3 = new vector<int>(4,1);
vector<int>* var_4 = new vector<int>(4,1);
printf("var:%p\n",&var);
printf("var:%p\n",&var_1);
printf("var:%p\n",&var_2);
printf("var:%p\n",&var_3);
printf("var:%p\n",&var_4);
printf("stack var:%p\n",var);
printf("stack var:%p\n",var_1);
printf("stack var:%p\n",var_2);
printf("stack var:%p\n",var_3);
printf("stack var:%p\n",var_4);
return 0;
}
效果图:
可以看见,堆和栈地址的创建,都是先使用高地址的再使用低地址,
当我们仔细观察栈地址和堆地址的就会发现,栈的地址比堆区的大。
下面我们输出一下对应的命令行和环境变量对应的地址
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int variable_One;
int variable_Two = 1;
int main(int argc, char* argv[], char* env[])
{
int * var = (int*)malloc(10);
int * var_2 = (int*)malloc(10);
int * var_3 = (int*)malloc(10);
int * var_4 = (int*)malloc(10);
printf("var:%p\n",&var);
printf("var:%p\n",&var_4);
printf("stack var:%p\n",var);
printf("stack var:%p\n",var_4);
free(var);
free(var_2);
free(var_3);
free(var_4);
for(int i = 0; i < argc; i++)
{
printf("argv[%d]地址:%p,值为:%s\n",i , argv+i, *(argv+i) );
printf("env[%d]地址:%p,值为:%s\n",i , env+i, *(env+i) );
}
return 0;
}
效果如下:
可以看清楚,环境变量的地址比命令行参数的地址可以说明,先有命令行表才有环境变量表
虚拟地址
到这里我们就有一个疑问,这个表中是否是存在内存中的呢?答案不是
下面写一段代码进行验证
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int g_var = 200;
int main()
{
int pid_d = fork();
if(pid_d)
{
int variable = 0;
while(1)
{
variable++;
printf("father: PID: %d, PPID:%d g_var:%d &g_var:%p\n",getpid(), getppid(), g_var, &g_var);
sleep(1);
if(variable == 5)
{
printf("father: g_var chage 1000\n ");
g_var = 1000;
}
}
}
else
{
while(1)
{
printf("child: PID: %d, PPID:%d g_var:%d &g_var:%p\n",getpid(), getppid(), g_var, &g_var);
sleep(1);
}
}
return 0;
}
效果:
可以观察出,父进程和子进程在变量改变的情况下,地址还是一样,这就说明我们以前C/C++打印出来的地址都是虚拟地址,不是物理地址 ,所以上面的图不是存在物理内存 叫做进程地址空间 ,所在的地方是操作系统的内部
C/C++的每个进程都有一个对应的进程地址空间
进程地址空间结论
如图:
我们知道进程 = PCB+可执行程序,所以说每个进程都有PCB,然后我们还知道,每个进程也有一个进程地址空间, 而在物理内存和进程地址空间之间有一个映射表,通过对应的虚拟地址,查找映射表,找到对应的物理内存地址,然后访问对应的数据,这就是结论,
前面我们创建子进程的情况就是如下:
子进程会继承父进程相关的PCB和映射表,这就是前面我写的代码输出的结果,当前父进程修改对应的值的时候,物理内存就会创建一个新的地址,拷贝过来(写时拷贝),修改对应的映射表,如图:
这就解释出来为啥上面的代码输出相同的地址了。也解释出为啥fork()函数返回值的不同。
进程地址空间: 是数据结构, 先描述在组织,具体在进程的PCB就是一个特定的数据结构对象
操作系统需要对进程地址空间进行管理。
下面是大致情况
进程地址空间在内部会进行区域划分,划分的本质不是为了 区分区域,而是为了保证每个地址都能使用到,
地址空间 对于我们的代码和数据是不具备保存能力 的,保存我们的代码和数据是在物理内存的,所以可以理解为:通过虚拟地址转化到物理内存上,映射表(页表)就是这个重要的媒介。这个映射是CPU进行的 ,因为CPU有一个CR3的区域,保存着这个页表的物理地址,当PUB读取这个进程的时候,读取到对应的 进程地址空间,就会去找到对应的页表地址进行映射。
进程地址空间是进程拥有的
进程的大致情况 如图:
一个进程 = PCB + 可执行程序(exe),图中的左边的就是进程管理,右侧的就是内存管理,
内存管理,把可执行程序的内容加载到物理内存上,然后进程的PCB通过页表映射出对应的内容,把对应的内容拿到进程地址空间中。
有进程地址空间+页表的好处:
- 把物理内存存储数据的无序存储映射到进程地址空间进行有序存储,让进程用统一的视角看待内存
- 将进程管理和内存管理解耦合。
- 是保护内存安全的重要手段。(野指针访问影响的只是进程地址空间)
扩展
- 前面C/C++使用malloc/new申请的地址不是物理地址而是虚拟地址,在未使用该地址时,对应的页表不会存在对应的物理内存映射关系,物理内存也不会开辟空间,当我们使用该地址的时候,由于没有对应的映射关系,操作系统就会获取到相关操作,期间会进行缺页中断,在物理内存中开辟空间,然后和对应的虚拟地址形成映射关系。然后才能进行使用。
页表
页表不仅仅有虚拟地址和物理地址,还有权限。

下面我们写一个代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
char* str = "hello";
*str = "H";
printf("%s", str);
return 0;
}
前面我们我们知道,字符串常量是不能修改的,根本上就是对应的虚拟地址映射到物理内存中的权限只是 读取权限,不是具备写入权限,前面我们定义字符串常量是要添加const的,是为了防止在运行时报错,提前把报错放在预编译的时候。 ,如图:
总结
认识冯诺依曼系统
操作系统概念与定位
深入理解进程概念,了解PCB
学习进程状态,学会创建进程,掌握僵尸进程和孤儿进程,及其形成原因和危害
了解进程调度,Linux进程优先级,理解进程竞争性与独立性。
理解环境变量,熟悉常见环境变量及相关指令, getenv/setenv函数
理解C/C++内存空间分配规律,了解进程内存映像和应用程序区别, 认识地址空间。