进程优先级
1.基本概念
进程优先级是操作系统中用于调度进程执行的重要概念。每个进程都有一个相应的优先级,它决定了进程在多任务环境下获得 CPU 时间片的优先级别。进程优先级的调整可以影响进程的执行顺序,从而对系统的性能和响应性产生影响。
在多任务操作系统中,CPU 时间被划分为小的时间片,并按照一定的调度算法分配给不同的进程。调度算法根据进程的优先级决定在给定时刻应该运行哪个进程。较高优先级的进程在调度时会优先获得 CPU 时间,而较低优先级的进程则可能需要等待更长时间才能得到执行的机会。
进程优先级通常由操作系统根据一些规则来设置和调整。一些常见的优先级调整规则包括:
1.静态优先级:在进程创建时指定,通常由程序员或系统管理员指定,表示进程的固定优先级。
2.动态优先级:根据进程的行为和资源使用情况动态调整。比如,某些操作系统可能会根据进程的 CPU 使用时间、I/O 请求等来动态调整优先级,以实现公平的资源分配。
3.实时优先级:适用于实时系统,用于确保特定任务的响应时间。实时进程通常具有较高的优先级,以保证其能够及时响应特定事件。
进程优先级的合理调整可以对系统的性能和响应时间进行优化。例如,在响应用户交互的操作时,可以将用户界面相关的进程设置为较高优先级,确保系统对用户的操作能够及时响应。而对于一些资源密集型任务,可以将其优先级设置较低,以免其占用过多的 CPU 时间影响其他任务的执行。
需要注意的是,优先级调整需要慎重进行,过高的优先级可能会导致其他任务得不到足够的执行时间,而过低的优先级可能会导致某些任务无法及时响应,影响系统的整体性能。因此,在进行优先级调整时,需要仔细考虑不同任务的重要性和资源需求,以平衡系统性能和资源利用率。
2.查看系统进程优先级
首先输入指令ps -l
我们很容易注意到其中的几个重要信息:
名称 | 作用 |
UID | 代表执行者的身份 |
PID | 代表这个进程的代号 |
PPID | 代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号 |
PRI | 代表这个进程可被执行的优先级,其值越小越早被执行 |
NI | 代表这个进程的nice值 |
3.PRI和NI
在Linux系统中,优先级由pri和nice值共同确定(优先级的数值越小,优先级越高;优先级的数值越大,优先级越低)
PRI(Priority):表示进程的静态优先级,是一个整数值,基准默认值为80,默认范围是 60 到 99(普通进程而言)。数值越小,优先级越高。负数表示高优先级,正数表示低优先级。对于普通用户创建的进程,默认的优先级为 0。如果进程的优先级值是负数,表示它是实时进程。
NI(Nice Value):表示进程的动态优先级,也是一个整数值,在-20到+19之间。数值越大,优先级越低。NI 值可以在运行时通过用户或管理员调整来影响进程的调度优先级。
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进
程的优先级变化,可以理解nice值是进程优先级的修正修正数据
示例:
首先我们生成并运行一个死循环的可执行程序,查看其PID
第二步进入top后按“r”–>输入进程PID–>输入nice值
退出查看PRI
可以看到我们输入的100,但是普通我们最多加到99,提升优先级也是一样,最低只能到60,但需要注意的是,不管在什么时候进行加减,都是在基准PRI(80)上进行加减,且提升优先级(即NI为负值)需要root权限。
环境变量
1.环境变量的基本概念
**环境变量(environment variables)**是在操作系统中用于配置程序运行环境的一种机制。它是一些由操作系统或用户定义的键值对(Key-Value pairs),其中键表示环境变量的名称,而值表示环境变量的内容。环境变量通常在操作系统的全局范围内生效,被用于存储程序运行所需的配置信息、路径、语言设置、系统资源等。
1.环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数
2.比如我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
3.环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
环境变量的优点在于它们可以用于传递配置信息给程序,而无需修改程序的源代码。这使得程序更加灵活,可以在不同的环境中运行而不需要重新编译。许多程序和操作系统本身都使用环境变量来配置其行为和设置。例如,PATH 环境变量用于指定系统的可执行程序所在的路径,使得用户可以在终端中直接运行程序而不需要指定绝对路径。
2.常见环境变量及查看环境变量方法
常见的环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。
查看环境变量
echo $环境变量名称
在我们创建一个可执行文件时,为什么还要加./才能运行,而其他指令和程序却不需要,那是因为我们所处的路径不在环境变量PATH中,当我们输入指令export PATH=$PATH:程序所在路径时,就将程序所在路径添加到了PATH中,这样就可以不带./就能直接运行了。
3.环境变量相关的命令及组织方式
命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
4.通过代码获取环境变量
①main函数第三个参数
首先我们先介绍前两个参数int argc, char *argv[]
其中:
argc(Argument Count):表示命令行参数的数量,包括程序本身。即 argc 的值至少为 1。
argv(Argument Vector):是一个指向指针的数组,每个指针指向一个表示命令行参数的字符串。
比如下面的代码:
#include <stdio.h> int main(int argc, char *argv[]) { printf("Number of command-line arguments: %d\n", argc); for (int i = 0; i < argc; i++) { printf("Argument %d: %s\n", i, argv[i]); } return 0; }
运行时我们输入命令./program arg1 arg2 arg3 输出将会是:
Number of command-line arguments: 4 Argument 0: ./program Argument 1: arg1 Argument 2: arg2 Argument 3: arg3
下面我们看第三个参数char *env[]
env(Environment Variable):它一个指向指针的数组,每个指针指向一个环境变量字符串。
通过 env 参数,程序可以获取和修改当前进程的环境变量。环境变量通常用于设置程序的配置信息、路径等。在一些特定的编程语言和操作系统中,main 函数支持这样的参数形式。
以下是一个简单的例子,展示了如何在 C 语言中使用 env 参数获取环境变量:
#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; }
结果同第一种
③通过系统调用获取环境变量
getenv()函数
代码:
#include <stdio.h> #include <stdlib.h> int main() { printf("%s\n", getenv("PATH")); return 0; }
环境变量通常具有全局属性,可以被子进程继承下去
比如下面这段代码:
#include <stdio.h> #include <stdlib.h> int main() { char * env = getenv("MYENV"); if(env){ printf("%s\n", env); } return 0; }
直接运行肯定是没有用的,因为没有这个全局变量,但是我们输入指令export MYENV="hello world" 导出环境变量,再次运行,就有输出结果了,这说明环境变量是可以被子进程继承下去的!
进程地址空间
基于Linux kernel 2.6.32
32位平台
1.内存分布
正常我们在学习C/C++时,了解到的内存分布如下图
我们再通过下面的代码深入了解一下内存的构造
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int g_val=100; 5 int g_unval; 6 7 int main(int argc,char* argv[],char* env[]) 8 { 9 printf("code addr :%p\n",main); 10 const char* p="hello"; 11 printf("read only :%p\n",p); 12 printf("global val :%p\n",&g_val); 13 printf("global uninit val:%p\n",&g_unval); 14 char* q1=(char*)malloc(10); 15 char* q2=(char*)malloc(10); 16 char* q3=(char*)malloc(10); 17 char* q4=(char*)malloc(10); 18 printf("heap addr :%p\n",q1); 19 printf("heap addr :%p\n",q2); 20 printf("heap addr :%p\n",q3); 21 printf("heap addr :%p\n",q4); 22 23 printf("stack addr :%p\n",&q1); 24 printf("stack addr :%p\n",&q2); 25 printf("stack addr :%p\n",&q3); 26 printf("stack addr :%p\n",&q4); 27 28 static int i=0; 29 printf("static addr :%p\n",&i); 30 printf("args addr :%p\n",argv[0]); 31 printf("env addr :%p\n",env[0]); 32 return 0; 33 }
2.什么是进程地址空间
我们看下面的代码
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 int g_val = 100; 5 int main() 6 { 7 pid_t id = fork(); 8 if(id < 0) 9 { 10 perror("fork"); 11 return ; 12 } 13 else if(id == 0) 14 { //child 15 16 g_val = 50; 17 printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); 18 } 19 else 20 { //parent 21 22 sleep(3); 23 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); 24 } 25 sleep(1); 26 return 0; 27 }
我们建立子进程时,将子进程中的全局变量值修改,再让其父进程运行,我们看输出结果
然后我们发现一个惊人的结果,子进程先运行的情况下,修改了全局变量,父进程后运行,但全局变量还是初始值,最让人惊讶的是,他们是相同的地址。
在这里我们要提出一个概念:进程地址空间(即我们常说的虚拟内存)
其实在我们生成每一个C/C++程序时,他就已经为所有的内容分配好了地址,我们可以输入指令objdump -afh 程序名
虚拟内存是计算机操作系统中的一种技术,它扩展了物理内存(RAM)的大小,使得进程能够访问比实际物理内存更大的地址空间。虚拟内存允许多个进程同时运行,每个进程都有自己独立的地址空间,从而实现了进程之间的隔离和保护。
虚拟内存的主要思想是将物理内存和磁盘空间结合起来,形成一个连续的地址空间,称为虚拟地址空间。这个虚拟地址空间对于每个进程都是独立的,每个进程都认为自己在独占使用整个地址空间,而无需关心其他进程的存在。
当进程访问虚拟地址空间时,操作系统会通过一个称为内存管理单元(MMU)的硬件部件将虚拟地址映射到物理内存或磁盘上的相应位置。如果所需的数据在物理内存中,那么访问就会立即完成。如果所需的数据不在物理内存中,操作系统会将不常用的数据从物理内存中置换(换出)到磁盘上的一个特定区域,然后将所需的数据从磁盘读取到物理内存中(换入)。这个过程是透明的,对于进程来说是不可见的。
所以在每个进程的眼里,整个内存中只有自己的存在。
3.为什么要有地址空间
地址空间的存在是为了实现内存管理和进程隔离。
1.内存管理:地址空间允许操作系统有效地管理计算机的内存资源。通过地址空间,操作系统可以跟踪哪些内存地址被使用,哪些是空闲的,以及哪些是属于不同进程的。当进程需要访问内存时,操作系统通过地址映射将虚拟地址转换为物理地址,确保进程访问的内存是合法的,并且不会与其他进程的内存冲突。
2.进程隔离:地址空间实现了进程之间的隔离。每个进程都有自己独立的地址空间,使得进程之间的数据和指令相互隔离,互不干扰。这样,即使一个进程出现错误或崩溃,也不会对其他进程产生影响。进程之间的隔离还提供了更高的系统稳定性和安全性。
3.多道程序设计:地址空间使得操作系统能够在多个进程之间进行切换,实现多道程序设计。通过将进程的地址空间保存在内存中,操作系统可以暂停一个进程的执行,将其状态保存下来,然后恢复另一个进程的执行,从而实现多个进程的并发执行,提高了系统的利用率和响应性。
4.虚拟化:地址空间实现了虚拟化,使得每个进程都认为自己在独占整个内存空间。进程访问的地址是虚拟地址,而不需要关心实际物理内存的布局。这种虚拟化使得编程和应用程序开发更加简单,不需要考虑实际的物理内存位置。
总的来说,地址空间是计算机系统中内存管理和进程隔离的基础,它为操作系统提供了一种有效的内存管理机制,同时保障了不同进程之间的数据安全和隔离。地址空间的存在使得计算机系统能够同时运行多个进程,提高了系统的效率和灵活性。
地址空间在Linux内核中的定义
struct mm_struct { struct vm_area_struct * mmap; /* 映射区域列表的头部 */ struct rb_root mm_rb; /* 存储映射区域的红黑树 */ struct vm_area_struct *mmap_cache; /* 最后查找的映射区域 */ unsigned long (*get_unmapped_area)(struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); pgd_t * pgd; /* 页全局目录(Page Global Directory) */ atomic_t mm_users; /* 进程共享的计数器 */ atomic_t mm_count; /* 进程描述符的引用计数 */ int map_count; /* 映射区域的计数器 */ rwlock_t page_table_lock; /* 页表的自旋锁 */ struct rw_semaphore mmap_sem; /* mmap_sem 用于对进程的映射区域进行保护 */ spinlock_t page_table_lock; /* 页表锁,保护页表修改 */ struct list_head mmlist; /* 进程的链表节点,链接到所有进程的mm_struct列表 */ unsigned long start_code, end_code;/* 进程代码段的起始地址和结束地址 */ unsigned long start_data, end_data;/* 进程数据段的起始地址和结束地址 */ unsigned long start_brk, brk; /* 进程堆的起始地址和当前堆顶地址 */ unsigned long arg_start, arg_end; /* 进程参数的起始地址和结束地址 */ unsigned long env_start, env_end; /* 进程环境变量的起始地址和结束地址 */ unsigned long saved_auxv[AT_VECTOR_SIZE]; /* 辅助向量 */ unsigned long rss; /* resident set size, 进程使用的物理页数量 */ unsigned long total_vm; /* 进程总共的虚拟页数量 */ unsigned long locked_vm; /* 锁定的虚拟页数量 */ unsigned long pinned_vm; /* 固定的虚拟页数量 */ unsigned long shared_vm; /* 共享的虚拟页数量 */ unsigned long exec_vm; /* 程序映像的虚拟页数量 */ unsigned long stack_vm; /* 栈的虚拟页数量 */ unsigned long def_flags; /* 默认页标志 */ unsigned long nr_ptes; /* 被映射页表项的数量 */ unsigned long nr_pmds; /* 被映射页中间表项的数量 */ unsigned long dummy_page; /* 空页表项指针 */ struct vmacache vmacache_seqnum; /* 虚拟页的页表缓存 */ struct vmacache vmacache; /* 虚拟页的页表缓存 */ struct rw_semaphore pagetable_rwsem; /* 用于页表的读写信号量 */ struct page ** pmd_huge_pte; /* Huge PMD页表项 */ int map_count; /* 映射区域的计数器 */ struct list_head mmlist; /* 进程的链表节点,链接到所有进程的mm_struct列表 */ unsigned long start_code, end_code;/* 进程代码段的起始地址和结束地址 */ unsigned long start_data, end_data;/* 进程数据段的起始地址和结束地址 */ unsigned long start_brk, brk; /* 进程堆的起始地址和当前堆顶地址 */ unsigned long arg_start, arg_end; /* 进程参数的起始地址和结束地址 */ unsigned long env_start, env_end; /* 进程环境变量的起始地址和结束地址 */ unsigned long saved_auxv[AT_VECTOR_SIZE]; /* 辅助向量 */ unsigned long rss; /* resident set size, 进程使用的物理页数量 */ unsigned long total_vm; /* 进程总共的虚拟页数量 */ unsigned long locked_vm; /* 锁定的虚拟页数量 */ unsigned long pinned_vm; /* 固定的虚拟页数量 */ unsigned long shared_vm; /* 共享的虚拟页数量 */ unsigned long exec_vm; /* 程序映像的虚拟页数量 */ unsigned long stack_vm; /* 栈的虚拟页数量 */ unsigned long def_flags; /* 默认页标志 */ unsigned long nr_ptes; /* 被映射页表项的数量 */ unsigned long nr_pmds; /* 被映射页中间表项的数量 */ unsigned long dummy_page; /* 空页表项指针 */ struct vmacache vmacache_seqnum; /* 虚拟页的页表缓存 */ struct vmacache vmacache; /* 虚拟页的页表缓存 */ struct rw_semaphore pagetable_rwsem; /* 用于页表的读写信号量 */ struct page ** pmd_huge_pte; /* Huge PMD页表项 */ struct mm_rss_stat rss_stat; /* 进程内存使用的统计信息 */ unsigned int def_flags; /* 默认页标志 */ pgd_t *pgd; /* 页全局目录表项指针 */ struct mm_struct *mm; /* 进程所属的mm_struct */ unsigned long start_brk; /* 堆的起始地址 */ unsigned long brk; /* 当前堆的顶部地址 */ unsigned long start_stack; /* 栈的起始地址 */ unsigned long arg_start; /* 参数区的起始地址 */ unsigned long arg_end; /* 参数区的结束地址 */ unsigned long env_start; /* 环境变量区的起始地址 */ unsigned long env_end; /* 环境变量区的结束地址 */ unsigned long saved_auxv[AT_VECTOR_SIZE];
4.页表
在虚拟内存中的进程要映射到物理内存中,是通过页表来实现的,那什么是页表呢?
页表(Page Table)是计算机操作系统中用于实现虚拟地址到物理地址转换的一种数据结构。在使用虚拟内存的系统中,每个进程都有自己的虚拟地址空间,而虚拟地址需要通过页表映射为实际的物理地址才能在内存中定位数据。
当进程访问虚拟地址时,操作系统的内存管理单元(MMU)会使用页表来完成地址转换。页表记录了虚拟地址和对应的物理地址之间的映射关系。它将虚拟地址划分成固定大小的页面(通常是4KB或2MB),然后将这些页面映射到物理内存中的对应位置。
页表通常以表格的形式存储,其中每一行对应一个虚拟页,包含了虚拟地址和物理地址之间的映射关系。虚拟地址的高位部分用于查找页表中的行,而低位部分则用于在对应页中定位实际的物理地址。
页表通常存放在计算机的主存(物理内存)中,具体来说,页表存放在进程的内核地址空间中。每个进程在运行时都有自己的页表,用于将其虚拟地址空间中的虚拟地址映射到实际的物理地址。
在 32 位系统中,页表一般存储在操作系统内核的地址空间中的某个特定区域,例如 Linux 内核中常见的位置是从 0xC0000000(3GB)开始的一段地址空间。这个区域被称为内核空间,用于存放操作系统内核的代码、数据和数据结构,包括页表。进程的用户空间通常从 0 开始,虚拟地址从 0 到 3GB 是用户空间的范围。
在 64 位系统中,由于地址空间更为宽广,页表的存储方式可能有所不同。不过一般仍然是在内核地址空间中存放页表,而用户空间则会占用更大的范围。
页表通常是由操作系统动态地创建和管理的,而且对于每个进程都有独立的页表。当进程切换时,操作系统会切换对应的页表,以便为每个进程提供独立的虚拟地址空间。页表的管理和切换是操作系统内存管理的核心功能之一,它使得虚拟内存系统能够高效地实现进程之间的隔离和内存共享。
在虚拟内存系统中,常见的页表结构包括:
1.单级页表:使用一个单独的表来存储所有虚拟页和物理页的映射关系。这种方法简单直接,但对于大型地址空间,需要较大的页表,可能会造成空间浪费。
2.多级页表:将页表分为多个级别,每个级别包含一个部分虚拟地址和物理地址的映射。通过多级页表,可以有效地减少页表的大小,节省内存空间。常见的多级页表结构有两级页表、三级页表等。
3.反向页表:通常用于处理大型的物理内存。反向页表将物理页映射到虚拟页,从而可以更快速地查找物理地址对应的虚拟地址。
虚拟地址到物理地址的转换是操作系统内存管理的核心部分。页表的使用允许操作系统在虚拟内存和物理内存之间实现透明的地址转换,使得进程感觉自己在独占整个内存空间,提高了系统的灵活性和资源利用率。
在 Linux 内核中,页表是通过多级页表的方式来实现的。每个进程都有自己独立的页表用于将虚拟地址映射到物理地址。页表的数据结构主要包括以下几个层次:
pgd_t(Page Global Directory):页全局目录,最顶层的页表,用于将虚拟地址映射到中间页表 pmd。
pmd_t(Page Middle Directory):页中间目录,第二层的页表,用于将虚拟地址映射到底层页表 pte。
pte_t(Page Table Entry):页表项,最底层的页表,用于将虚拟地址映射到物理地址。
这些页表数据结构的定义可以在 Linux 内核的头文件 linux/pgtable.h 中找到。
5.理解进程地址空间
通过上面这幅图结合前面的知识我们不难理解,为什么子进程和父进程相同代码同一内存地址可以有不同的结果,亦或是之前的fork函数为什么可以有两个返回值。同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
所以在每一个进程建立时,其中内容都有其对应的虚拟地址,然后通过页表再访问内存,页表通过映射找到合适的物理地址再运行,页表存在的进程虚拟地址及对应物理地址可以做到更好的交互,比如我们使用动态内存分配函数申请的空间未使用,造成的浪费空间,页表并不会直接映射,而是有延迟分配,等待你使用后再映射,从而达到提高整机效率,让物理内存真正做到100%有效使用。
总结
有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!