目录
1. 冯诺依曼
1.1 计算机的硬件体系结构
五大硬件单元:输入设备(如:键盘),输出设备(如:显示器),运算器,控制器,存储器(指的是内存)

所有的设备都是围绕内存工作的。CPU不能直接从输入设备取数据,而是要去存储器取数据(存储器最主要的工作就是数据缓冲)

总线一般有内部总线、系统总线和外部总线。
内部总线是微机内部各外围芯片与处理器之间的总线,用于芯片一级的互连;而系统总线是微机中各插件板与系统板之间的总线,用于插件板一级的互连;外部总线则是微机和外部设备之间的总线,微机作为一种设备,通过该总线和其他设备进行信息与数据交换,它用于设备一级的互连。
1.2 例子:QQ聊天发送消息
键盘输入信息(电信号转换为数字信号)键盘捕捉到我们的输入信息——>放内存缓冲起来——>交给CPU对信息进行处理(翻译输入了哪些字符)——>处理好了再次发给存储器——>通过输出设备网卡(通过网络进行发送信息,将我们的数字信号再次转换为电信号)——>到达对方电脑,对方的网卡接受我们的信号——交给CPU (处理信息)——>放入内存进行输出——>输出设备显示器
2. 操作系统
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。
笼统的理解,操作系统包括:内核(进程管理,内存管理,文件管理,驱动管理)+其他程序(例如函数库,shell程序等等)
- 设计OS的目的
操作系统通过管理好底层的软硬件资源(手段),为用户提供一个良好(稳定高效安全)的执行环境(目的)
2.2 是什么——操作系统是什么?
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

2.3 为什么——为什么要有操作系统?
操作系统里面,里面会有各种数据。可是,操作系统不相信任何用户!它需要保护自己的数据,自己的数据太重要了。
操作系统为了保证自己数据安全,也为了保证给用户能够提供服务,操作系统以接口的方式给用户提供调用的入口。来获取操作系统内部的数据。这个接口是操作系统提供的用C实现的,自己内部的函数调用—系统调用
所有访问操作系统的行为,都只能通过系统调用完成!
有了这些系统调用,我们就允许用户对底层软件进行间接管理了,有很多人基于系统调用接口来设计各种各样的软件,比如我们常见的shell外壳程序,通过调用某些系统调用接口完成我们的需求。比如:执行我们的命令,需要从磁盘加载到内存这是OS的能力。
操作系统的系统调用接口,使用的时候难度比较大。对我们用户来说,使用起来不太方便,就又有很多人,对OS的系统调用接口进行了封装,封装成库,比如我们在linux下实现C语言就用到了这些封装的库(例如printf是硬件操作从内存输出到外设显示器。printf函数不是我们实现的,是C语言设计者实现的,但是它就停留在把库写出来。真正的实现还是要OS通过系统调用实现)
所以的任何一门语言,C、 c++、JAVA、python、go…等。需要间接的访问硬件,必须通过操作系统,那通过操作系统那必须通过系统调用。
我们学习操作系统的意义:它是这个世界上少有的不变的东西,上层语言一直在变,但OS理论思想是不变的,底层一定只能通过这种方式访问硬件。
访问指令的操作,例如pwd(查看我们的当前路径),又比如创建一个文件 touch(在指定路径下(磁盘上)创建文件)等都会访问硬件,你以为是指令操作的,但是其实本质都是需要操作系统接口,完成该操作。
我们把这种基于操作系统的系统接口的开发称为系统编程。我们未来学习的就是系统编程之上做的开发。
有了外壳shell程序,有了c、c++库,有了部分指令,用户就可以进行指令操作、进行开发,进行系统的各种管理操作。
最终结论:
- OS对下管理好软硬件资源,为了对上提供良好的运行环境
- 我们OS不相信任何用户,需要让系统层面为用户提供C语言实现的系统调用接口,让用户使用系统接口,直接或间接的访问操作系统。操作系统既能在保护自己的情况下,又能给用户提供服务。
2.4 怎么办——操作系统是如何管理软硬件资源的呢?
操作系统是如何做到对软硬件资源的管理呢?
先描述——再管理。
- 管理者和被管理者是不需要见面的
- 管理者在不见被管理者的情况下,如何做好的管理呢?
只要能够得到管理信息,就可以在未来进行。 管理决策 ----管理的本质:是通过对 数据 的管理,达到对人的管理。 - 管理者和被管理者面都不见,我们怎么拿到对应的数据呢?
通过执行者。

那么具体Linux 是怎么做的?
pcb ->task_struct 结构体,里面包含进程的所有属性。
Linux中数如何组织进程,Linux内核中,最基木的组织进程task_struct的方式,采用双向链表组织的
3. 进程概念
-
进程基本概念
用户的角度:是运行中的程序。
操作系统的角度:是程序运行的动态描-PCB,其中包含程序运行的各项信息,实现操作系统对于运行中程序管理。(pcb描述信息:标识符-PID,内存指针,程序计数器,上下文数据进程状态,进程优先级,IO信息,记账信息)
linux下:进程就是task_struct结构体。是操作系统对程序运行的描述,通过这些描述完成对进程的管理。
课本概念:程序的一个执行实例,正在执行的程序等(程序:软件——就是程序员所写的代码,程序本质上都存储在硬盘,因为程序都存储在文件中,文件存储在硬盘里)。
内核观点:担当分配系统资源(CPU时间,内存)的实体。 -
描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
3.1 进程理解
进程概念:
一个已经加载到内存中的程序,叫做进程正在运行的程序,叫做进程(任务)。
进程=内核PCB数据结构对象+你自己的代码和数据
其中PCB数据结构对象指的是:描述你这个进程的所有属性值。
操作系统必须将进程管理起来,
如何管理进程呢?先描述,再组织。
一个操作系统,不仅仅只能运行一个进程,还可以同时运行多个进程。
任何一个进程,从加载到内存的时候,形成真正的进程时候,操作系统要先创建描述进程的结构体对象——PCB ,process ctrl block,进程控制块
PCB是进程属性的集合,struct 结构体包含:进程编号、进程的状态、进程的优先级…
根据进程的PCB类型,为该进程创建对应的PCB对象。
3.2 实例
- 编写myprocess.c
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("my is a process\n");
sleep(1);
}
return 0;
}

- 编写makefile文件
myprocess:myprocess.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -r myprocess
- 让myprocess运行起来
3.2.1 查看进程方式一:ps ajx |grep myprocess
[yxy@VM-4-8-centos 20250816_lessen11]$ ps ajx |grep myprocess

4.2.1 查看进程方式2:
[yxy@VM-4-8-centos 20250816_lessen11]$ ps aux |grep myprocess |grep -v grep
yxy 21332 0.0 0.0 4348 352 pts/1 S+ 23:45 0:00 ./myprocess
[yxy@VM-4-8-centos 20250816_lessen11]$ ls /proc/21332 -l
- 代码新增打开一个文件



3.3 标识符
- 查看进程方式:
[yxy@VM-4-8-centos 20250816_lessen11]$ ps axj|head -1 ; ps ajx |grep mproc
[yxy@VM-4-8-centos 20250816_lessen11]$ ps axj|head -1 && ps ajx |grap mproc
这里 “;”和“&&”作用一样,就是左边命令和右边命令都执行

COMMAND:表示进入这个进程执行的命令
下面的grep ,我们在查mproc,经过grep过滤的时候,包含了一个mproc,所以这里有2条信息。
[yxy@VM-4-8-centos 20250816_lessen11]$ ps axj|head -1 ; ps ajx |grep mproc |grep -v grep

管理进程
[yxy@VM-4-8-centos 20250816_lessen11]$ ps axj|head -1 ; ps ajx |grep mproc |grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
10003 12304 12304 9944 pts/0 12304 S+ 1001 0:00 ./mproc
[yxy@VM-4-8-centos 20250816_lessen11]$ kill -9 12304
[yxy@VM-4-8-centos 20250816_lessen11]$

3.3.1 系统调用——getpid()
我们认识的第一个系统调用函数,进程数据结构在操作系统内部。因为操作系统不相信用户,所以不能让用户直接访问我们的数据结构体内容PID,所以操作系统提供了系统调用接口getpid()。
- 监控进程
编写代码
- 查看man getpid
- 包含头文件,然后头文件包含,让代码打印pid
- vim mproc.c

[yxy@VM-4-8-centos 20250816_lessen11]$ while :; do ps axj|head -1 ; ps ajx |grep mproc |grep -v grep; echo "-------------------------"; sleep 1;done

多跑几次进程,我们发现我们的进程PID每次都是不同的,

多运行几次,我们发现PID每次都变,但是PPID一直没变,一直是10003

grep下10003这个是什么进程
[yxy@VM-4-8-centos 20250816_lessen11]$ ps axj|head -1 ; ps ajx |grep 10003 |grep -v grep

发现PID为10003是bash这个进程,每条指令执行都属于bash的子进程,bash子进程。
当我们每次登录xshell的时候系统会单独给bash建一个进程。即命令行终端。
我们在命令行中输入的所有的指令都是一个进程,所有的指令进程的父进程都是bash进程,bash进程只负责命令行解释,具体执行的时候出任何问题,只会影响子进程。
题外话
1.有没有母亲进程?没有。
2. getpid,getppid,有没有获取爷爷进程的函数呢?没有。
使用系统调用接口的时候和使用C接口的时候一样,直接调用即可。
4 进程创建
我们在3.2示例中就清楚了,我们在线运行一个程序就是创建了一个进程,那还有什么其他方式创建进程呢,那就是使用fork函数来创建一个进程。
- ./运行一个可执行文件 —— 在指令层面创建的进程
- 调用系统调用函数fork()—— 在代码层面创建的子进程
4.1 fork函数初识
输入命令:man fork


通过复制调用进程(谁调用他就复制谁)来创建一个子进程。子进程从fork之后进行运行。(这个复制是复制了大部分信息,不是复制全部信息比如进程ID就不会复制的。)
返回值:如果成功复制子进程则返回子进程PID,在父进程中返回子进程PID ;在子进程中返回0;失败返回-1。
- 基本使用
- vi mproc.c :只打印两个信息

#include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 printf("leihoua~~\n");
8 return 0;
9 }
编译一下:
make
运行一下
./mproc
- 修改mproc.c :中间放开fork()函数;
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("before:only one line \n");
fork();
printf("after:only one line \n");
sleep(1);
//pid_t id = getpid();
//pid_d pid= getppid();
// while(1)
// {
// printf("my is a process,my id is:%d,parent:%d \n",getpid(),getppid());
// sleep(1);
// }
return 0;
}
编译运行一下

再考虑下函数返回值

- 再次修改mproc.c:
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("我是一个进程,pid:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id==0)
{
//走到这里表示这是一个子进程
while(1)
{
printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else if(id>0)
{
//走到这里表示这是一个父进程
while(1)
{
printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//这里表示fork出错了。
}
return 0;
}
编译运行:

父进程的父进程是7258,它是我们的bash进程。
[yxy@VM-4-8-centos 20250818_lessen11]$ ps axj|head -1 ; ps ajx |grep 7258 |grep -v grep

所以它的调用顺序是这样的:
我们的bash 在命令行中执行./mproc
它和普通进程一样被创建了,之后系统给它分配了PID :20750
后来这个进程被操作系统调度执行代码,执行到fork之后,由fork一分为二,变成了两个执行分支,一个是父进程(就是他自己),fork之后新的分支就是它的子进程。
当我们执行了fork之后,
4.1.1 fork函数的四个问题
1. 为什么fork要给子进程返回0,给父进程返回子进程pid?
(1)为了fork之后,返回不同的返回值,为了让不同的执行流执行不同的代码块。
一般而言,fork之后的代码,父子共享。
在现实生活中一个父亲可以有多个子女,但是一个子女只能有一个父亲,所以父进程要对子进程做控制,所以对父进程来说他需要对子进程进行区分,所以需要给父进程返回子进程的pid,用来标定子进程的唯一性。对于子进程来说,它直接自己调用系统调用函数getpid()和getppid()即可获取自己的pid以及自己的父进程pid,所以只需要给子进程返回0,表示它创建成功即可。
2. 一个函数是如何做到返回两次的?如何理解?

frok是个函数,当它当走到return的时候,此时的子进程已经被创建完成了,父子进程共享代码,所以return的时候,子进程和父进程都进行了一次return。
3. 一个变量怎么会有不同的内容?如何理解?
这里涉及到了数据层面的写时拷贝技术,因为我们父进程调用fork函数创建了子进程(进程是由内核数据结构+自己的代码和数据组成的)子进程被创建之后,操作系统会自动给子进程创建对应的数据结构,但是子进程目前是没有自己的代码和数据。此时,子进程就直接复制了父进程的代码。
对于数据这块,我们思考下它如果把父进程的所有数据都拷贝进来,这样会造成内存浪费,很多数据我们子进程并不一定会使用。所以这里引入了一个数据层面的写时拷贝技术:当我们子进程要对父进程的数据进行修改的时候,操作系统就给子进程单独开辟一个空间,来把子进程要修改的父进程的数据,拷贝到这个子进程的空间中,所以我们看到的id此时有两个值,他俩来自于不同的空间。这个就是数据层面的写时拷贝技术。
我们fork函数走到return的时候,我们就需要对数据进行写回了,此时操作系统会给子进程开辟自己的内存空间,存放子进程的id值。
但是这里我们还是不理解为什么一个程序里面一个变量,来自两个内存空间,但是却可以同时存在一个代码里面。这个要学习到后面地址空间才能理解,目前就学习到这个程度。等我后面学到了,再来补这个小坑~
4. fork函数,究竟在干什么?干了什么?为什么要这么做?
在没有fork之前,内核中只有一个进程,当fork之后创建子进程(本质就是:系统中多了一个进程,包含内核数据结构和子进程自己的数据和代码),以父进程为模板进行创建子进程。
刚开始创建的时候子进程是没有自己的数据和代码,所以当子进程创建之后,它也指向了父进程的代码,所以fork之后父子进程共享代码。——代码是不可以被修改的。
我们创建子进程目的是为了让父子进程跑不一样的代码,虽然父子进程共享代码,但是我们还是想让父子执行不一样的代码块,所以我们在fork函数的设计上,我们让其有了不同的返回值、
(1)分摊任务处理压力。(2)可以让子进程做另一件事情。这种最常用。
继续修改main.c 根据fork函数的返回值来区分父子进程
vi main.c
#include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main()
6 {
7
8 pid_t pid = fork();
9 if(pid >0)
10 {
11 printf("i m parent\n");
12
13 }else if(pid == 0)
14 {
15 printf("i m child~\n");
16
17 }
20 printf("hello !\n");
23 return 0;
24 }
编译运行一下

5. 进程状态
5.1 概念
-
描述当前进程处于什么状态应该如何被操作系统调度管理。
-
进程状态种类:就绪、运行、阻塞。
5.2 linux下的进程状态分类:
- 运行态-R:正在运行的,以及拿到时间片就能运行的。在运行队列的进程。
- 可中断休眠态-S:能够被打断的休眠状态。
- 不可中断休眠态-D:不可被打断的阻塞状态(最典型的就是磁盘休眠)
- 停止态-T:停止运行。
- 僵尸态-Z:程序退出后的中间等待处理状态。
僵尸进程:僵尸态的进程,退出后资源没有完全被释放的进程 。
产生:子进程先于父进程退出,为了保存自己的退出返回值,因此没有完全释放资源,等待父进程处理。
避免:进程等待。
处理:退出父进程。
危害:资源泄露(内存+进程数量)。
5.2.1 运行态
一个CPU绑定一个运行队列,四个CPU就有4个运行队列。自己有自己的运行队列。

5.2.2 阻塞态
CPU对于外设(硬件)的管理,也是先描述在组织。操作系统对硬件设置了一个描述结构体,描述结构体内包含了自己的属性以及自己的等待队列。一个进程对某个资源进行获取,比如等待键盘输入,如果该键批还没输入,或者键盘的等待队列很长,它只能跟在等待队列的后面进行排队。放入到某个资源的等待队列的进程,就是阻塞进程。
当该资源可以被使用的时候,就会从自己的等待队列中唤醒最前面排队的进程,该进程就由阻塞态转变为运行态(R )了
所以从头至尾,所谓的进程状态,本质上其实都是想办法把进程的PCB放在不同的队列中

5.2.3 挂起态
比如现在我们好多个进程在等待键盘输入,所以我们键盘的等待队列就有多个,在其中一个排队进程排队的时候。突然,操作系统的内存资源严重不足了。此时操作系统就得想办法,在保证正常的情况下省出来我们对应的内存资源,什么办法呢?当我们的进程是阻塞队列的时候,这些进程没有真正被CPU调度的时候,这些进程的代码和数据当前在内存中是属于空闲状态,此时操作系统就想办法把这些进程内核的PCB给先保留,把代码和数据交换到外设(比如磁盘)当中,相当于一个进程现在在内存中只有PCB在排队,当资源就绪了,到时候再考虑把外设中的该进程对应的代码和数据进行换入——这个过程就是内存数据的换入和换出。对于这种进程只有PCB在内存,代码和数据被换出的进程的状态就叫做挂起状态。对于系统中所有等待的进程做这个操作,CPU就能节省出一大部分内存了。
所有这种情况就是阻塞挂起态。
5.3 linux是如何维护进程状态的?
看看Linux内核源代码怎么说
- 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* 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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程状态查看
ps aux / ps axj 命令

5.3.1 S状态实例
vim mproc.c

ls > makefie

死循环让运行起来,我们查看进程状态是S状态,代码中有printf代码,需要访问输出设备,显示器,因为计算机速度很快,而且运行进程也很多,所以我们的进程一直在等待,所以其实S表示的就是阻塞。
[yxy@VM-4-8-centos 20250825_lessen13]$ ps axj |grep mproc

5.3.2 R状态实例
我们修改代码,把printf代码去掉,查看进程运行状态是r了。即运行态。

[yxy@VM-4-8-centos 20250825_lessen13]$ ps axj |grep mproc

5.3.3 D状态实例
- 如果一个进程处于d状态我们称之为Disk sleep状态,也叫作深度睡眠。我们的S状态我们称之为浅度睡眠。可以被唤醒。
- 举一个生活中的例子,你把钱存在银行,但是你的钱真的在银行嘛,其实不是,银行会给要买房的人借钱,你的钱就可能借出去了,但是银行会告诉你吗?不会,你只需要知道你的钱存在银行就行了。取钱的时候可以取出来就可以。
这就和挂起状态很像,操作系统不需要把挂起进程暴露给用户,内存不够的时候操作系统把进程的数据和代码换出到磁盘中,对我们用户来说是不需要知道的,等进程需求满足的时候,操作系统会自己把程序换入,进程能正常运行即可。
D状态进程分析
- 假如现在有一个进程P,要往磁盘写入1GB的数据。打算由于磁盘速度非常慢。所以它只能进入等待队列等待磁盘的写入。而恰好此时操作系统内存不够用了,操作系统先就把其他等待进程挂起(包括P进程),但是操作系统已经把能换出的数据和代码都换出了,但是内存还是不够用,此时操作系统为了保证操作系统不奔溃,就只能自己判断哪些进程不要太重要,把不太重要的进程先杀掉。他刚好就把我们的P进程杀掉了。然后一会儿之后,磁盘写入失败了,它回过头来找P进程,想要问问P进程它写入失败了,是否要重新进行写操作呢?但是进程P已经被操作系统杀掉了。它也不知道怎么办(一般这种情况这1G数据就丢掉了。不同硬件有不同的操作方式)这1G数据大概率被丢掉了,但是如果这个1G数据刚好的银行用户的交易记录呢,这个丢失了,那损失就非常大了。
那为了避免这种情况,让进程P在等待磁盘写入完毕期间,这个进程不能被任何人杀掉,不就行了!
所以此时引入了D状态,只要一个进程目前有写入任务交给了磁盘,如果磁盘没有办法立马响应的话,需要进程等待,这个进程就绝对不能以浅度睡眠的状态S状态进行等待,必须把自己设为D状态,我们的源代码规定,D状态的进程,不能被任何人杀死,操作系统也不行。
当磁盘写入完成,D状态的进程再把自己恢复到运行态R态。即进程在等待磁盘写入时,此时所处的状态就是Disk sleep状态,D状态。不响应任何请求,除非它自己醒来。 - 如果系统中出现了大量D状态的进程,都在等待磁盘写入反馈,操作系统无法杀死,我们进行系统重启依然无法解决这些D状态的进程,解决办法:1. 要么就只能等待磁盘写入完成,2. 要么就只能断电,打断磁盘操作。一般如果系统中出现D状态了,那说明磁盘压力已经非常大了,说明操作系统已经快要奔溃了。如何出现了好几个,那就意味着操作系统马上就挂了。
D状态就不演示了,因为D状态是计算机处于高IO状态下的,很容易就把操作系统搞奔溃了。
可以了解下dd命令来模拟一下高IO的情况,能看到D状态进程。
小结
- D状态是休眠吗?是的是深度休眠状态
- D状态是阻塞状态的一种吗?是的。操作系统里面只有一个阻塞状态进程,但是在Linux下有浅度休眠和深度休眠,操作系统的理论和实践是有区别的。
5.3.4 T状态
在linux下T状态有两种,一个是T,一个是t,现在的Linux下这二者没什么太大的差别了。
我们称之为暂停态,也叫作stop,
实例
vim mproc.c

进程里面使用信号可以杀掉进程,我们之前用过9号信号,我们输入kill -l 可以查看所有信号。其中19信号就是stop信号,18信号是继续信号
[yxy@VM-4-8-centos 20250825_lessen13]$ kill -l


[yxy@VM-4-8-centos 20250825_lessen13]$ sudo kill -18 8079

两个问题
- stop状态和sleep(S/D)状态有什么区别?
S状态和D状态仅仅是在等待某种资源。
stop状态即T状态,为什么会暂停呢,是在等待某种资源吗?有时候是这样,但是有时候不是,有些情况就是想让该进程暂停。也可以理解成阻塞状态,但是具体情况具体分析。如果一个进程是T状态,那么就有两种情况导致它是T状态(1)他缺乏某种资源不得不暂停(2)我就是想让该进程暂停,比如我们上面的例子,就是手动暂停了进程。
结论:它俩的共性就是它俩都是操作系统级别的阻塞状态,区别就是S和D一定是等待某种资源,但T可能是在等待某种资源,还有可能是被其他进程控制。
- 什么场景下,我们需要控制一个进程呢,当我们在进行GDB进程的时候,GDB进程肯定在运行,但是我们被GDB调试的代码,就需要被GDB进程所控制。
5.3.5 X状态
一个进程死掉了,我们操作系统会把该进程的资源释放掉,一个是内核PCB数据,一个是进程自己的代码和数据。进程终止之后,我们可以把进程放入到一个叫做垃圾回收的队列中,操作系统可以定期把去垃圾回收的队列中把dead的进程释放掉。
5.4 Z状态——僵尸进程
5.4.1僵尸进程引入:
当一个进程死亡的时候,它在死亡之后,并不会立即进入dead状态,而是先进入一个Z状态。
举一个生活中的例子,当你晨跑的时候,有一个老大爷超过你,结果不一会儿你看到他由跑变成走,再然后啪叽一下躺下了~此时你怎么做,你需要打两个电话,1个120一个110 ,120确认死亡之后,110就开始维持现场秩序,围起来保护环境,让法医对其进行检查,然后给出结论,看是否排除谋杀可能性,然后给出结论。给社会一个结论,给其家人一个结论。
放到我们进程身上,也是一样的,当一个进程要退出的时候,它并不是立即把自己的资源全部释放掉。当一个进程要退出的时候,操作系统需要把当前进程的状态(退出信息)维持一段时间,以告诉别人,让关心该进程的人知道结果或原因,所以当一个进程从退出到被确认退出之前,这一段时间,状态还没有被检测,所以此时操作系统会对该进程进行维护,直到确认进程已经结束了,此时操作系统才真正释放掉该进程的资源。
当一个进程退出的时候谁最关心该进程呢?——该进程的父进程
我们父进程把子进程创建出来,子进程突然挂掉,我们的父进程就要关心下子进程是如何挂掉的(我们勤勤恳恳创建一个子进程运行我们的代码,结果它就挂了肯定要搞明白咋回事,我们还要继续完成我们的任务,可能需要开辟新的子进程等等)。当一个子进程退出时,会暂时维持住自己的状态。直到父进程或者关心该子进程的进程把子进程对应的信息读取到了,该子进程才会释放自己的资源。
当子进程结束了,但是一直没有父进程或者其他进程来读取它的信息,那么操作系统就必须把该子进程的状态维护住,我们把这种已经死掉的,但是还没有父进程来关心的进程,就称为僵尸进程。
这种情况下,要是父进程一直没有来确认子进程信息,那就会造成内存泄露问题。
所以内存泄露不仅仅是在写代码的时候申请空间new,malloc之后忘记free释放这种情况,在我们系统进程层面也可能会发生内存泄露问题。
实例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
int cnt = 5;
while (cnt)
{
printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
}
else
{
//father
while (1)
{
printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
//父进程目前并没有针对子进程干任何事事情
}
}
修改makefile文件内容

运行,开两个ssh渠道,一边运行代码,一边检测进程状态
[yxy@VM-4-8-centos 20250825_lessen13]$ ps ajx |grep t_mpc

因为这里我们的父进程while(1)死循环,所以虽然我们的子进程已经结束运行了,但是父进程并没有来查看它的状态,它一直在等待父进程进行状态确认,一直没有退出,此时这个子进程就变成了僵尸进程。
产生原因:子进程先于父进程退出,为了保存退出原因,因此等待父进程获取状态释放资源。
5.4.2 僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏?是的!
处理:退出父进程。
- vim main.c 代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main()
6 {
7
8
9 pid_t pid = fork();
10 if(pid > 0)
11 {
12
13 printf("i m parents~\n");
14
15 }else if(pid == 0)
16 {
17 printf("i m a child\n");
18 sleep(6);
19 exit(0); //退出进程;
20 }
21 while(1){
22
23 printf(" i m a zoomProcess");
24 sleep(1);
25 }
26 return 0;
27 }
- vim makefile代码
3 main:main.c
4 gcc $^ -o $@
- 输入:make

从上图可以看出我们无法进行任何其他操作,必须中断这个僵尸进程,才能继续输入命令。所以我们必须结束这个僵尸进程。
- 如何关闭僵尸进程(kill是杀不死僵尸进程的,我们只能关闭僵尸进程的父进程)
(1)查看僵尸进程的父id
输入:ps -ef

(2)查看进程状态(比较详细信息)
输入:ps -aux

(3)筛选一下我们的进程main
输入:ps -aux | grep main
(4)由于我的shell无法通过ctrl +C进行进程终止,我都是使用Ctrl+Z结束了我的僵尸进程。可以看到上图很多Z状态进程的父进程(就是Z上面的T)都是T状态。为了杀死T状态的进程我这里使用了 kill -9 PPID

5.5 孤儿进程
产生原因:父进程先于子进程退出,子进程就会成为孤儿进程。
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?父进程先退出,子进程就称之为“孤儿进程”,它的父进程就改为1号进程,1号进程就是我们的操作系统。
特性:运行在后台,父进程变为1号进程(孤儿进程退出后不会成为僵尸进程)。
守护进程:特殊的孤儿进程,在孤儿进程的基础上脱离与终端之间的关系(比如新建回话)。
实例1
vim
#include <stdio.h> #include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
int cnt = 500;
while (cnt)
{
printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
}
else
{
int cnt = 5;
//father
while (cnt)
{
printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
//父进程目前并没有针对子进程干任何事事情
}
}

修改makefile
orphan_Process:orphan_Process.c
gcc -o $@ $^
t_mproc:t_mproc.c
gcc -o $@ $^
mproc:mproc.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -r orphan_Process

make之后让代码跑起来,
[yxy@VM-4-8-centos 20250825_lessen13]$ ./orphan_Process
我们在另一边监控
[yxy@VM-4-8-centos ~]$ while :; do ps axj|head -1 ; ps ajx |grep orphan_Process |grep -v grep; echo "-------------------------"; sleep 1;done

我们发现前面父子进程都存在,后面父进程结束之后,该子进程的父进程ID就变成1了,而且该子进程用ctrl +c无法结束我们只能用kill -9 PID 结束该子进程
我们来看下这个进程ID 是1的进程是什么?
[yxy@VM-4-8-centos ~]$ ps axj |head -1 &&ps ajx |grep systemd

结论:如果父进程先退出,子进程的父进程会被改成1号进程即操作系统。
这种父进程是1号进程的子进程称之为孤儿进程。该进程被系统所领养。
为什么子进程的父进程退出了。就要被领养呢?
因为父进程已经退出了,所以将来没有父进程来回收子进程的僵尸进程,那么未来子进程是会退出的,所以需要操作系统来对其进行回收。
实例2
- vim main.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main()
6 {
7
8
9 pid_t pid = fork();
10 if(pid > 0)
11 {
12
13 printf("i m parents~\n");
14
15 sleep(6);
16 exit(0); //退出进程;
17 }else if(pid == 0)
18 {
19 printf("i m a child\n");
20 }
21 while(1){
22
23 printf(" i m a orphonProcess");
24 sleep(1);
25 }
26 return 0;
27 }
-
makefile和不用变
-
继续make一下执行 ./mian

此时我们发现父进程退出了,但是我们仍然可以执行命令。此时子进程并没有,而是shell占据终端。但是它并没有退出,而是在后台继续运行。 -
查看孤儿进程的父进程 输入命名 ps -ef | grep main

孤儿进程的父进程就变成了1号进程。
6. 进程优先级
6.1 是什么?——什么是优先级?
优先级VS权限:
权限决定的是能还是不能
优先级是你已经能了,只不过谁先谁后的问题。
优先级重点解决的是对于一个资源,谁先访问,谁后访问
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
6.2 为什么?——为什么要有优先级
引入:
现在吃饭也得排队,买东西也得排队,为什么要排队,为甚有优先级的概念?
如果学校给每个学生一个人一个厨子,每次吃饭都给你端到桌子上,那不需要排队了,但是这是不可能的,因为不可能养那么多厨师,资源是有限的
系统中上百个进程,你难道可以给每个进程配一个CPU吗?那显然是不可能的,CPU资源只有一个,所以进程只能互相竞争获取CPU资源
因为资源是有限的,所以进程是多个的,注定了进程之间是竞争关系!——竞争性
操作系统是秩序者,操作系统必须保证大家是良性竞争的——确定优先级。
如果恶性竞争,我们某个进程长时间得不到CPU资源,该进程的代码长时间无法得到推进—该进程的饥饿问题
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可能改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
6.3 怎么办?——linux下是如何实现优先级的?
6.3.1 实例


查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
[yxy@VM-4-8-centos ~]$ ps -al |head -1 &&ps -al |grep t_mproc



我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
6.3.2 进程优先级设置
PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行,
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别。
PRI vs NI
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
6.3.2 linux下调整进程优先级实例
优先级可以被调整,更改优先级的指令:nice和renice指令(可以浏览器搜索centOS7 nice renice 调整优先级)
查看进程优先级的命令
用top命令更改已存在进程的nice
top
进入top后按“r”->输入进程PID->输入nice值
- 我们创建一个进程

- 让进程运行起来,然后开另外一个ssh渠道,先看下该进程的优先级
[yxy@VM-4-8-centos 20250825_lessen13]$ ps -al |head -1 &&ps -al |grep t_mproc

- 此时进程优先级是80。我们通过top命令进行修改优先级
[yxy@VM-4-8-centos 20250825_lessen13]$ top
(1)然后进入top页面

[yxy@VM-4-8-centos 20250825_lessen13]$ sudo top
[sudo] password for yxy:

(2)然后我输入了我们的进程ID 13753



- 修改完成后,再次查看进程信息

因为操作系统是不希望某个进程把自己的优先级永远设置成最高,一直去运行它,所以它对NI的值进行了一个范围限定,就是你最多就只改这么大的范围。我们输入了-30,但是它最高只能设置-20
nice其取值范围是-20至19一共40个级别。
我们再设置NI的值为100,看看结果现在变成多少?
疑问,刚刚不是60吗?把nice值修改为100,实际应该是19,我们优先级不应该是60+19=79吗?
这个就是Linux下的一个设计,只要你修改NI值,这个old的值就是你修改之前的值,就是最初的80+19 = 99
6.3.4 其他概念
- 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰(eg:QQ崩了,不影响微信使用。)
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
进行进程切换+时间片=====>进程切换的基于时间片轮转的调度算法
两个问题
- 函数返回值,为什么会被保留下来?为什么会被外部拿到?
函数返回值,会写到对应的寄存器,通过CPU寄存器拿到的。
具体是什么寄存器,取决于CPU,
return a ->mov eax 10
- 计算机怎么知道我运行到哪一行了?系统如何得知我们的进程执行到哪一行代码了?
CPU内部有程序计数器(PC指针)。会记录当前进程正在执行指令的下一行指令的地址。
CPU寄存器分为两大类,一类是可见的一类是不可见的。
为什么要设置这么多寄存器?——为了提高效率,将进程的高频数据放入寄存器CPU内的寄存器。
CPU内的寄存器里面保存的是:进程相关的数据。
CPU寄存器里面保存的是进程的临时数据。——称之为进程的上下文
进程在被切换的时候做了2个工作:
- 保存上下文
- 恢复上下文
进程的上下文可以保存在PCB中,PCB中再套一个结构体存放这些数据。
什么时候切换进程?时间片到了?CPU它怎么知道时间片到了呢?而且进程优先级不是可以进行抢占吗?——学习中断外设和CPU 关系的时候再来理解。
7. 环境变量
学习思路
- 挑出来3~4个环境变量,带大家认识认识,环境变量是干什么的?
- 在谈什么是环境变量
window下的环境变量,计算机右击【属性】,选择【系统高级设置】,然后选择【环境变量】就能看到自己电脑的环境变量,上面是用户环境变量,下面是系统环境变量。

7.1 3~4个环境变量
7.1.1 PATH
创建两个文件:
vim mycmd.c
#include<stdio.h>
int main()
{
int i = 0;
for(;i<10;i++)
{
printf("hello %d\n",i);
}
return 0;
}
vim makefile
mycmd:mycmd.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f mycmd
make下,运行下
[yxy@VM-4-8-centos 20250901_lessen14_env]$ make
[yxy@VM-4-8-centos 20250901_lessen14_env]$ ./mycmd

我们根据以前学习的知识知道,我们自己写的程序也是指令(比如自己写的mycmd),命令也是指令(比如ls ,pwd等等)。
现在的问题是,为什么我们自己写的指令要加./ 而命令,ls .pwd等命令就不需要加./?
我们首先看下pwd是保存在哪里的
[yxy@VM-4-8-centos 20250901_lessen14_env]$ which pwd
/usr/bin/pwd

pwd在系统默认的搜索路径下,/usr/bin/路径下。所以我们执行对应的指令,不需要带./
我们自己写的文件在我们的用户目录下,操作系统找不到,所以需要我们带./
为什么是这样的呢?这就涉及我们的环境变量了。
系统当中针对于指令的搜索,会提供一个环境变量,这个环境变量叫做PATH。
这个是系统开机之后,我们登录xhsell之后,天然存在的,
查看PATH环境变量内容
[yxy@VM-4-8-centos 20250901_lessen14_env]$ echo $PATH

打印出来我们发现是是一堆路径。一个路径后:另一个路径:另一个路径:…。
用:做分隔符,这上面的每一条路径,就是一般在执行指令的时候操作系统查找指令的路径。
换句话说,操作系统怎么知道这些指令的路径呢?因为操作系统在执行命令的时候,shell会首先在路径下找,没到到就继续下一个路径去找,找到了就直接执行该路径下的命令。
我们的mycmd,它的路径没有添加到操作系统默认查找指令路径中,所以我们直接输入mycmd,会报找不到指令

我们可以把我们的mycmd的路径添加到系统路径中。
添加我们自己的路径到系统路径中
[yxy@VM-4-8-centos 20250901_lessen14_env]$ pwd
/home/yxy/linux_20250709/20250901_lessen14_env
[yxy@VM-4-8-centos 20250901_lessen14_env]$ PATH=$PATH:/home/yxy/linux_20250709/20250901_lessen14_env

注意:这里添加路径一定是加$PATH:如果不加会直接把原本的路径全部覆盖
这个是内存级的环境变量,如果我们失误我们覆盖的话,我们用xshell重新登录一下它就恢复了
[yxy@VM-4-8-centos 20250901_lessen14_env]$ echo $PATH


[yxy@VM-4-8-centos 20250901_lessen14_env]$ which myls

which是从PATH环境变量中的路径开始搜索的
7.1.2 HOME
指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
[root@VM-4-8-centos ~]# echo $HOME
root用户
yxy用户
7.1.3 SHELL
SHELL : 当前Shell,它的值通常是/bin/bash。
7.1.4 HISTSIZE
保存的是历史命令的条数
我们的history命令能存多少命令,就是被这个环境变量限制着。
[root@VM-4-8-centos ~]# echo $HISTSIZE

7.1.5 SSH_TTY

7.1.6 env
查看系统当前环境变量
USER:对应当前用户是谁。这里是root
LS_COLOR:ls的配色方案
PWD:当前进程所对应的路径
LANG:编码方案
7.2 环境变量的定义
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
环境变量:保存程序运行环境的变量
特性:具有进程之间的传递性。
思考:我们想获取环境变量,有没有其他方式获取呢?
新增知识点:命令行参数
大家有没有见过命令行函数是可以带参的?
C/C++的main函数是可以传参的,我们来看实例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char*argv[])
{
int i = 0;
for(;i<argc;i++)
{
printf("arcv[%d]->%s\n",i,argv[i]);//把main函数的参数,以这样的方式打印出来看
}
return 0;
}
[yxy@VM-4-8-centos 20250901_lessen14_env]$ vim mycode.c
[yxy@VM-4-8-centos 20250901_lessen14_env]$ make clean
rm -f mycode
[yxy@VM-4-8-centos 20250901_lessen14_env]$ make
gcc -o mycode mycode.c
[yxy@VM-4-8-centos 20250901_lessen14_env]$ ./mycode
arcv[0]->./mycode
[yxy@VM-4-8-centos 20250901_lessen14_env]$

我们发现命令行参数只有一个,指向的就是./mycode
我们继续输入-a -b -c -d -e

其实我们的char *argv[ ] 是一个字符指针数组 ,数组个数是argc个
当我们的C语言程序被运行起来的时候,这俩参数,会被我们的调用方进行传参。
main函数是我们C语言中第一个被调用的入口函数吗?
不是,
在用户层面上,认为main函数第一个被调用,但是其实不是的
第一个被调用的函数是Startup()函数,我们对应的main函数的参数,也是被别的函数传递的,
其他函数调用main函数时,也会给它传参,
我们在命令行输入的命令,我们以为是./mycode -a -b -c -d -e ,但是其实我们输入的是“./mycode -a -b -c -d -e ”,bush 做命令行解释的时候,会把这一长串字符串,以空格作为空格符,会打散成六个字符串
“./mycode”
“-a”
“-b”
“-c”
“-d”
“-e”
之后一共有几个字符串就初始化多少个argc(这里是6个),每一个字符串的起始地址保存到对应的argv指针数组里

所以在我们系统当中,打散成这种结构后,才把这两个参数传递给main函数,这就叫做命令行参数,命令行参数以空格为分隔符。此时我们可以给main函数传参,这个打散的过程叫做命令行解析工作,这是由bash完成的。
问题:为什么我们所对应的程序要做这个操作呢?为什么命令行参数要以这样的形式实现呢?
再来举例编写代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char*argv[])
{
if(argc !=2)
{
printf("Usage:%s -[a|b|c|d]\n",argv[0]);
return 0;
}
if(strcmp(argv[1],"-a")==0)
{
printf("功能1\n");
}
else if(strcmp(argv[1],"-b")==0)
{
printf("功能2\n");
}
else if(strcmp(argv[1],"-c")==0)
{
printf("功能3\n");
}
else if(strcmp(argv[1],"-d")==0)
{
printf("功能4\n");
}
else
{
printf("default功能\n");
}
return 0;
}
make

我们以前很少用到命令行,在linux下以后会用的很多。
为什么要使用命令行参数呢?
就是为了我们在使用命令的时候可以有不同的参数,来实现不同的功能(为了程序书写时使用不同的选项表现出不同的功能。)
不光是C语言要设计这个参数,java python只要是个语言几乎都要支持命令行参数,因为用任何语言写的软件,都需要通过选项来定制化完成想要的功能。
所以
[yxy@VM-4-8-centos 20250901_lessen14_env]$ ls --help

我们可以给我们自己写的函数自己添加一个–help

make一下,运行程序

命令行你以为你输入的是命令,但是bash会把命令根据空格分隔开解析成一个一个字符串,第一个是你要执行的命令,后面的全都是选项。
main函数只有这俩参数吗?还有没有其他参数?
答案是,不是,它还可以有其他参数
main(arcv,charargv[ ],charenc[ ])
{
}
所以我们C/C++代码会有两张核心向量表,一个叫命令行参数表一个是环境变量表。这两张表结构一模一样
[yxy@VM-4-8-centos 20250901_lessen14_env]$ vim mycode.c

make 之后运行,也可以直接实现

我们添加第三个参数看看
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char*argv[],char* env[])
{
int i = 0;
for(;env[i];i++)
{
printf("env[%d]->%s\n",i,env[i]);
}
return 0;
}
make一下,运行代码,环境变量被我们打印出来了:

我们一个进程在运行,不要简单认为就是把程序加载到内存中来。
而是我们的程序变成进程运行的时候,在程序启动时,一定会有其他函数调用我们的main函数,给我们main函数把这两张表传进来。一张叫命令行参数表,一张叫环境变量表。
这上面打印出来的环境变量和我们shell输入命令:env 打印的内容是一样的,本身我们的程序是没有环境变量的,但是当它启动起来变成进程的时候就具有环境变量了。
我们在shell中 ./mycode之后会变成进程,我们的进程是bash的子进程.
换句话说在没有子进程之前,我们对应的bash内部有很多环境变量,我们./mycode之后,我们的子进程是可以继续父进程的环境变量信息的
结论:bash本身在启动的时候会从操作系统的配置文件中读取环境变量信息。bash会自己构建出一张环境变量表,子进程(或我们所运行的进程都是子进程) ,我们的子进程会继续父进程交给我们的环境变量。
我们所运行的进程,都是子进程,bash本身在启动的时候,会从操作系统的配置文件中读取环境变量信息,子进程会继承父进程交给我的环境变量!
从当前的bash开始,此后所有的子进程都可以继承我们父进程bash的环境变量。都可以看到我们的历史环境变量。
1.环境变量也是数据,默认情况是父子共享的,但是进程都是独立的,如果我们创建子进程之后,对环境变量进行了修改,我们是不能影响父进程的,因为我们子进程会写时拷贝。
2.环境变量被继承有两种方式:(1)main函数传参(2)子进程直接继承父进程的环境变量
今天的重点是传参方式
环境变量可以被bash及其后面的子进程继承的。问题是我们如何验证?
自己定义一个环境变量,然后创建进程,查看是否继承了我们创建的环境变量。
[yxy@VM-4-8-centos 20250901_lessen14_env]$ MV_VALUE=12345678

我们在命令行自己定义的一个变量,我们在env中没有搜到,我们在命令行定义的这种变量叫做本地变量。
如果我想让我自己定义的变量变成环境变量,使用命令export
[yxy@VM-4-8-centos 20250901_lessen14_env]$ export MV_VALUE=12345678
[yxy@VM-4-8-centos 20250901_lessen14_env]$ env|grep MV_VALUE

下面我们直接./运行我们的mycode,这是我们的子进程,它会继承父进程bash的环境变量

取消环境变量
[yxy@VM-4-8-centos 20250901_lessen14_env]$ unset MV_VALUE
[yxy@VM-4-8-centos 20250901_lessen14_env]$ env|grep MV_VALUE

再次运行./mycode

这叫做环境变量具有全局属性
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
新增知识点:本地变量&&内建命令
与环境变量相关的还有一个叫做本地变量。本地变量指的就是在命令行中直接定义
这样的变量就是本地变量
[yxy@VM-4-8-centos 20250901_lessen14_env]$ env |grep a
我们在env里面是查不到的。

有没有一个命令可以查看所有变量——set命令
[yxy@VM-4-8-centos 20250901_lessen14_env]$ set

[yxy@VM-4-8-centos 20250901_lessen14_env]$ set|grep a=1

本地变量是不会被继承的,只会在本bash内部有效。
常见的本地变量,set命令中去掉环境变量就是了。
本地变量可以通过export 命令变成环境变量,通过unset命令取消环境变量。
命令行中的命令都是bash的子进程,为什么我们定义一个变量,它是本地变量,不是子进程呢?

执行echo命令的时候,它要不要创建子进程?
如果它要创建,为什么echo能获取到本地变量的值?
这里需要纠正一个概念:命令行上的命令,不一定都要创建子进程。
命令要被分成两批命令:
1.常规命令,通过创建子进程完成的
2.内建命令:bash不创建子进程,而是由自己亲自执行。——类似于bash调用了自己写的或者系统提供的函数。
cd命令是一个典型的内建命令。就是不创建子进程的命令。
编辑: mycode_cd.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
//./mycode_cd /a/b/c
W>int main(int argc,char*argv[],char*env[])
{
if(strcmp(argv[1], "cd")==0)
{
chdir(argv[1]);
}
sleep(30);
printf("change begin\n");
if(argc == 2)
{
chdir(argv[1]);
}
printf("change end\n");
sleep(30);
return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
7.4 和环境变量相关的命令
env: 显示所有环境变量
echo: 显示某个环境变量值(打印某个指令变量的数据)

export: 设置一个新的环境变量(声明一个环境变量)

set: 显示本地定义的shell变量和环境变量

unset: 清除环境变量

7.5 环境变量的组织方式

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
7.6 通过代码如何获取环境变量
7.6.1命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
7.6.2 通过第三方变量environ获取
[yxy@VM-4-8-centos 20250901_lessen14_env]$ man environ

编辑acquire.c
[yxy@VM-4-8-centos 20250901_lessen14_env]$ vim acquire.c
#include<stdio.h>
int main()
{
extern char **environ;
int i = 0;
for(; environ[i]; i++)
{
printf("%d: %s\n", i, environ[i]);
}
return 0;
}
修改miakefile


libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
7.7 通过系统调用获取或设置环境变量
- 通过系统调用获取环境变量:char *getenv(char *name),输入:man getenv
[root@VM-4-8-centos ~]# man getenv
name :环境变量名称;
返回值:对应name环境变量的数据,如果找不到返回NULL

7.7.1 实例:通过系统调用获取环境变量
vim mycode.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
char who[32];
strcpy(who, getenv("USER"));
if(strcmp(who, "root") == 0)
{
printf("让他做任何事情");
}
else
{
printf("你就是一个普通用户,受权限约束\n");
}
return 0;
}
修改Makefile
make
运行

因为有环境变量的存在,所以系统能够认识你是谁。
7.7.2 例子,获取环境变量的 PATH 的值
- vim env.c
1 #include<stdio.h>
2 #include <unistd.h>
3 #include<stdlib.h>
4
5
6 int main()
7 {
8 char *ptr = getenv("PATH");
9 if(ptr == NULL)
10 {
11 printf("there is no PATH\n");
12 }else {
13 printf("%s\n",ptr);
14 }
15
16 return 0;
17 }
- makefile
2 env:env.c
3 gcc $^ -o $@
- make

7.8 环境变量的特性具有进程之间的传递性的例子
- vim env.v
1 #include<stdio.h>
2 #include <unistd.h>
3 #include<stdlib.h>
4
5
6 int main()
7 {
8 char *ptr = getenv("MYVAL");
9 if(ptr == NULL)
10 {
11 printf("there is no MYVAL\n");
12 }else {
13 printf("%s\n",ptr);
14 }
15
16 return 0;
17 }
- makefile文件和上一个例子一样,不用变。make一下。体会环境变量相较于普通变量的传递性:

7.9 环境变量通常是具有全局属性的
环境变量通常具有全局属性,可以被子进程继承下去
7. 程序地址空间
地址空间一共会讲三次,第一次讲框架,后面会慢慢填充
7. 1 引入
在进程创建的时候,我们就说了,一个id值为什么会有两个不同的值,当时没有分析清楚,现在讲程序地址空间,会让我们清晰。
之前学C语言的时候有见过这个地址图

7.1.1 验证1: 验证这张图的真实性
我们验证一下
vim mycode.c

makefile

make 一下,运行mycode


7.1.2 验证2:验证堆栈的增长方向
栈是向着地址减少地方增长
堆的向着地址增加的地方增长

先定义的变量先入栈,因为栈向地址减少的地方增长,所以后定义变量后入栈,后面变量的地址就会小。
- 验证堆区


堆和栈之间距离非常的远,堆的地址6位,栈的地址12位。
1.1.3 验证一个语法问题 staic 修饰的变量,其生命周期不随着函数调用结束而结束。
全局变量,不论已初始化还是未初始化,都在全局数据区,


很显然空间static 地址变了,static修饰的局部变量,编译的时候已经被编译到了全局数据区了,所以它才不会随着你函数的调用被释放了。只不过它在一个函数代码块中,它只能在函数代码块中使用。


再次修改子进程内容


现象:父子进程,同一个变量,大家都在读取,但是地址居然是一样的,但是读的值居然不一样?
怎么可能同一变量,同一个地址,同时读取,读到了不同的内容!!!
结论:如果变量的地址,是物理地址,不可能存在上面的现象!
所以:这个地址绝对不是物理地址
这个地址我们一般称为:线性地址或者虚拟地址。
我们平时写的C/C++,用的指针,指针里面的地址,全部都不是物理地址!!!
7.2
本质上就是操作系统为进程通过mm_struct描述的虚拟的地址空间,让每个进程都能访问一个完整独立的连续的虚拟地址,经过映射之后,实现物理内存上的离散存储,提高内存利用率。提高内存访问控制。
为什么要有地址空间呢?
- 让进程以统一的视角看待内存 – 为什么?
- 直接访问物理空间,如果一旦出现越界,就会损坏别人的代码。必须先在虚拟地址空间加页表进行映射,以前进程之间访问内存,现在一个进程要访问内存的时候需要进行一个中间转换的过程。为什么要有这个转换呢?
小故事分析:你小的时候,过年了你的亲戚给你发红包,你舅舅,姑姑等,把压岁钱给你,你拿着你的钱去商店买东西,但是因为你太小了,你总是会买到不适合你的东西,而且有时候会被无良商家给骗了,会出现乱花钱的情况。所以有一次你收到了500元的压岁钱。你妈妈就在你拿到压岁钱的晚上找到你说:小王,你把你的压岁钱给妈妈。比如你要买什么东西钱就给你,比如你要买笔,需要10元,我就给你10元。你妈妈说,其实和以前没有什么区别,我只是帮你保管而已。其实你是不情愿的,但是你没有办法,你只能把钱给你妈妈。你以前是你和商店老板对接的。然后你把钱给你妈妈之后。你和商店之间有一个你妈妈在中间。有一天你要去买一个20元的文件盒,你给你妈妈说你要买文具盒,你妈妈就给了你20元,她觉得你是去办正事儿。有一天你又想买一个游戏机,大概50元,然后你和你妈妈说了之后,你妈妈瞪了你一眼,你现在还在上学买什么游戏机,直接拒接了你,在这个过程中,你妈妈存在的意义是什么呢?————是为了让你在花钱的时候不在过多的犯错,你妈妈会对你要花的钱做一个判定,如果你要花的钱是一个非法的钱,你妈妈就可以提前拦截你。
所以在我们对应的软件当中,从你自己直接访问会犯错,你直接访问内存,一旦非法访问,你自己挂掉都是小事儿,你把别人影响了那问题就很大。所以有地址空间的存在,建立了一个页表映射。由虚拟地址空间到页表的映射。这个中间转化过程,可以对虚拟到物理的转换,可以进行一个系统的安全扫描,可以拦截进程的非法访问。
增加虚拟地址空间,可以让我们访存的时候增加一个转化的过程,在这个过程中,可以对我们的寻址请求进行审查,所以一旦出问题,异常访问,就可以直接拦截。所以该请求不会到达你的物理内存,保护了我们的物理内存。 - 因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合!
7.3 页表
可以把页表看成一个数组,虚拟地址可以理解为数组下标,物理地址就是值。(其实这里不太准确,以后会进行修正。初识页表可以暂时粗浅的这么理解)
- CPU内部有一个寄存器叫做CR3寄存器(X86下)这个寄存器会保存当前进程页表的起始地址。当前正在运行进程页表的地址,在这里可以找到。如果进程被切换走了。当前页表是否可以找到?
答案是一点都不担心,可以找到的。进程运行期间,进程的页表起始地址,本质上属于进程的硬件上下文,如果进程不运行了,这个信息我会一起带走,如果我重新运行,我会重新把信息带回来。就能找到我的页表起始地址。
当我们CPU读取对应地址空间地址时候,会自动帮我们做查找。
CR3寄存器中的地址是物理地址。
假设一个初始化的变量的虚拟地址是0x123456,它对应物理地址假设是0x321 ,存的值是100。
左侧就是虚拟地址,右侧就是物理地址,

我们经常发现我们访问地址,字符常量区和代码区这些段是只读的。问题是我们怎么知道我要访问的区域能被我们读取还是写入呢?或者可执行呢?
- 所以页表中还有标志位。说明了可读可写等信息。
假设我们的权限是可读可写。所以我们标志位写的就是rw,
一个代码段 的虚拟地址是0x1111 他对应的物理地址是:0x12,存放的代码是mov eax 10,页表中的标志位就是只读r 。
万一你尝试向0x12进行写入,那页表就发现这个地址是只读的,那么操作系统直接把你拦截了。页表可以给你提供很好的权限管理
代码区和字符常量区为什么是只读的?
物理内存没有只读只写这个概念,也没有权限控制概念,你想写就写你想读就读,CPU可以直接访存你自己可以加载代码到物理内存的任何地址。所以代码区和字符常量区,匹配的是虚拟地址,对应的虚拟地址的标志位是只读的,所以你对这两个地址进行写操作的时候操作系统才会拦截你不让你写入,所以你写的时候你的进程才会挂掉。
曾经讲过进程是可以被挂起的,一个进程处于阻塞状态的时候,如果操作系统内存严重不足,就会把进程的数据和代码先换出到磁盘中,那么你怎么知道你进程的代码数据在不在内存?linux内核的进程状态没有挂起态,
建立一个共识:现代操作系统几乎不做任何浪费空间和浪费时间的事情。操作系统会想尽一切办法将自己调优,任何浪费空间和时间,都会有自己的应对方式。
一个生活经验:有同学打游戏,在电脑上,比如使命召唤,或者其他特别大的游戏,你下载的最大的游戏,下载要花好长时间,下下来几十个G,你有没有想过一个问题,当前你计算机的物理内存才4G,但是你的游戏依然在你的电脑上可以运行,充分说明操作系统可以对大文件实现分批加载。不可能把几十个G全加载在内存,那不可能。所以能帮助我们加载一些大的软件。那么下面在我们的操作系统中,如果我加载了500M空间,但是我的代码是一行行跑的,短期直接只能用5M。那么剩下的495M需不需要加载到内存中呢?
把空间给你,你没有正儿八经的使用,实际上就是浪费空间和时间的行为。
- 操作系统实际加载空间的方式采用的是一个惰性加载方式。我承诺给你这么大的空间,但是你真实在物理内存使用的,是你实际用多少给你多少。页表中,虚拟地址填入,物理地址目前不填,有一个标志位(可以标志是指向磁盘。还是指向物理内存),可以标识,对应的代码和数据是否已经加载到内存。为0表示未加载,为1表示已加载。
如果你的进程的数据和代码没在内存中,那么操作系统就会报缺页中断,然后在内存中开辟一段空间,然后去磁盘找到数据和代码,然后搬到刚刚内存开辟的空间,并且把这个空间写到虚拟地址对应的物理地址的位置。
写时拷贝也是缺页中断,重新开辟空间,
创建一个进程的时候,操作系统创建PCB,创建地址空间,创建页表,,但是,对应的代码和数据,可以根本不加载你的数据和代码。当进程真正运行的时候,再给你加载,操作系统就实现了变运行变加载了。
实际上操作系统创建进程的时候,是会加载一部分代码,还要预读下你代码的格式,因为你对于的地址空间页表等填充,来自你的代码。
所以进程具有独立性,那是怎么实现的?
进程在被创建的时候,是先创建内核数据结构呢?还是先加载对应的可执行程序呢
一定先要创建内核数据结构,数据地址空间,页表,都创建好,然后再加载程序代码。
加载的时候问题来了。(1)是加载多少代码呢?(2)你申请哪部分空间呢?你是加载在哪里呢?物理内存那么大,你放在哪里呢?(3)都确定了,页表申请好之后,虚拟地址,和物理地址,对应之后,这个物理地址怎么填进去呢?什么时候填进去呢?(4)这些是谁来做呢?
答案是:操作系统来做。申请内存再进行代码加载,再填写页表,整个过程我们在未来会学习到,这个过程称为linux的内存管理模块。
而这些是进程不关心的,它也不需要关心,左侧这部分叫做进程管理。右侧在物理内存中空间的申请和释放,这部分叫做内存管理。其中我们会发现,因为有页表的存在,可以让我们的进程管理不用关心内存管理。实现了软件层面的解耦。


7.1 虚拟地址空间
同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址
本文探讨了冯诺依曼计算机体系结构的工作原理,重点介绍了操作系统概念,特别是进程的生命周期、状态(就绪、运行、阻塞和僵尸/孤儿进程)、Linux中fork函数的应用,以及环境变量的原理和传递性。此外,通过实例解析了进程创建的过程和僵尸/孤儿进程的处理方式。













515

被折叠的 条评论
为什么被折叠?



