一:进程优先级
基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程 ps -la
ps -la命令用于以长格式列出当前系统中所有进程的信息,包括所有用户的进程。
-l
选项提供详细的进程信息,而-a
则表示显示所有进程,包括没有控制终端的进程。UID : 代表执行者的身份 (跟该程序的持有者 所属组对比,看是否有对应权限)
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行 PRI = 80+NI
NI :代表这个进程的nice值 [-20,19]
修改优先级
top -> r(进入修改模式) -> 要修改的进程的PID -> 修改后的NI值
PRI=80+NI
[root@hcss-ecs-178e ~]# ps -la F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 0 30838 30810 0 80 0 - 47970 do_wai pts/1 00:00:00 su 4 S 1001 30839 30838 0 80 0 - 28887 do_wai pts/1 00:00:00 bash 0 S 1001 30860 30839 0 80 0 - 1054 hrtime pts/1 00:00:00 myexe 1 S 1001 30861 30860 0 99 19 - 1054 hrtime pts/1 00:00:00 myexe 4 R 0 30894 30862 0 80 0 - 38332 - pts/2 00:00:00 ps
二:进程切换
1. 什么是进程切换?
进程切换是指 CPU 停止执行当前进程,并将控制权转移到另一个进程的过程。这种机制使得操作系统能够在多任务环境中并发运行多个进程。
2. 进程切换的原因
时间片到期:当某个进程的时间片用完时,调度器会选择其他进程。
I/O 操作:进程请求 I/O 操作时,会被阻塞,操作系统会切换到其他可运行的进程。
3. 如何进行切换的
首先结构体指针current会指向目标task_struct
pc(程序计数器)内数据是下一条指令的地址,ir(指令寄存器)内数据是目前要执行的指令内容。
1.控制器读取PC中的地址。
2.从内存中获取该地址对应的指令,并将其放入IR中。
3.PC自增,指向下一条指令的地址。
4.控制器执行IR内指令。
5.不断循环直到进程结束或停止。
那这些数据哪里来的呢?如果执行到一半,该第二个进程执行,那这些数据怎么办呢?
其实这些相关寄存器的内容会被存入task_struct(PCB)中,等到再执行到它的时候,再把数据拷贝回PCU的寄存器中,就可以继续运行。
三:Linux2.6内核进程调度队列
CPU的调度顺序是按照队列的顺序从头到尾变量一遍吗?那优先级体现在那呢?
runqueue
runqueue调度队列中有两个指针*active *expired,他们各自指向一个结构体struct queue array
,array中有
1.queue[140]: 一个元素就是一个进程队列。
下标0~99是实时优先级(不关心)(实时优先级使用 SCHED_FIFO 和 SCHED_RR 策略,优先级高的实时任务会优先获得 CPU,实时任务可以抢占普通任务。)
下标100~139是普通优先级(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
task_struct优先级越高,下标越小。优先级相同的串在同一个位置,类似哈希桶那样。
2.int nr_active 代表队列中有多少个进程
3.int bit_map[5] 用来快速查找进程位置
活动队列 过期队列
*active指向的就是活动队列,*expried指向的就是过期队列。
为什么要有两个队列呢?
1.活动队列中就是要将要调度的进程,如果该进程运行退出了/时间片到了,或者又有新的进程出现了,要重新插到活动队列里吗?随便插就不能叫队列了,尾插优先级就体现不出来了。2.所以我们一般这3种情况的进程按优先级大小插入到过期队列中。
3.当我们活动队列中nr_active==0时说明已经没有进程了,之后swap(&active,&expried),通过交换指针的对象来改变活动队列 过期队列,保证活动队列只出不进,过期队列只进不出。
快速查找队列中进程
int bit_map[5] 用来快速查找进程位置,这是怎么做到的呢?
5 int = 20 字节 = 160 bit 可以满足140个进程
每一个bit位代表一个进程,存在1 不存在0。
遍历数组,一次性就可以看32个位置存不存在进程。
1.为0说明没有进程
2.不为0有进程,下面代码可以查一个数的二进制中有几个1
int count_set_bits(int x) { int count = 0; while (x) { count++; x &= (x - 1); } return count; }
这是因为
x - 1
会将x
的最低位的“1”变为“0”,并将其右侧的所有位(如果有的话)变为“1”。然后,使用按位与(&
)操作符,x
和x - 1
结合后,最低的“1”位被清除。举个例子:
假设
x = 12
,它的二进制表示为1100
。
计算
x - 1
:
x - 1 = 11
,二进制为1011
。按位与操作:
x = 1100 ;x - 1 = 1011 x &= (x - 1) = 1000
补充:为什么进程可以在不同队列中
一般队列中的元素都是包含 数据data 指向下个元素指针struct node*next,
而在struct task_struct这个结构体中,有多个队列 struct node link struct node queuenode...
里面的成员只有指向下个元素的指针struct node*next,而这个元素是struct task_struct的成员变量。知道成员变量地址,就可以算出该结构体首地址,进而随便访问它的成员变量。
成员变量地址-该成员变量的偏移量=struct task_struct的首地址。
offsetof(结构体名, 成员变量名)
&(((type *)0)->member)
:通过解引用该指针获取成员的地址。
四:命令行参数
什么是命令行参数
命令行参数是用户在运行程序时通过命令行界面传递给程序的输入值。这些参数通常用于配置程序的行为或提供必要的数据。
eg. ls -l -a
ls指令后面的-l -a 就是命令行参数
这段代码可以打印出命令行参数
argc代表参数个数,argv[] 里面是以空格分开是字符串
根据参数列表的不同,同一个程序可以实现出不同的功能。
eg.ls -a ls -l
谁传给main函数参数列表的?
参数是由操作系统在程序启动时传递的,当用户在命令行中运行程序时,操作系统会解析命令行并将参数传递给程序。
五:环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
查看环境变量
1.通过main函数第三个参数
env[0]:XDG_SESSION_ID=354
env[1]:HOSTNAME=hcss-ecs-178e
env[2]:SHELL=/bin/bash
env[3]:TERM=xterm
env[4]:HISTSIZE=10000
env[5]:SSH_CLIENT=121.36.59.153 43740 22
env[6]:OLDPWD=/root
env[7]:SSH_TTY=/dev/pts/0
env[8]:USER=wws
env[9]:LD_LIBRARY_PATH=:/home/wws/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
env[10]:LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
env[11]:PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
env[12]:MAIL=/var/spool/mail/root
env[13]:PWD=/home/wws
env[14]:LANG=en_US.UTF-8
env[15]:HISTCONTROL=ignoredups
env[16]:HOME=/home/wws
env[17]:SHLVL=2
env[18]:LOGNAME=wws
env[19]:SSH_CONNECTION=121.36.59.153 43740 192.168.15.237 22
env[20]:LESSOPEN=||/usr/bin/lesspipe.sh %s
env[21]:XDG_RUNTIME_DIR=/run/user/0
env[22]:HISTTIMEFORMAT=%F %T wws
env[23]:_=./myexe
2.env指令
env 查看所有环境变量
3.echo $环境变量名
在 Linux 中,echo $环境变量名 命令用于显示环境变量的值。
[wws@hcss-ecs-178e ~]$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
4.getenv函数
#include <stdio.h> #include <stdlib.h> int main() { const char *path = getenv("PATH"); // 获取 PATH 环境变量 if (path != NULL) { printf("PATH: %s\n", path); } else { printf("环境变量 PATH 不存在。\n"); } return 0; }
头文件#include<stdlib.h> 找到返回指向该变量值的指针,找不到返回NULL
5.environ全局变量
environ
是一个全局变量,其类型通常定义为char **environ
,表示指向字符指针的指针。#include <stdio.h> #include <stdlib.h> extern char **environ; // 声明 environ int main() { // 打印所有环境变量 for (char **env = environ; *env != 0; env++) { printf("%s\n", *env); } }
常见的环境变量
1.PATH
PATH是一个环境变量,它指定了系统查找可执行文件的目录。
如果我们运行程序时没有加路径就会在PATH的路径下查找。eg. ls
通常是由多个路径组成,以冒号为分隔符。
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
如果我们自己写的程序不想加路径就运行,1.可以把程序move到默认路径下。
2.可以在PATH中添加我们程序所处的路径。PATH=$PATH:新增路径
$PATH是原本的路径
[wws@hcss-ecs-178e ~]$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin [wws@hcss-ecs-178e ~]$ pwd /home/wws [wws@hcss-ecs-178e ~]$ PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin [wws@hcss-ecs-178e ~]$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
但当我们重新登录的时候却发现PATH中没有我们新增的路径,这是为什么?
我们要知道PATH等环境变量的内容是从系统的配置文件中获取的,在家目录下找到.bash_profile文件(用于设置环境变量、启动程序及其他一次性配置。)在里面添加自己的路径就可以保存下来。source ~/.bashrc命令让它立即生效。
2.HOME
HOME的内容代表家目录路径,也就是用户一开始登录的路径。
[wws@hcss-ecs-178e ~]$ echo $HOME
/home/wws
[wws@hcss-ecs-178e ~]$ su -
Password:
Last login: Thu Sep 26 19:25:10 CST 2024 from 121.36.59.153 on pts/0
[root@hcss-ecs-178e ~]# echo $HOME
/root
根据用户的不同,系统设定的HOME也不同,所以用户就能根据自己的HOME找到自己的家目录。
3.SHELL
查看当前使用的 shell:
[wws@hcss-ecs-178e ~]$ echo $SHELL /bin/bash
4.PWD
获取当前工作目录
[wws@hcss-ecs-178e ~]$ echo $PWD /home/wws
5.USER
当前用户
[wws@hcss-ecs-178e ~]$ echo $USER wws
根据比对USER是否相同,程序就可以实现让特定用户来执行。
6.OLDPWD
上一次所处的路径
[wws@hcss-ecs-178e ~]$ echo $OLDPWD /root
cd ~
本地变量
set
[wws@hcss-ecs-178e ~]$ a=1 [wws@hcss-ecs-178e ~]$ b=2 [wws@hcss-ecs-178e ~]$ echo $a 1 [wws@hcss-ecs-178e ~]$ echo $b 2
定义的本地变量用env是查不到的,可以用set查看所有环境变量+本地变量
export
将本地变量变为环境变量。
也可以直接设置环境变量 export a=1
bash中有环境变量表 本地变量表,但只有环境变量表可以传给子进程,本地变量表不能,可以把本地变量变为环境变量再传给子进程。所以环境变量有全局属性(环境变量可以在父进程和子进程之间传递信息)
unset
可以删除环境/本地变量
[wws@hcss-ecs-178e ~]$ unset a [wws@hcss-ecs-178e ~]$ echo $a
六:进程地址空间
上图是进程的地址空间的划分,这和我们之前C++内存区域划分是不是有点像呢?它们间有什么联系吗?
什么是进程地址空间
#include<stdio.h>
#include<unistd.h>
int g_val = 0;
int main()
{
printf("begin.....%d\n",g_val);
pid_t id = fork();
if(id==0)
{
//child
int count = 0;
while(1)
{
printf("child: pid: %d,ppid: %d, g_val:%d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
g_val++;
}
}
else if(id>0)
{
//father
while(1)
{
printf("father: pod: %d,ppid: %d, g_val:%d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;
}
上面代码子进程对全局变量++,父进程不改变全局变量。
可以看到子进程读取g_val时它的值一直变化,而父进程读取g-val永远是0,但是父子进程取得g_val的地址都是一样。这个地址是物理地址吗?
不是,它是一种虚拟地址。是进程空间地址
进程空间地址的本质是一种结构体struct mm_struct,里面包含了不同区域的范围。
记住堆区的开始和结束地址,中间的都是可以用的。
struct mm_struct { unsigned long code_start;//代码区 unsigned long code_end; unsigned long init_start;//初始化区 unsigned long init_end; unsigned long uninit_start;//未初始化区 unsigned long uninit_end; unsigned long heap_start;//堆区 unsigned long heap_end; unsigned long stack_start;//栈区 unsigned long stack_end; //...等等 }
每一个进程都有一个虚拟地址,struct_task中成员变量有指向mm_struct的指针。
在内存中不止存有一个程序的资源,每个进程都有自己的虚拟地址,而虚拟地址的作用就是让每一个进程认为自己可以独享所有内存空间。
比如说进程申请空间,在虚拟地址中标记0x1111~0x2222的空间被申请,实际上在物理地址不一定是0x1111~0x2222,可能是在其它地方。虚拟地址栈区中有4G空间,但实际在物理地址中可能只有2G,这样进程申请3G空间,就会被系统拒绝。
页表
上述虚拟地址和物理地址中间是怎么联系的?
其实是根据页表产生联系的,页表是操作系统中用于管理虚拟内存的一种数据结构,它负责维护虚拟地址与物理地址之间的映射关系。
当进程访问某个变量,它在、的虚拟地址是0x1111,而物理地址是0x2222,页表的作用就是将进程使用的虚拟地址转换为物理地址。
fork()创建子进程,子进程的task_struct mm_struct 页表都是以父进程为模板创建的,所以指向同一段代码,如果子进程对数据进行修改g_val++ ,内存就会再开辟一块空间存放子进程的g_val,子进程页表映射就指向新开辟的空间。也就是写时拷贝
printf打印的就是虚拟地址,而系统真正访问的是物理地址,所以值会不一样。
标志位
在页表中处理了虚拟地址和物理地址外,还有一些标识符,那标志位有什么用?
1.读/写位(Read/Write Bit):
指示该页面是可读的还是可写的。如果该位为0,表示该页面为只读;如果为1,表示可以进行读写操作。
当我们对一个变量进行修改时,先看在页表中是否有对应的映射关系,如果有看是否有写权限,有就修改,反之终止进程。
就像char*str="111" *str="222",对它进行修改编译时不会进行权限检查,所以不会报错,运行时就会终止。也因此出现了const
2.存在位(Present Bit):
页面是否在物理内存中。如果该位为0,表示该页面不在内存中,可能需要从磁盘加载。
当程序从磁盘加载到内存中,不是一次性加载而是分批加载,访问时在页表中有映射关系并且存在位为1,才能访问。如果为0,要等数据从磁盘中加载到内存把存在位变1.
进程地址空间区域范围划分
每个程序都有自己的进程空间,大小不一样,进程空间的代码区等范围也是不一样的。mm_struct本质就是结构体变量,也需要初始化。那么它是根据什么进行初始化划分代码区,栈区 堆区等的范围的呢?
1.首先可执行程序除了代码和数据还包含属性,里面就有代码区范围的大小,变量权限是否可读写。
mm_struct的代码区范围就是从可执行程序的属性中读取。
2.栈区 堆区 命令行参数环境变量 这些程序编译的时候并不会创建,他们是由操作系统动态生成并管理的。当进程被创建时,操作系统会为其分配一个默认的栈/堆大小,函数开辟栈帧改变栈区大小,malloc/free会改变堆区大小。
此时改变的mm_struct中堆区范围大小,在物理内存上并没有进行开辟,当对申请的空间使用时,才会在物理内存上开辟。确保物理内存中开辟的空间都在使用
为什么要有进程空间地址
1. 内存隔离
个进程都有独立的地址空间,这样可以防止一个进程访问或修改另一个进程的内存数据。
对内存进行修改时,要查看是否有对应权限才能修改。防止野指针随便修改内容
2.进程管理和内存管理在系统层面上解耦合
创建进程时可以先在进程空间上进行操作,等真正使用数据时再写入内存空间中。
3.让进程以统一的视角看待物理内存
每个进程在操作系统中都有自己独立的虚拟地址空间。这意味着每个进程都可以以统一且一致的方式访问其内存,而不必担心其他进程的内存内容。通过页表映射即使在物理内存中无序放置数据,也可以快速找到