Linux下进程以及相关概念理解(二)

简介: Linux下进程以及相关概念理解

六、进程地址空间

6.1 进程地址空间的验证


2f105165111e422689d7454fce2a989c.png

通过如下代码可以验证进程地址空间与上图一致

#include <stdio.h>                                                                   
#include <stdlib.h>
int un_val;
int init_val = 100;
int main(int argc,char* argv[],char* env[])
{
     int i = 0;
     int count = 0;
     while(env[i] != NULL && count < 5){
         printf("环境变量地址: %p\n",env[i]);
         ++count;
     }
     for(int i = 0;i < argc; ++i){
         printf("命令行参数地址: %p\n",argv[i]);
     }
     char* p1 = (char*)malloc(10);
     char* p2 = (char*)malloc(10);
     char* p3 = (char*)malloc(10);
     printf("栈区地址: %p\n",&p3);
     printf("栈区地址: %p\n",&p2);
     printf("栈区地址: %p\n",&p1);
     printf("堆区地址: %p\n",p3);
     printf("堆区地址: %p\n",p2);
     printf("堆区地址: %p\n",p1);
     printf("未初始化数据区: %p\n",&un_val);
     printf("初始化数据区: %p\n",&init_val);
     printf("代码区: %p\n",main);
     return 0;
}

f67f74d1a748457098ae15e8d6cd4f2e.png


栈区向低地址增长,堆区向高地址增长。


6.2 感知进程地址空间

通过下面一份代码我们可以发现一个问题

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>                                                                  
int val = 100;
int main()
{
    pid_t id = fork();
    if(id < 0)//err
    {
        exit(-1);
    }
    else if(id > 0)//father
    {
        sleep(3);
        printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
    }
    else//id == 0
    {
        val = 200;
        printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
    }
    return 0;
}

a100f30602a44e0ea058d36a7b8753a8.png


代码当中用fork函数创建了一个子进程,其中让子进程相将全局变量val该从100改为200后打印,而父进程先休眠3秒钟,然后再打印全局变量的值。按道理来说子进程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200。但是结果并不是,而且两个进程中val变量的地址是一样的,但是为什么打印出的结果不一致呢?


若我们是在同一个物理地址处获取的数据,那一定是相同的,而现在在同一个地址处获取到的值却不同,这只能说明我们打印出来的地址绝对不是物理地址。


实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户一概是看不到的,是由操作系统统一进行管理的。所以就算父子进程当中打印出来的全局变量的地址(虚拟地址)相同,但是两个进程当中全局变量的值却是不同的。


6.3 详细认知

进程地址空间地址大小由0x00000000到0xffffffff,且被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个区域的边界地址。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。

堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界地址。

我们生成的可执行程序实际上就被分为了各个区域(例如初始化区、未初始化区等),并且采用与Linux内核中一样的编址方式。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。而进行可执行程序的"分区"操作的实际上就是编译器,所以说代码的优化级别实际上是编译器说了算。

8de7e86cd33949f4abaa271f2d2edf8d.png



每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。


b8414f0a4fe244c6ba15781578645d4c.png


而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。体现了进程之间的独立性,这种在需要进行数据修改时才进行拷贝的技术被称为写时拷贝技术。


为什么不在创建子进程时完成数据的拷贝呢?

子进程大概率不会使用父进程中所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝。在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间


代码会不会进行写时拷贝?

大多数的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候就需要进行代码的写时拷贝。


为什么要有进程地址空间?

1.进程地址空间和页表是OS创建和管理的,凡是非法的访问或映射都会被操作系统终止,起到了保护物理内存中的所有合法数据(各个进程以及内核的相关有效数据),不会存在任何系统级别的越界问题了。

2.有了进程地址空间后,每个进程看到的都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。可以让进程以一种统一的视角看待内存,方便以统一的方式来编译和加载所有的可执行程序,简化进程本身的设计与实现。

3.有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦。


进程如何创建?

一个进程的创建伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)和页表的创建


七、进程优先级

7.1 优先级概念

优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。


7.2 优先级存在的原因

优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。


7.3 PRI与NI

PRI代表进程的优先级,即进程被CPU执行的先后顺序,该值越小进程的优先级别越高。

NI代表的是nice值,其表示进程可被执行的优先级的修正数值。

PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。

若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。

调整进程优先级,在Linux下,就是调整进程的nice值。

NI的取值范围是-20至19,一共40个级别。

注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。


7.4 修改进程nice值

7.4.1 top命令

top命令就相当于Windows操作系统中的任务管理器,它能够动态实监测系统中进程资源占用情况


使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID;输入进程PID并回车后,要求输入调整后的nice值。若想退出输入q即可。


db66e8e39fa948cd82057cf225fc9ad4.png


7.4.2 renice命令

renice + 更改的nice值 + PID

e38a6f6f972c4419a6b6fb4d1d4e436f.png



若想使用renice命令将NI值调为负值,需要root权限


八、环境变量

8.1 概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。如编写的C/C++代码,在各个目标文件进行链接的时候,从来不知道所链接的动静态库在哪里,但依然可以链接成功生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

环境变量通常具有某些特殊用途,并且在系统当中通常具有全局特性。

8.2 常见环境变量

PATH: 指定命令的搜索路径(系统命令本质也是可执行程序,但启动时不需要指定路径)

HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)

SHELL: 当前Shell,它的值通常是/bin/bash


d3a750e018754ae3b15eb373d3c0cf7b.png

8.3 环境变量相关命令

echo:显示某个环境变量的值

be1868fa5d3d4a0bb96367ab33688504.png

export:设置一个新的环境变量

4479b42110e044b99a85653596345f01.png

env: 显示所有环境变量

e2da18bf494840e0a41e7856eee078ce.png

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

unset:清除环境变量

b714699f12134e7b828b6965dbd43331.png

8.4 环境变量的组织方式

在Linux系统当中,环境变量的组织方式如下:


0b052ce1789d4a35bbbbd9bf32d48bd0.png


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


8.5 获取环境变量的方法

8.5.1 main函数参数

main函数其实有三个形参,只是平时不常使用所以没有写出来。

int main(int argc, char* argv[],char* env[])
{ …… }

main函数的第二个参数是一个字符指针数组,该数组当中的第一个字符指针存储的是可执行程序的字符串,其余字符指针存储的是所给的若干选项的字符串,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。

#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
    for(int i = 0; env[i] != NULL; ++i){
         printf("%s\n",env[i]);                                                                                                                                                 
    }
    return 0;
}

130071d9f3f54155948a6f46eec88da9.png

8.5.2 第三方变量environ

c语言给我们提供了一个全局变量environ,可以利用其访问环境表


#include <stdio.h>
int main()
{
    extern char** environ;
    for(int i = 0;environ[i] != NULL; ++i){
        printf("%s\n",environ[i]);                                                                                                                                               
    }
    return 0;
}

8.5.3 getenv函数

可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("%s\n",getenv("PATH"));                                                                                                                                               
    return 0;
}

9521a36a15fb4cdf8b888e8e40f82380.png

目录
相关文章
|
4月前
|
NoSQL Linux 编译器
GDB符号表概念和在Linux下获取符号表的方法
通过掌握这些关于GDB符号表的知识,你可以更好地管理和理解你的程序,希望这些知识可以帮助你更有效地进行调试工作。
186 16
|
4月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
117 16
|
4月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
93 20
|
3月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
75 0
|
3月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
102 0
|
3月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
66 0
|
3月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
68 0
|
6月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
|
6月前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
318 5
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能