三、进程状态
3.1 准备知识
进程阻塞:进程因为等待某种条件就绪,而导致的一种不推进的状态(例如进程卡顿),因而阻塞一定是在等待某种资源。为什么阻塞?进程需要通过等待的方式,等具体的资源被别人使用结束后,再被自己使用,因而阻塞就是进程等待某种资源就绪的过程。这些资源可以是磁盘、网卡、显卡和键盘等各种外设。而等待的过程需要考虑大量的进程,这些进程需要被管理(先描述,再组织,详见下图及模拟结构)。
//将外设抽象成数据结构
struct dev{
struct task_struct* queue;//pcb可以被维护在不同的队列中
//dev其他所有的属性
}
因而,进程阻塞就是不被调度, 一定因为当前进程需要等待某种资源就绪,一定是进程task_struct结构体需要在某种被OS管理的资源下排队,图2所示。
3.2 进程状态内核源码
为了理解正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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运行状态,并不意味着进程一定在运行中,它表明进程要么在运行中,要么在运行队列里;
- S休眠状态,意味着进程在等待时间完成,可中断休眠;
- D休眠状态,不可中断休眠,这个状态的进程通常会等待IO的结束;
- T暂停状态,可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行;
- t追踪状态
- X死亡状态,这个状态只是一个返回状态,用户不会在任务列表里看到这个状态
- Z僵尸状态
孤儿进程:父进程先退出,子进程就被称为孤儿进程。此外,孤儿进程被1号init进程领养,当然要有init进程回收。
3.3 进程状态查看
进程只要是R状态,不一定正在CPU上运行。进程有自己的运行队列 ,task_struct是一个结构体,内部会包含各种属性,包含状态。
//案例1--查看进程状态
#include<stdio.h>
int main(){
while(1){
//printf()就是向外设打印消息
printf("我在运行码??\n");
}
return 0;
}
例如我们常用的VS2019等编译软件中,在debug模式下,断点调试就相当于将进程(任务)处于暂停状态。此外,我们为什么创建进程?因为我们要让进程帮我们办事,我们关心它的结果 。
命令echo $?
,可获取进程的退出码:
如果一个进程退出了,立马X状态
,立马退出,作为父进程,有没有机会拿到退出的结果?详见下列代码及结果。
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(){
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);
}
}
return 0;
}
在图6中,通过指令kill -9 8961
杀死子进程后,子进程不会立即彻底退出,而是维持Z状态
(僵尸状态),以方便后续父进程(操作系统)读取该子进程退出的退出结果。
3.4 僵尸进程危害
- 进程的退出状态必须被维持下去,因为它要告知父进程,父进程交待的任务,完成得怎么样了。父进程若一直不读取,那么子进程就会一直处于僵死状态;
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,僵尸状态一直不退出,PCB就要一直维护;
- 若一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存资源(例如C语言中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间);
- 此外,过多的僵尸进程会引起内存泄漏。
四、环境变量
4.1 基本概念
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。例如,我们在编写C/C++代码的时候,在代码链接时,从来不知道我们所链接的动静态库在哪里,但是照样可以链接成功,生成可执行程序,因为相关环境变量帮助编译器进行查找的。环境变量在系统中通常具有全局性。
4.1.1 常见环境变量
- PATH:指定命令的搜索路径;
- HOME:指定用户的主工作目录(即用户登录到Linux系统中时,默认的目录);
- SHELL:当前Shell,它的值通常是/bin/bash。
4.1.2 PATH环境变量
Linux系统中导入环境变量的方法
export PATH=$PATH:地址
注意:在Linux中,把可执行程序拷贝到系统默认路径下,让我们可以直接访问的方式 相当于Linux软件下的安装如cp -rf "可执行程序名" /usr/bin
,删除则可以使用rm /usr/bin/"可执行程序名"
。
查看环境变量的方法:
echo $NAME //NAME,用户的环境变量名称
查看系统环境变量直接使用命令env
:
4.2 环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以\0
结尾的环境字符串。
4.3 通过代码获取环境变量
1、命令行第三个参数:
int main(int argc, char *argv[], char *envp[]){
for(int i = 0; envp[i]; i++){
printf("envp[%d] -> %s\n", i, envp[i]);
}
return 0;
}
打印该进程的环境变量表:
2、通过第三方变量environ获取
int main(){
extern char **environ;
for(int i = 0; environ[i]; i++){
printf("environ[i] -> %s\n", i ,environ[i]);
}
}
3、 通过系统调用获取或设置环境变量
使用getenv()
来访问特定的环境变量:
int main(){
char *user = getenv("USER");
if(user == NULL){
perror("USER");
}else{
printf("USER: %s\n",user);
}
return 0;
}
4.4 实验
1、模拟Linux中用户的判别:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define NAME "ChuHsiang"
int main(){
char *own = getenv("USER");
if(strcmp(own,NAME) == 0){
printf("这个程序已经执行啦...\n");
}else{
printf("当前用户%s是一个非法用户, 无法执行\n", own);
}
return 0;
}
环境变量本质就是一个内存级的一张表(环境变量对应的数据是从系统的相关配置文件中读取的,例如.bash_profile
和.bashrc
),这张表由用户在登录系统的时候,进行给特定用户形成属于自己的环境变量表;环境变量中每一个,都有自己的用途:有的是进行路径查找,有的是进行身份认证,有的是动态库查找,有的是进行确认当前用户路径等等;每一个环境变量都有自己的特定应用场景,每一个元素都是KV结构。
扩展:
命令行式自定义变量,shell本地变量,不能被环境变量继承
命令行式自定义变量导入环境变量
int main(){
printf("myenv: %s\n", getenv("hello"));
return 0;
}
int main(int argc, char *argv[]){
//char *argv[]
for(int i = 0; i < argc; i++){
printf("argv[%d] -> %s\n", i, argv[i]);
}
return 0;
}
bash制作这个表
命令行参数
void Usage(const char *name){
printf("\nUsage: %s -[a|b|c]\n", name);
exit(0);//终止进程
}
int main(int argc, char *argv[]){
//eg ./myproc arg
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;
}
4.5 进程的优先级
4.5.1 基本概念
- CPU资源分配的先后顺序,就是指进程的优先权;
- 优先权高的进程有优先执行的权力。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能;
- 还可以将进程运行到指定的CPU上,这样一来,将不重要的进程安排到某个CPU,可以大大改善系统整体性能。
对于优先级和权限,我们通常会有一定的误解;权限代表能或不能;优先级已经能,但是谁先谁后的问题。
为什么有优先级?因为CPU资源有限,而进程个数较多。
4.5.2 查看系统进程
在Linux中,使用ps -l
命令则会输出下图内容:
上图中,我们注意到几个重要信息:
- UID:表示执行者身份;
- PID:表示这个进程的代号;
- PPID:父进程的代号;
- PRI:表示这个进程可被执行的优先级,其值越小越早被执行;
- NI:表示这个进程的nice值。
PRI和NI的理解
- PRI进程的优先级,即程序被CPU执行的先后顺序,此值越小进程的优先级越高;
- NI即nice,表示进程可被执行的优先级的修正数值;
- PRI值越小越快被执行,加入nice值后,PRI变为:
PRI(new)=PRI(old)+nice
- nice取值范围-20~19,一共40个级别;
注意:进程的nice值不是进程的优先级,两者不是同一个概念,但进程nice值会影响到进程优先级的变化;可以理解nice值是进程优先级的修正数据。
4.5.3 查看进程优先级的命令
用top命令更改已存在进程的nice值:
top
- 进入top后,按
r
->输入进程PID->输入nice值
五、进程地址空间
5.1 地址空间知识回顾
64位计算机相较于32位计算机复杂了些,但原理基本相似,本文以32位平台为基础,后续不再重复叙述。图17所示32位平台的程序地址空间布局图是我们在计算机相关课程学习到的,但对其原理知之甚少。例如为什么地址空间是4GB大小?因为2^32bit = 4 * 2^10 * 2^ 10 * 2^10bit = 4 * 2^10 * 2^10Byte = 4 * 2^10 MB = 4 GB
。
地址空间是内核的数据结构,详如:
struct mm_struct //4GB
{
long code_start;
long code_end;
long init_start;
long init_end;
...
long brk_start;
long brk_end;
long stack_start;
long stack_end;
};
//如果限定了区域那么区域之间的数据 叫虚拟地址或线性地址
代码区:存放程序的代码,即CPU执行的机器指令,并且只读;
堆区:由程序员调用malloc等函数来主动申请,需要free()函数来释放内存;申请堆区内存后,忘记释放容易造成内存泄漏;
区域划分:对线性区域进行指定start和end即可完成区域划分,地址空间本质就是一个线性区域。
含有子进程的进程的地址空间实验
#include<stdio.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
//全局变量
int g_value = 100;
int main(){
pid_t id = fork();
//fork()在返回的时候,父子都有了,return两次,id不是是pid_t类型的变量,返回本质就是写入
//谁先返回,就让OS发生写时拷贝
assert(id >= 0);
if(id == 0){
//子进程
while(1){
printf("我是子进程, 我的ID是: %d, 我的父进程是: %d, g_value: %d, &g_value: %p\n", getpid(), getppid(), g_value++, &g_value);
sleep(1);
}
}else{
//父进程
while(1){
printf("我是父进程, 我的ID是: %d, 我的父进程是: %d, g_value: %d, &g_value: %p\n", getpid(), getppid(), g_value ,&g_value);
sleep(1);
}
}
return 0;
}
上图中,我们发现:输出的变量g_value
的地址一模一样,但变量的内容不一样。可知子进程对全局数据修改,并不影响父进程,因而进程具有独立性(由于写时拷贝);这里内存地址为0x60105c不是物理地址,一般称之为虚拟地址或线性地址;我们在用C/C++语言所看到的地址,全部是虚拟地址,物理地址用户一概无法看到,其由操作系统统一管理。操作系统负责将虚拟地址
转化成物理地址
。
注意:物理内存不存在读取同一个变量的地址,会读到不同的数值。进程 = 内核数据结构 + 代码和数据
5.2 进程地址空间
上图可知:
-
数据和代码真正只能存在内存中;
-
我们直接用的是虚拟地址;
-
找到地址不是目的(是手段),该地址多对应的内容。
5.2.1 地址空间为什么要存在?
- 防止地址随意访问,保护物理内存与其他进程;
- 将进程管理和内存管理进行解耦合;
- 可以让进程以统一的视角,看待自己的代码和数据
5.2.2 malloc的本质
-
向操作系统申请内存,OS不会立马给用户,在用户需要的时候才给 --> 因为操作系统不允许任何浪费(或不高效的行为);
-
在用户申请成功之后,在使用之前,有一小段的时间窗口,这个空间没有被正常使用,但是别人用不了(闲置状态) 缺页状态。
5.2.3 重新理解地址空间
-
我们程序在没有被编译的时候,没有被加载到内存,我们程序内部是有地址的
-
源代码被编译的时候,就是按照虚拟地址空间的方式进行对代码和数据早就已经编好了对应的编制
-
虚拟地址空间不仅仅会影响操作系统,还会让编译器遵守它的规则(ELF格式)