【Linux系统编程】日积月累——进程(2)

三、进程状态

3.1 准备知识

  进程阻塞:进程因为等待某种条件就绪,而导致的一种不推进的状态(例如进程卡顿),因而阻塞一定是在等待某种资源。为什么阻塞?进程需要通过等待的方式,等具体的资源被别人使用结束后,再被自己使用,因而阻塞就是进程等待某种资源就绪的过程。这些资源可以是磁盘、网卡、显卡和键盘等各种外设。而等待的过程需要考虑大量的进程,这些进程需要被管理(先描述,再组织,详见下图及模拟结构)。

c++ 复制代码
//将外设抽象成数据结构
struct dev{
	struct task_struct* queue;//pcb可以被维护在不同的队列中
    //dev其他所有的属性
    
}


图3-1 进程资源等待

  因而,进程阻塞就是不被调度, 一定因为当前进程需要等待某种资源就绪,一定是进程task_struct结构体需要在某种被OS管理的资源下排队,图3-2所示。


图3-2 进程阻塞

3.2 进程状态内核源码

  为了理解正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时也叫任务)。下列代码是状态在kernel源代码里的定义:

c++ 复制代码
/*
* 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是一个结构体,内部会包含各种属性,包含状态。


图3-3 进程运行状态

c 复制代码
//案例1--查看进程状态
#include<stdio.h>
int main(){
    while(1){
        //printf()就是向外设打印消息
        printf("我在运行码??\n");
    }
    return 0;
}


图3-4 进程状态查看

  例如我们常用的VS2019等编译软件中,在debug模式下,断点调试就相当于将进程(任务)处于暂停状态。此外,我们为什么创建进程?因为我们要让进程帮我们办事,我们关心它的结果 。

  命令echo $?,可获取进程的退出码:


图3-5 进程状态查看

  如果一个进程退出了,立马X状态,立马退出,作为父进程,有没有机会拿到退出的结果?详见下列代码及结果。

c 复制代码
#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;
}


图3-6 进程状态查看

  在图3-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系统中导入环境变量的方法

c 复制代码
export PATH=$PATH:地址

注意 :在Linux中,把可执行程序拷贝到系统默认路径下,让我们可以直接访问的方式 相当于Linux软件下的安装如cp -rf "可执行程序名" /usr/bin,删除则可以使用rm /usr/bin/"可执行程序名"。

查看环境变量的方法:

c 复制代码
echo $NAME //NAME,用户的环境变量名称

查看系统环境变量直接使用命令env:


图4-1 系统环境变量

4.2 环境变量的组织方式

  每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以\0结尾的环境字符串。


图4-2 环境变量表的组织方式

4.3 通过代码获取环境变量

1、命令行第三个参数:

c 复制代码
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;
 }

打印该进程的环境变量表:


图4-3 进程环境变量表

2、通过第三方变量environ获取

c 复制代码
int main(){
    extern char **environ;
    for(int i = 0; environ[i]; i++){
        printf("environ[i] -> %s\n", i ,environ[i]);
    }
}


图4-4 第三方变量获取进程环境变量

3、 通过系统调用获取或设置环境变量

使用getenv()来访问特定的环境变量:


图4-5 getenv()函数使用方法

c 复制代码
int main(){
    char *user = getenv("USER");
    if(user == NULL){
        perror("USER");
    }else{
        printf("USER: %s\n",user);
    }
    return 0;
}


图4-6 系统函数调用获取环境变量

4.4 实验

1、模拟Linux中用户的判别:

c 复制代码
#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;
}


图4-7 系统指令模拟

  环境变量本质就是一个内存级的一张表(环境变量对应的数据是从系统的相关配置文件中读取的,例如.bash_profile和.bashrc),这张表由用户在登录系统的时候,进行给特定用户形成属于自己的环境变量表;环境变量中每一个,都有自己的用途:有的是进行路径查找,有的是进行身份认证,有的是动态库查找,有的是进行确认当前用户路径等等;每一个环境变量都有自己的特定应用场景,每一个元素都是KV结构。

扩展:

命令行式自定义变量,shell本地变量,不能被环境变量继承

命令行式自定义变量导入环境变量

c 复制代码
int main(){
 	printf("myenv: %s\n", getenv("hello"));
 	return 0;
}
c 复制代码
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制作这个表


图4-8 进程模拟系统指令

命令行参数

c 复制代码
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-9 进程模拟系统指令结果

4.5 进程的优先级

4.5.1 基本概念

  • CPU资源分配的先后顺序,就是指进程的优先权;
  • 优先权高的进程有优先执行的权力。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能;
  • 还可以将进程运行到指定的CPU上,这样一来,将不重要的进程安排到某个CPU,可以大大改善系统整体性能。

  对于优先级和权限,我们通常会有一定的误解;权限代表能或不能;优先级已经能,但是谁先谁后的问题。

  为什么有优先级?因为CPU资源有限,而进程个数较多。

4.5.2 查看系统进程

在Linux中,使用ps -l命令则会输出下图内容:


图4-10 系统进程

上图中,我们注意到几个重要信息:

  • 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 。


图5-1 地址空间结构图

地址空间是内核的数据结构,详如:

c 复制代码
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即可完成区域划分,地址空间本质就是一个线性区域。

含有子进程的进程的地址空间实验

c 复制代码
#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;
}


图5-2 进程地址空间实验

  上图中,我们发现:输出的变量g_value的地址一模一样,但变量的内容不一样。可知子进程对全局数据修改,并不影响父进程,因而进程具有独立性(由于写时拷贝) ;这里内存地址为0x60105c不是物理地址,一般称之为虚拟地址 或线性地址 ;我们在用C/C++语言所看到的地址,全部是虚拟地址,物理地址用户一概无法看到,其由操作系统统一管理。操作系统负责将虚拟地址转化成物理地址。

注意:物理内存不存在读取同一个变量的地址,会读到不同的数值。进程 = 内核数据结构 + 代码和数据

5.2 进程地址空间


图5-3 页表及地址空间

上图可知:

  • 数据和代码真正只能存在内存中;

  • 我们直接用的是虚拟地址;

  • 找到地址不是目的(是手段),该地址多对应的内容。

5.2.1 地址空间为什么要存在?

  • 防止地址随意访问,保护物理内存与其他进程;
  • 将进程管理和内存管理进行解耦合;
  • 可以让进程以统一的视角,看待自己的代码和数据

5.2.2 malloc的本质

  • 向操作系统申请内存,OS不会立马给用户,在用户需要的时候才给 --> 因为操作系统不允许任何浪费(或不高效的行为);

  • 在用户申请成功之后,在使用之前,有一小段的时间窗口,这个空间没有被正常使用,但是别人用不了(闲置状态) 缺页状态。

5.2.3 重新理解地址空间

  • 我们程序在没有被编译的时候,没有被加载到内存,我们程序内部是有地址的

  • 源代码被编译的时候,就是按照虚拟地址空间的方式进行对代码和数据早就已经编好了对应的编制

  • 虚拟地址空间不仅仅会影响操作系统,还会让编译器遵守它的规则(ELF格式)

相关推荐
且从容.5 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道5 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.8 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落10 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨10 小时前
WSL2的安装与使用
linux·wsl
微小的鱼10 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng10 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮11 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky1 天前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默1 天前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu