进程 = 内核关于进程的相关数据结构 // task_struct
+
当前进程的代码和数据
这个相关的数据结构就是我们通常所说的 PCB(process control block),Linux 下的 PCB 是 task_struct
比如我们输入 ./可执行程序
的时候:数据从磁盘调到内存变成进程
阻塞:就是不被调度。
阻塞一定是因为 当前进程需要等待某种资源就绪,
也一定是 进程 task _struct 结构体需要在某种被 OS 管理的资源下排队(queue)。
挂起:操作系统对阻塞的进程,为了腾出内存空间,将进程的代码和数据部分放入磁盘中,直到轮到进程被调度时,在调出代码和数据进入内存。(可以理解成一种特殊的阻塞状态)
正在执行的进程,会有一个和进程 PID
同名的文件夹,存在 /proc
目录下,其中存放进程相关信息。
进程消失后,同名文件夹消失。
# 查看全部进程
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
函数声明:
pid_t getpid(void);
// 查看自身进程 PID- 头文件包含:
pid_t getppid(void);
// 查看父进程 PID#include
#include
getpid() :当前程序运行时可以获得 自身进程 PID
getppid() :当前程序运行时可以获得 父进程 PID
pid_t 相当于一个有符号整数,返回的就是 PID 号,也是 /proc 里的文件名
测试代码:
观察结果如下:
频繁多次运行发现:子进程每次进入都是新的 PID,父亲的 PPID 一直都是同一个。查看这里的 3395 为例,可知父进程是 bash
这里有个生动案例帮助理解:
角色设定:
bash --> 媒婆
子进程 --> 媒婆实习生
说,村里阿猫阿狗太多,媒婆为了保护自己的声誉,放出他的实习生说媒,但凡某个实习生谈崩了或者被骗了,总之没处理好这活,坏掉的是这个实习生的声誉,媒婆狂喜...
同样,bash 放出 子程序,去测你写的代码,如果你的代码有问题,崩的是子程序,保护了 bash...
除了 ctrl+C
,杀进程有专门的命令 kill
:
方法一:
kill -9 [进程PID]
方法二:
killall [可执行文件]
(如果我们不小心 bash 把他杀了,bash 会崩溃…需要重新连接一下
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
输入 ps -l
命令可以得到的关键信息有如下内容:
PRI and NI
PRI vs NI
用 top
命令更改已存在进程的 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 */
};
ctrl + C
让程序停止。
进程只要是 R 状态,就一定是在CPU上运行吗?
事实上,进程是 R 状态并不直接代表进程在运行,而代表该进程在运行队列中排队,这个队列是由操作系统维护的。
操作系统在内存里,这个队列也在内存里被维护的。操作系统对 task_struct 的管理就是把他们放到不同的队列当中。
进程是什么状态,一般也看这个进程在哪里排队(是 task_struct 在排队,而不是代码和数据)。
运行状态 R 是瞬时状态。当进程会调用资源(如打印到显示器)时,由于 CPU 运行速度太快,我们去 ps axj 进程信息的时候,极大概率只能看到进程的其他状态,而无法捕捉到 R 状态。
S 休眠状态是 可中断休眠,本质上就是一种 阻塞状态,处于等待某种资源的状态。
D 是 不可中断休眠,也是阻塞状态的的一种(在做系统管理、运维、系统存储的时候才会遇到)。
面对普通的休眠状态的进程,在特殊场景下,操作系统可以做出判断并杀掉休眠进程。D 状态的休眠,则是操作系统无法杀掉的。只能等进程自己运作,或者拔掉电源…
T 是 暂停状态。
用户主动使用 kill -19 操作,可以让进程进入 T 状态:
kill -19 [进程PID]
用户主动关闭 T 状态,使进程变成 R / S(后台运行 / 休眠) 状态,继续运行:
kill -18 [进程PID]
此时进程变成后台运行,无法通过 ctrl + C 的方式结束,需要输入另一个信号 kill -9:
kill -9 [进程PID]
追踪暂停,也是暂停的一种。当我们给程序打上断点并在断点处停下时,进程会显示追踪状态。
在了解 Z 状态之前,我们先引出一个概念。
main 函数 里的 return 0,实际上是进程退出码。可以交给程序去判断,进程结束的结果是否正确。
// 进程退出码使用举例
int main()
{
// 算法省略
int result = 10;
if(result == 10)
return 0; // 正常退出
else
return 3; // 异常退出
}
查看进程退出码:
echo $?
注意:$?
只会保存最后一次执行的退出码。
僵尸状态
子进程退出后,等待后续父进程(OS)读取子进程退出的退出结果的状态。
终止状态,也是一个瞬时状态。当进程从 Z 状态被回收,会变成 X 终止状态,继而操作系统才会正真释放进程的所有资源。
孤儿进程:父进程退出,子进程会被 OS 自动领养(通过让 1 号进程成为新的父进程)。被领养的进程,就是孤儿进程
环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数。
环境变量本质就是一 个 内存级 的一张表,这张表由 用户在登陆系统的时候,进行给特定用户形成属于自己的环境变量表。在系统当中通常具有全局特性,可以被子进程继承。
环境变量中的每一个,都有自己的用途:有的是进行路径查找的,有的时进行身份认证的,有的时进行动态库查找的,有的是用来进行确认当前路径…等等。每一个环境变量都有自己的特定应用场景。每一个元素都是 kv 的。
我们平时写代码中生成的可执行文件 xx,在我们需要运行它时输入的 ./xx 实际上就是这个可执行文件的路径。而众多的命令实际也是一个个可执行文件,为什么命令可以直接被读取,而我们生成的可执行文件则要带上路径呢?
分别 which 一下随便某个命令、再 which 我们的可执行文件可以发现。是因为我们的可执行文件不在 PATH 路径下。
两个解决思路,让我们输入可执行文件名 xx,就可以执行程序:
1、把我们生成的可执行文件 cp -rf 到 PATH 的路径下。
2、把可执行文件所在路径 export 到原有路径后面。
PATH
: 指定命令的搜索路径HOME
: 指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)SHELL
: 当前 Shell,它的值通常是 /bin/bash。which
:在环境变量中查找某个命令的路径
env
:输出所有 环境变量
set
:同时输出 环境变量 和 本地变量
unset [变量名]
:取消某个 本地 / 环境变量
echo $[环境变量名称]
:查看某个环境变量
export [变量名]
:设置新的 / 更新环境变量
本质上就是,把本地变量添加到环境变量表里!
(注意:如果环境变量被我们误操作不慎覆盖,导致命令无法使用,只需要重启虚拟机即可)
以 PATH 环境变量举例
------------------
# 添加路径到环境变量
export PATH = $PATH:[指定路径]
# 设置并覆盖原来的环境变量
export PATH = [指定路径]
# 设置新的环境变量,env 中可查,可以被子进程继承
export hello = 123456
# 设置普通的本地变量,env 中没有
hey = abcde
# 查看变量的值
echo $hello
echo $hey
这里要引出一个问题了:
既然我们说,本地变量只能在 shell 内部使用,不能被子进程继承,
echo 命令必然会调用子进程,子进程又是怎么访问到本地变量的呢?
这里要用 内建命令 来解释了。后续更新。
#include
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++)
{
printf("%s\n", env[i]);
}
return 0;
}
environ
获取extern
声明。#include
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
其实获取环境变量最主要的是下面这种方式:
putenv
getenv
常用getenv和putenv函数来访问特定的环境变量。
我们模拟实现一个pwd
#include
#include
int main()
{
char* pwd = getenv("PWD");
if(pwd == NULL)
perror("geienv");
else
printf("%s\n", pwd);
return 0;
}
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
#include
#include
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
我们已经知道的是:
进而可以发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
OS 必须负责将 虚拟地址 转化成 物理地址。
进程地址空间,本质上也是一个内核数据结构,struct mm_struct{};
所以,为什么要有地址空间:
- 防止地址随意访问,保护物理内存与其他进程。
- 将 进程管理 和 内存管理 进行 解耦合
- 更重要的是,可以让进程以统一的视角,看待自己的代码和数据
(扩展)解释第三点:
我们的程序在被编译,还没有被加载到内存的时候,程序内部也存在地址!
编译器编译可执行程序时,本来就是按照 虚拟地址空间 的方式、各种内存布局来编译的,在磁盘上已经给我们规定好了代码区、已初始化数据区…等等这样的概念。编译时只需要进行模块式的加载,进行对应的映射到内存,则有了物理地址。而代码彼此之间是使用虚拟地址互相跳转的。所以说,程序在没加载到内存的时候就有了地址,这个地址是虚拟地址。
比如,我们写一个函数,再去调用它,在反汇编中查看能看到他们的地址,这个地址就是 程序自拟的 虚拟地址。
最后一个问题:
进程和代码必须一直在内存中?
不一定!实际上,我们拥有了虚拟地址空间,我们的代码是可以边加载边执行的。需要的数据才加载,这样就可以保证我们在内存使用量很低的情况下,还完成了大软件的运行。
链接在此
如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~