文章目录
- 一、概述
- 二、查看进程信息
- 1. 系统文件夹 /proc
- 2. 用户级工具 ps
- 3. getpid() 函数:查看进程 PID
- 4. 用 kill 杀进程
- 5. 进程优先级
- 二、进程状态分析
- 0. +
- 1. R (running) 运行状态
- 2. S (sleeping) 休眠状态
- 3. D (disk sleep) 不可中断的休眠状态
- 4. T (stopped) 暂停状态
- 5. t (tracing stop) 追踪暂停状态
- 6. Z (zombie) 僵尸状态
- 7. X (dead) 终止状态
- 8. 孤儿进程
- 9. 一些概念
- 三、 环境变量🔺
- 1. 常见的环境变量
- 2. 有关指令
- 3. 通过代码如何获取环境变量
- 4. 通过系统调用 获取 或 设置 环境变量
- 5. 补充:命令行的 int argc 和 char *argv[]
- 四、进程地址空间🔺
- 1. 如何理解 进程地址空间
- 2. 为什么要有地址空间?
- 3. malloc 的本质?
- 4. 再谈 地址空间
-
硬件 - - 冯诺依曼计算机
- 1、CPU 不和外设直接沟通,而是和内存打交道
- 2、数据层面:外设也只会和内存打交道 软件 - - 操作系统
- 手段:对下通过管理好软硬件资源
- 目的:对上给用户提供良好(安全、稳定…)的执行环境 管理的本质:先描述,再组织
- 管理的实际是数据,用面向对象进行描述,数据结构进行组织。
一、概述
进程 = 内核关于进程的相关数据结构 // task_struct
+
当前进程的代码和数据
这个相关的数据结构就是我们通常所说的 PCB(process control block),Linux 下的 PCB 是 task_struct
比如我们输入 ./可执行程序
的时候:数据从磁盘调到内存变成进程
阻塞:就是不被调度。
阻塞一定是因为 当前进程需要等待某种资源就绪,
也一定是 进程 task _struct 结构体需要在某种被 OS 管理的资源下排队(queue)。
挂起:操作系统对阻塞的进程,为了腾出内存空间,将进程的代码和数据部分放入磁盘中,直到轮到进程被调度时,在调出代码和数据进入内存。(可以理解成一种特殊的阻塞状态)
二、查看进程信息
1. 系统文件夹 /proc
正在执行的进程,会有一个和进程 PID
同名的文件夹,存在 /proc
目录下,其中存放进程相关信息。
进程消失后,同名文件夹消失。
2. 用户级工具 ps
# 查看全部进程
ps axj
# 查看某个程序的进程
ps axj | grep [可执行程序]
# 拿 进程表头 && 某个程序的进程
ps axj | head -1 && ps ajx | grep [可执行程序]
# 拿 进程表头 && 某个程序的进程 && 去掉自己 grep 这个进程
ps axj | head -1 && ps ajx | grep [可执行程序] | grep -v grep
# 在上面的基础上,每隔一秒打印一次结果
while :; do ps axj | head -1 && ps ajx | grep [可执行程序] | grep -v grep; sleep 1; echo "----------"; done
3. getpid() 函数:查看进程 PID
函数声明:
pid_t getpid(void);
// 查看自身进程 PID- 头文件包含:
pid_t getppid(void);
// 查看父进程 PID#include <sys/types.h>
#include <unistd.h>
getpid() :当前程序运行时可以获得 自身进程 PID
getppid() :当前程序运行时可以获得 父进程 PID
pid_t 相当于一个有符号整数,返回的就是 PID 号,也是 /proc 里的文件名
🐎测试代码:
观察结果如下:
频繁多次运行发现:子进程每次进入都是新的 PID,父亲的 PPID 一直都是同一个。查看这里的 3395 为例,可知父进程是 bash
-
结论
-
- 🎯bash(命令行解释器) 也是个进程
-
- 🎯命令行启动 的 所有程序,最终都会变成进程,而该进程 对应的 父进程 都是 bash
这里有个生动案例帮助理解:
角色设定:
bash --> 媒婆
子进程 --> 媒婆实习生
说,村里阿猫阿狗太多,媒婆为了保护自己的声誉,放出他的实习生说媒,但凡某个实习生谈崩了或者被骗了,总之没处理好这活,坏掉的是这个实习生的声誉,媒婆狂喜...
同样,bash 放出 子程序,去测你写的代码,如果你的代码有问题,崩的是子程序,保护了 bash...
4. 用 kill 杀进程
除了 ctrl+C
,杀进程有专门的命令 kill
:
方法一:
kill -9 [进程PID]
方法二:
killall [可执行文件]
(如果我们不小心 bash 把他杀了,bash 会崩溃…需要重新连接一下
-
结论
-
- 🎯如何创建的子进程??
fork 之后,执行流会变成两个执行流
fork 之后,谁先运行由调度器决定
fork 之后,fork 之后的代码共享,通常我们通过 if 和 else if 进行执行流分流
- 🎯如何创建的子进程??
5. 进程优先级
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
输入 ps -l
命令可以得到的关键信息有如下内容:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的 nice 值
PRI and NI
- PRI 也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被 CPU 执行的先后顺序,此值越小 进程的优先级别越高
- 那 NI 呢?就是我们所要说的 nice 值了,其表示进程 可被执行的优先级的 修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old,即 80)+nice
- 这样,当 nice 值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在 Linux 下,就是调整进程 nice 值
- nice 其取值范围是 -20 至 19,一共 40 个级别。
PRI vs NI
- 需要强调一点的是,进程的 nice 值不是进程的优先级,他们不是一个概念,但是进程 nice 值会影响到进程的优先级变化。
- 可以理解 nice 值是进程优先级的修正修正数据
用 top
命令更改已存在进程的 nice:
- 进入 top后按 r(renice) –> 输入进程 PID –> 输入 nice 值
二、进程状态分析
task_struct 是一个结构体,内部会包含各种属性,其中就有一项是当前状态。
struct task_struct
{
int status;
//...
};
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 */
};
0. +
-
进程状态后面带 + 号
-
则说明该进程在
前台运行,可以用
ctrl + C
让程序停止。
进程状态后面没有 + 号
- 该进程在 后台运行,不能用 ctrl + C 让程序停止了。
1. R (running) 运行状态
进程只要是 R 状态,就一定是在CPU上运行吗?
事实上,进程是 R 状态并不直接代表进程在运行,而代表该进程在运行队列中排队,这个队列是由操作系统维护的。
操作系统在内存里,这个队列也在内存里被维护的。操作系统对 task_struct 的管理就是把他们放到不同的队列当中。
进程是什么状态,一般也看这个进程在哪里排队(是 task_struct 在排队,而不是代码和数据)。
运行状态 R 是瞬时状态。当进程会调用资源(如打印到显示器)时,由于 CPU 运行速度太快,我们去 ps axj 进程信息的时候,极大概率只能看到进程的其他状态,而无法捕捉到 R 状态。
2. S (sleeping) 休眠状态
S 休眠状态是 可中断休眠,本质上就是一种 阻塞状态,处于等待某种资源的状态。
3. D (disk sleep) 不可中断的休眠状态
D 是 不可中断休眠,也是阻塞状态的的一种(在做系统管理、运维、系统存储的时候才会遇到)。
面对普通的休眠状态的进程,在特殊场景下,操作系统可以做出判断并杀掉休眠进程。D 状态的休眠,则是操作系统无法杀掉的。只能等进程自己运作,或者拔掉电源…
4. T (stopped) 暂停状态
T 是 暂停状态。
用户主动使用 kill -19 操作,可以让进程进入 T 状态:
kill -19 [进程PID]
用户主动关闭 T 状态,使进程变成 R / S(后台运行 / 休眠) 状态,继续运行:
kill -18 [进程PID]
此时进程变成后台运行,无法通过 ctrl + C 的方式结束,需要输入另一个信号 kill -9:
kill -9 [进程PID]
5. t (tracing stop) 追踪暂停状态
追踪暂停,也是暂停的一种。当我们给程序打上断点并在断点处停下时,进程会显示追踪状态。
6. Z (zombie) 僵尸状态
在了解 Z 状态之前,我们先引出一个概念。
main 函数 里的 return 0,实际上是进程退出码。可以交给程序去判断,进程结束的结果是否正确。
// 进程退出码使用举例
int main()
{
// 算法省略
int result = 10;
if(result == 10)
return 0; // 正常退出
else
return 3; // 异常退出
}
查看进程退出码:
echo $?
注意:$?
只会保存最后一次执行的退出码。
僵尸状态
子进程退出后,等待后续父进程(OS)读取子进程退出的退出结果的状态。
-
僵尸进程的危害:
-
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
-
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
-
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。
-
- 会造成内存泄漏。
7. X (dead) 终止状态
终止状态,也是一个瞬时状态。当进程从 Z 状态被回收,会变成 X 终止状态,继而操作系统才会正真释放进程的所有资源。
8. 孤儿进程
孤儿进程:父进程退出,子进程会被 OS 自动领养(通过让 1 号进程成为新的父进程)。被领养的进程,就是孤儿进程
9. 一些概念
- 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
三、 环境变量🔺
环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数。
环境变量本质就是一 个 内存级 的一张表,这张表由 用户在登陆系统的时候,进行给特定用户形成属于自己的环境变量表。在系统当中通常具有全局特性,可以被子进程继承。
环境变量中的每一个,都有自己的用途:有的是进行路径查找的,有的时进行身份认证的,有的时进行动态库查找的,有的是用来进行确认当前路径…等等。每一个环境变量都有自己的特定应用场景。每一个元素都是 kv 的。
我们平时写代码中生成的可执行文件 xx,在我们需要运行它时输入的 ./xx 实际上就是这个可执行文件的路径。而众多的命令实际也是一个个可执行文件,为什么命令可以直接被读取,而我们生成的可执行文件则要带上路径呢?
分别 which 一下随便某个命令、再 which 我们的可执行文件可以发现。是因为我们的可执行文件不在 PATH 路径下。
两个解决思路,让我们输入可执行文件名 xx,就可以执行程序:
1、把我们生成的可执行文件 cp -rf 到 PATH 的路径下。
2、把可执行文件所在路径 export 到原有路径后面。
1. 常见的环境变量
PATH
: 指定命令的搜索路径HOME
: 指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)SHELL
: 当前 Shell,它的值通常是 /bin/bash。
2. 有关指令
which
:在环境变量中查找某个命令的路径
env
:输出所有 环境变量
set
:同时输出 环境变量 和 本地变量
unset [变量名]
:取消某个 本地 / 环境变量
echo $[环境变量名称]
:查看某个环境变量
export [变量名]
:设置新的 / 更新环境变量
本质上就是,把本地变量添加到环境变量表里!
(注意:如果环境变量被我们误操作不慎覆盖,导致命令无法使用,只需要重启虚拟机即可)
以 PATH 环境变量举例
------------------
# 添加路径到环境变量
export PATH = $PATH:[指定路径]
# 设置并覆盖原来的环境变量
export PATH = [指定路径]
-
环境变量
- 存在 shell 里
- 放进环境变量表
- 可以被子进程继承 普通本地变量
- 存在 shell 里
- 只能由 shell 内部调用
- 不能被子进程继承
# 设置新的环境变量,env 中可查,可以被子进程继承
export hello = 123456
# 设置普通的本地变量,env 中没有
hey = abcde
# 查看变量的值
echo $hello
echo $hey
这里要引出一个问题了:
既然我们说,本地变量只能在 shell 内部使用,不能被子进程继承,
echo 命令必然会调用子进程,子进程又是怎么访问到本地变量的呢?
这里要用 内建命令 来解释了。后续更新。
3. 通过代码如何获取环境变量
- 命令行第三个参数,就是 环境变量表
#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;
}
- 通过第三方变量
environ
获取
(libc 中定义的全局变量 environ 指向环境变量表,environ 没有包含在任何头文件中,所以在使用时 要用extern
声明。
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
其实获取环境变量最主要的是下面这种方式:
4. 通过系统调用 获取 或 设置 环境变量
putenv
getenv
常用getenv和putenv函数来访问特定的环境变量。
🌰我们模拟实现一个pwd
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* pwd = getenv("PWD");
if(pwd == NULL)
perror("geienv");
else
printf("%s\n", pwd);
return 0;
}
5. 补充:命令行的 int argc 和 char *argv[]
-
char *argv[]
- 命令行输入的
int argc
void Usage(const char *name)
{
printf("\nUsage: %s -[a|b|c]\n\n", name);
exit(0); // 终止进程
}
int main(int argc, char *argv[])
if(argc != 2) Usage(argv[0]);
if(strcmp(argv[1], "-a") == 0) printf("打印当前目录下的文件名\n");
else if(strcmp(argv[1], "-b") == 0) printf("打印当前目录下的文件的详细信息\n");
else if(strcmp(argv[1], "-c") == 0) printf("打印当前目录下的文件名(包含隐藏文件)\n");
else printf("其他功能,待开发\n");
return 0;
}
四、进程地址空间🔺
先看如下这个测试:
🐎测试代码:
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
int g_val = 100;
int main()
{
pid_t id = fork();
assert(id >= 0);
else if(id == 0) //child
{
while(1)
{
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
g_val++;
sleep(1);
}
}
else //parent
{
while(1)
{
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
测试结果:
parent[2995]: 100 : 0x80497d8
child[2996]: 100 : 0x80497d8
parent[2995]: 100 : 0x80497d8
child[2996]: 101 : 0x80497d8
parent[2995]: 100 : 0x80497d8
child[2996]: 102 : 0x80497d8
parent[2995]: 100 : 0x80497d8
child[2996]: 103 : 0x80497d8
我们已经知道的是:
- 子进程对全局数据修改,并不影响父进程。进程具有独立性!
进而可以发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在 Linux 地址下,这种地址叫做 虚拟地址 / 线性地址
- 我们在用 C/C++ 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
OS 必须负责将 虚拟地址 转化成 物理地址。
1. 如何理解 进程地址空间
进程地址空间,本质上也是一个内核数据结构,struct mm_struct{};
2. 为什么要有地址空间?
- 防止地址随意访问,保护物理内存与其他进程。
- 地址空间 和 物理内存 之间 由 页表 连接,这张表不止简单的定义了映射关系,还为用户设置了各种区域的不同权限。
- 例如,我们知道 char* str = “hello”; 如果我们去编译 str = ‘H’; 肯定是不能通过的,因为我们通过 页表 所映射的 “hello” 所存入的物理内存的常量区,在 key 被设置为只读!我们平时说的 代码是只读的,也是这个道理。
3. malloc 的本质?
- 当我们像 OS 申请内存时,操作系统是立马给我们这个地址,还是需要的时候再给我们?
- 首先,OS 一般不允许任何的浪费不高效。
- 其次,申请内存不一定立马使用。
- 什么意思呢?在我们申请成功之后,使用之前,就有一段小小的时间窗口(这个空间没有被正常使用,但别人用不了),这块空间处于闲置状态?这个不高效的行文,OS 就不允许的!
- 于是,我们申请成功后,地址空间给我们一个地址,连接页表的 key(此前是 进程管理),而 value 也是没有映射存在的,即此时没有开辟空间。
- 只有当我们调用或者访问内存,OS 发现没有相应的数据,才会给我们把数据换入。(这里是内存管理)
- 这个现象叫做 缺页中断,也是一个典型的 解耦合
4. 再谈 地址空间
所以,为什么要有地址空间:
- 防止地址随意访问,保护物理内存与其他进程。
- 将 进程管理 和 内存管理 进行 解耦合
- 更重要的是,可以让进程以统一的视角,看待自己的代码和数据
(扩展)解释第三点:
-
我们的程序在被编译,还没有被加载到内存的时候,程序内部也存在地址!
-
编译器编译可执行程序时,本来就是按照 虚拟地址空间 的方式、各种内存布局来编译的,在磁盘上已经给我们规定好了代码区、已初始化数据区…等等这样的概念。编译时只需要进行模块式的加载,进行对应的映射到内存,则有了物理地址。而代码彼此之间是使用虚拟地址互相跳转的。所以说,程序在没加载到内存的时候就有了地址,这个地址是虚拟地址。
-
比如,我们写一个函数,再去调用它,在反汇编中查看能看到他们的地址,这个地址就是 程序自拟的 虚拟地址。
- 可以看到的是,地址空间约束的不光是 OS,我们的编译器也需要遵守这样的规则。
- 所以一个可执行程序加载到内存,是拥有两套地址的。
最后一个问题:
进程和代码必须一直在内存中?
不一定!实际上,我们拥有了虚拟地址空间,我们的代码是可以边加载边执行的。需要的数据才加载,这样就可以保证我们在内存使用量很低的情况下,还完成了大软件的运行。
🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~