目录
1.操作系统(Operator System)
1.1 概念
1.2 设计OS的目的
1.3 定位
1.4 如何理解 "管理"
1.5 总结
1.6 系统调用和库函数概念
2.进程
2.1基本概念
2.2描述进程-PCB
2.3 task_struct-PCB的一种
2.4task_ struct内容分类
2.5进程状态
3. 进程优先级
3.1基本概念
3.2查看系统进程
3.3 PRI and NI
3.4 PRI vs NI
3.5 查看进程优先级的命令
3.6 其他概念
4.环境变量
4.1基本概念
4.2 常见环境变量
4.3 查看环境变量方法
4.4 和环境变量相关的命令
4.5 环境变量的组织方式编辑
4.6 通过代码如何获取环境变量
4.7 通过系统调用获取或设置环境变量
4.8 环境变量通常是具有全局属性的
5.程序地址空间
5.1 研究背景
5.2 程序地址空间回顾
5.3 进程地址空间
6. Linux2.6内核进程调度队列-选学
6.1 一个CPU拥有一个runqueue
6.2 优先级
6.3 活动队列
6.4 过期队列
6.5 active指针和expired指针
6.6 总结
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!
——By 作者:新晓·故知
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
与硬件交互,管理所有的软硬件资源为用户程序(应用程序)提供一个良好的执行环境
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
管理的例子描述被管理对象组织被管理对象 .
计算机管理硬件1. 描述起来,用struct结构体2. 组织起来,用链表或其他高效的数据结构
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
课本概念:程序的一个执行实例,正在执行的程序等内核观点:担当分配系统资源(CPU时间,内存)的实体。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct实例演示:这里写一个C语言程序进行演示:输入指令:创建目录、项目,编写程序#include
#include int main() { while(1) { printf("This is a process!\n"); sleep(1); } return 0; } 编译执行:这就是进程!
(蓝色的数字就是进程的pid)
只查看指定进程信息:(需要保证被查看的进程正在进行,因此这里开启两个窗口!)
注:每次程序取消,再次运行,进程的pid就会变化,这里的仅作为演示,因为每次进程都会有对于的数据!
如何验证进程实时信息?
(pid=27611接上面的进程,这里只作为演示,因为每次进程改变,pid就会变化)
当前路径:即进程当前所在路径,进程自己会维护!
打印进程的pid:
使用函数getpid():
#include
#include #include int main() { while(1) { printf("This is a process! pid:%d\n",getpid()); sleep(1); } return 0; }
打印父进程ppid:
使用man手册查看getppid()函数:
#include
#include #include int main() { while(1) { printf("This is a process! pid:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } return 0; }
查看ppid的信息:
使用fork()函数:
通过查看Linux中的man手册知晓:fork()函数是用来创建子进程的,并且它不同于以往的函数:即fork()函数竟有两个返回值!!!
#include
#include #include int main() { pid_t id=fork(); printf("hello,linux\n"); // while(1) // { // printf("This is a process! pid:%d,ppid:%d\n",getpid(),getppid()); // sleep(1); // } return 0; } 不同以往的发现:调用一次printf,却打印两次:
下面再次打印id:
![]()
#include
#include #include int main() { pid_t id=fork(); // printf("hello,linux\n"); printf("hello,linux! id:%d\n",id); // while(1) // { // printf("This is a process! pid:%d,ppid:%d\n",getpid(),getppid()); // sleep(1); // } return 0; } #include
#include #include int main() { pid_t id=fork(); if(id==0) { //id==0:子进程 while(1) { printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); sleep(1); } } else { //id>0:父进程 while(1) { printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); sleep(1); } } return 0; } 1.C语言中,if与else不能同时执行!
2. C语言中,两个以上的死循环不会同时运行!
但在Linux却发现:
- fork()之后,父进程和子进程会共享代码,一般会执行后续的代码。(这就解释了printf为什么会打印两次的问题!)
- fork()之后,父进程和子进程返回值不同,可以通过不同的返回值进行判断,让父、子进程执行不同的代码块!
那么我们就会有几个问题需要解释:
1.fork()函数如何做到会有不同的返回值?
2.同一个id调用打印,且没有修改id的值,为什么却打印出两个不同的值?
3.fork()函数为什么给父进程返回的是子进程的pid,给子进程返回的是0?
4.为什么fork()函数会返回两次?
解析:
1.后面学习
2.后面学习
3.父进程必须要有标识子进程的方案,fork()之后,给父进程返回子进程的pid!
而子进程最重要的是要知道自己被创建成功了,子进程去寻找父进程成本非常低!(利用树的结构分析)
4.首先fork()之后,操作系统做了什么?
既然创建进程,则系统多了一个新进程。则有task_struct+进程代码和数据、task_struct+子进程的代码和数据。而子进程的task_struct的对象内部的属性数据,基本上(大部分)是从父进程拷贝(继承)下来的,但有些不是,例如子进程的pid等。子进程被创建后,要执行代码和计算数据,那这些代码是和父进程执行同样的代码,即fork()之后,父、子进程代码共享,但数据要各自独立!虽然代码共享,但耗费这么大精力去创建子进程,不是为了代码共享,而是通过不同的返回值去执行不同的代码!
5.调用一个函数,当这个函数准备return的时候,这个函数的核心功能完成了吗?
答:完成了,因为返回是为了告知调用方成功还是失败,是要去通知了,返回不是函数核心功能的一部分!
6.如何理解进程被运行?
对于进程,当return pid时,则说明子进程已经被创建,并且接下来就会将子进程放入运行队列,而return是代码语句,父进程、子进程在运行队列里,以供CPU去调度,当运行代码至return语句时,父进程、子进程均会return且是各自return,则这里就返回了两个值!但同一个值id接收,同一个变量id打印却是不同的值(这与系统中父进程和子进程如何看待它们各自的数据有关!此处请与以往的C/C++等语言中的变量以及接收打印等对比(实际上这些变量并不是在内存中,而是一个变量名对应多种存储空间!),发现之前的理解都比较粗浅,当随着操作系统学习的深入就会更加理解本质!)
在Linux中描述进程的结构体叫做task_struct。
标示符: 描述本进程的唯一标示符,用来区别其他进程。状态: 任务状态,退出代码,退出信号等。优先级: 相对于其他进程的优先级。程序计数器: 程序中即将被执行的下一条指令的地址。内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。其他信息组织进程可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。查看进程进程的信息可以通过 /proc 系统文件夹查看如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。大多数进程信息同样可以使用top和ps这些用户级工具来获取进程状态:实际上说进程,就要想到进程的task_struct,使用不同值描述!将抽象概念数据化!通过系统调用获取进程标示符进程id(PID)父进程id(PPID)通过系统调用创建进程-fork初识运行 man fork 认识forkfork有两个返回值父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)fork 之后通常要用 if 进行分流
看看Linux内核源代码怎么说为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态写一段代码用于演示:#include
#include int main() { while(1) { printf("I am a process! pid:%d\n",getpid()); sleep(1); } return 0; } 使用指令查看父、子进程的pid信息:
睡眠状态:因为CPU的处理比外设快很多,因此程序大部分时间都在等外设(这里是显示器打印),
D:深度睡眠(不可中断睡眠)
由于计算机CPU计算之快,所以这个难以模拟查看!可以使用dd命令,但几率小!服务器压力过大,操作系统会终止用户进程!
一般而言,Linux中,我们所等待的若是磁盘资源,那我们进程阻塞所处的状态就是D状态!操作系统都无权杀进程!只能等到D状态自己醒来(磁盘处理完毕)
强制解决办法:关机重启或拔电源!
进程状态查看Z(zombie)-僵尸进程僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态来一个创建维持30秒的僵死进程例子:Z(zombie)- 僵尸进程僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态:来一个创建维持30秒的僵死进程例子:当一个Linux中的程序退出的时候,一般不会直接进入X状态(程序死亡,资源可以立马被回收),而是进入Z状态,因为一般需要将进程的执行结果告知给父进程或操作系统,进入Z状态,就是为了维护退出信息,反馈给父进程或操作系统读取(通过进程等待方式读取)#include
#include #include int main() { pid_t id=fork(); if(id==0) { //子进程 int cnt=5; while(cnt) { printf("我是子进程,我还剩下 %d S\n",cnt--); sleep(1); } printf("我是子进程,已经进入zombie状态,等待被检测!\n"); exit(0); } else { //父进程 while(1) { sleep(1); } } return 0; }
长时间处于zombie状态,如果没有回收处于zombie状态的子进程,那该状态就会一直维护!该进程的相关资源(task_struct)不会被释放,则造成内存泄漏!
一般必须要求父进程进行回收!僵尸进程危害进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!内存泄漏?是的!如何避免?后面讲进程状态总结至此,值得关注的进程状态全部讲解完成,下面来认识另一种进程孤儿进程父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?父进程先退出,子进程就称之为“孤儿进程”状态后面有+号,代表这是个前台进程的状态,可以使用Ctrl+c终止此进程!例:S+
而S就成为了后台进程状态!
#include
#include #include int main() { pid_t id=fork(); if(id==0) { //子进程 int cnt=5; while(1) { printf("我是子进程,我还剩下 %d S\n",cnt--); sleep(1); } printf("我是子进程,已经进入zombie状态,等待被检测!\n"); exit(0); } else { //父进程 int cnt=3; while(cnt) { printf("我是父进程,我还剩下 %d S\n",cnt--); sleep(1); } exit(0); } return 0; } 如果父进程提前退出,那么子进程就会成为孤儿进程,会被OS领养!
对于T /t状态:
3. 进程优先级
3.1基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。优先级是进程获取资源的先后顺序。权限谈的是能还是不能的问题!优先级是在能的前提下,只是先后顺序!为什么会确立优先级?系统里面都是进程占用大多数,而资源是少数!进程竞争资源是常态!写一段代码用于演示:修改优先级:
Linux更改进程优先级,需要更改的不是PRI,而是NI,
(nice:进程优先级的修正数据)
Linux有指令指定优先级:nice、renice
Linux不允许用户无限制修改优先级,因此优先级有范围!这里要注意一点:每次修改优先级时,都会以80作为基准点。(即上次修改的优先级对后面没有影响!)
3.2查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:我们很容易注意到其中的几个重要信息,有下:UID : 代表执行者的身份PID : 代表这个进程的代号PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号PRI :代表这个进程可被执行的优先级,其值越小越早被执行NI :代表这个进程的nice值3.3 PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 ,进程的优先级别越高那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值nice其取值范围是-20至19,一共40个级别3.4 PRI vs NI
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据3.5 查看进程优先级的命令
用top命令更改已存在进程的nice:top进入top后按“r”–>输入进程PID–>输入nice值3.6 其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发多个进程在系统中运行!=多个进程在系统中同时运行!1.操作系统允许不同优先级的进程的存在,2.相同优先级的进程是可以存在多个的。
Linux内核根据不同的优先级将特定的进程放入不同的队列中!将相同的优先级进程链入同一优先级的队列!
调度算法:根据不同的哈希值将优先级进程处理。
进程切换:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性4.2 常见环境变量
PATH : 指定命令的搜索路径HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)SHELL : 当前Shell,它的值通常是/bin/bash。4.3 查看环境变量方法
echo $NAME //NAME:你的环境变量名称测试PATH1. 创建hello.c文件2. 对比./hello执行和之间hello执行3. 为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径才能执行?答:4. 将我们的程序所在路径加入环境变量PATH当中, export PATH=$PATH:hello程序所在路径5. 对比测试6. 还有什么方法可以不用带路径,直接就可以运行呢?测试HOME1. 用root和普通用户,分别执行 echo $HOME ,对比差异. 执行 cd ~; pwd ,对应 ~ 和HOME 的关系4.4 和环境变量相关的命令
1. echo: 显示某个环境变量值2. export: 设置一个新的环境变量3. env: 显示所有环境变量4. unset: 清除环境变量5. set: 显示本地定义的shell变量和环境变量使用指令查看环境变量:
4.5 环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串4.6 通过代码如何获取环境变量
命令行第三个参数#include
int main(int argc, char *argv[], char *env[]) { int i = 0; for(; env[i]; i++) { printf("%s\n", env[i]); } return 0; } #include
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; } #include
#include #include //实现一个简易版计算器 // ./myproc -a 10 20 // 10+20=30 // ./myproc -s 10 20 // 10-20=-10 // ./myproc -a -s -m -d int main(int argc,char* argv[]) { if(argc!=4) { printf("Usage: %s [-a|-s|-m|-d] one_data two_data\n",argv[0]); return 0; } int x=atoi(argv[2]); int y=atoi(argv[3]); if(strcmp("-a",argv[1])==0) { printf("%d+%d=%d\n",x,y,x+y); } else if(strcmp("-s",argv[1])==0) { printf("%d-%d=%d\n",x,y,x-y); } else if(strcmp("-m",argv[1])==0) { printf("%d*%d=%d\n",x,y,x*y); } else if(strcmp("-d",argv[1])==0&&y!=0) { printf("%d/%d=%d\n",x,y,x/y); } else { printf("Usage: %s [-a|-s|-m|-d] one_data two_data\n",argv[0]); } return 0; } 以上是通过实现命令行计算器程序说明:同一个程序,通过传递不同的参数,让同一个程序有不同的执行逻辑、执行结果!
Linux系统中,会根据不同的选项,让不同的命令,可以有不同的表现!这就是指令中那么多选项的由来和起作用的方式。
Windows系统命令行也类似!
C语言中的main()函数可以带参数吗?如果可以,那可以带几个?
答:可以。3个
写程序演示:
#include
int main(int argc, char* argv[],char* env[]) { for(int i=0;env[i];i++) { printf("env[%d]: %s\n",i,env[i]); } return 0; } 通过第三方变量environ获取
#include
int main(int argc, char *argv[]) { extern char **environ; int i = 0; for(; environ[i]; i++) { printf("%s\n", environ[i]); } return 0; } 4.7 通过系统调用获取或设置环境变量
putenv , 后面讲解getenv , 本次讲解#include
#include int main() { printf("%s\n", getenv("PATH")); return 0; } 一定有特殊用途!
2.环境变量是谁给的?
#include
#include #include int main() { while(1) { printf("This is a test!,pid: %d,ppid: %d,myenv=%s\n",getpid(),getppid(),getenv("testname")); sleep(1); } return 0; } 结论:
1.环境变量:通常具有全局属性,可以被子进程继承下去!
2.本地变量:本质就是在bash内部定义的变量,不会被子进程继承!
3.Linux下大部分命令都是通过子进程的方式执行的!但是还有一部分命令,不通过子进程的方式执行,而是由bash自己执行的(直接调用自己对应的函数来完成特定的功能,把这种吗命令称为内建命令)
(但set和env也是一条命令,是子进程,为什么仍能够读取testname1的内容呢?
答:Linux下大部分命令都是通过子进程的方式实现的!但是还有一部分命令,不通过子进程的方式执行,而是由bash自己执行(调用自己对应的函数来完成特定功能,这种命令称为内建命令! 例如:cd、echo、pwd等都是在bash的上下文执行,而没有创建子进程帮助执行,这些命令信任度高,通过函数识别,由bash自己执行!))4.8 环境变量通常是具有全局属性的
环境变量通常具有全局属性,可以被子进程继承下去#include
#include int main() { char * env = getenv("MYENV"); if(env) { printf("%s\n", env); } return 0; } 直接查看,发现没有结果,说明该环境变量根本不存在导出环境变量export MYENV="hello world"再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!想想为什么?实验如果只进行 MYENV=“helloworld” ,不调用export导出,在用我们的程序查看,会有什么结果?为什么?普通变量5.程序地址空间
5.1 研究背景
kernel 2.6.3232位平台5.2 程序地址空间回顾
可是我们对他并不理解!来段代码感受一下#include
#include #include int g_val = 0; int main() { pid_t id = fork(); if(id < 0) { perror("fork"); return 0; } else if(id == 0) { //child printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); } else { //parent printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); } sleep(1); return 0; } 输出//与环境相关,观察现象即可parent[2995]: 0 : 0x80497d8child[2996]: 0 : 0x80497d8我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变 量进行进行任何修改。可是将代码稍加改动:#include
#include #include int g_val = 0; int main() { pid_t id = fork(); if(id < 0){ perror("fork"); return 0; } else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取 g_val=100; printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); }else{ //parent sleep(3); printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); } sleep(1); return 0; } 输出结果://与环境相关,观察现象即可child[3046]: 100 : 0x80497e8parent[3045]: 0 : 0x80497e8我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:变量内容不一样,所以父子进程输出的变量绝对不是同一个变量但地址值是一样的,说明,该地址绝对不是物理地址!在Linux地址下,这种地址叫做 虚拟地址我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理#include
#include #include int un_g_val; int g_val=100; int main(int argc,char* argv[],char* env[]) { printf("code addr : %p\n",main); //代码地址 printf("init global addr : %p\n",&g_val); //初始化全局变量地址 printf("uninit global addr : %p\n",&un_g_val); //未初始化全局变量地址 char* m1=(char*)malloc(50); printf("heap addr : %p\n",m1); //堆地址 printf("stack addr : %p\n",&m1); //栈地址 int i=0; for( ; i < argc; i++ ) { printf("argv addr : %p\n",argv[i]); //命令行参数地址 } for( ; env[i]; i++ ) { printf("env addr : %p\n",env[i]); //环境变量地址 } return 0; } 验证堆和栈的增长方向:
#include
#include #include int un_g_val; int g_val=100; int main(int argc,char* argv[],char* env[]) { printf("code addr : %p\n",main); //代码地址 printf("init global addr : %p\n",&g_val); //初始化全局变量地址 printf("uninit global addr : %p\n",&un_g_val); //未初始化全局变量地址 char* m1=(char*)malloc(50); char* m2=(char*)malloc(50); char* m3=(char*)malloc(50); char* m4=(char*)malloc(50); printf("heap addr : %p\n",m1); //堆地址 printf("heap addr : %p\n",m2); //堆地址 printf("heap addr : %p\n",m3); //堆地址 printf("heap addr : %p\n",m4); //堆地址 printf("stack addr : %p\n",&m1); //栈地址 printf("stack addr : %p\n",&m2); //栈地址 printf("stack addr : %p\n",&m3); //栈地址 printf("stack addr : %p\n",&m4); //栈地址 int i=0; for( ; i < argc; i++ ) { printf("argv addr : %p\n",argv[i]); //命令行参数地址 } for( ; env[i]; i++ ) { printf("env addr : %p\n",env[i]); //环境变量地址 } return 0; } 所以,一般在C语言函数中的定义的变量,通常在栈上保存,那么先定义的一定是地址比较高的!
如何理解static变量?
#include
#include #include int g_val=100; int main() { pid_t id=fork(); if(id==0) { //子进程 while(1) { printf("我是子进程,我的pid:%d ,ppid:%d ,g_val:%d ,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val); sleep(1); } } else { //父进程 while(1) { printf("我是父进程,我的pid:%d ,ppid:%d ,g_val:%d ,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val); sleep(2); } } return 0; } 如果尝试修改全局变量的值:
#include
#include #include int g_val=100; int main() { pid_t id=fork(); if(id==0) { //子进程 //让子进程修改全局变量g_val 的值 int flag=0; while(1) { printf("我是子进程,我的pid:%d ,ppid:%d ,g_val:%d ,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val); sleep(1); flag++; if(flag==3) { g_val=200; printf("我是子进程,全局变量g_val已被修改,请注意查看!\n"); } } } else { //父进程 while(1) { printf("我是父进程,我的pid:%d ,ppid:%d ,g_val:%d ,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val); sleep(2); } } return 0; } 为什么父、子进程读取的值不一样?
修改之前,父、子进程通过相同的虚拟地址找到物理地址,读取值,但修改之后,虽然父、子进程共享代码,但进程具有独立性,修改的进程会被OS维护,其在页表的物理地址重新建立映射关系,但虚拟地址不变,这就是写时拷贝!所以,虚拟地址不变,但发生写时拷贝,通过页表,将父子进程的数据进行了分离!
为什么fork()有两个返回值,pid_id的id同一个变量,怎么会有不同的值?
因为pid_id属于父进程栈空间中定义的变量,fork()内部,return会被执行两次,return的本质就是通过寄存器写入到接受返回值的变量中!当id=fork()的时候,谁先返回,谁就要发生写时拷贝,所以,同一个变量,会有不同的内容值,本质是因为父子进程的虚拟地址是一样的,但对应的物理地址不一样!
为什么操作系统不让用户直接看到物理内存呢?
内存就是一个硬件,不能阻止你访问!只能被动的进行读取和写入!但为了安全问题(涉及数据访问、进程顺序等),增加了虚拟地址机制!
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图
每一个进程在起启动的时候,都会让操作系统给他创建一个地址空间,该地址空间就是进程地址空间!每一个进程都会有一个自己的进程地址空间!操作系统需要管理这些进程地址空间,(先描述,再组织),这些进程地址空间,其实是内核的一个数据结构,struct mm_struct.什么是进程地址空间?指操作系统给进程画的大饼,逻辑上抽象的概念!让每一个进程都认为自己是独占系统中的所有资源的!所谓的地址空间,其实就是操作系统通过软件的方式,给进程提供提个软件视角,认为自己会独占系统的所有资源(内存)![]()
程序是如何变成进程的?
程序被编译出来,没有被加载的时候,程序内部是有地址和区域的。当被加载到内存时,其实是将磁盘中的相对地址加载到内存经转换后的物理地址(这个转换依据是内存的起始地址)程序内部的地址和加载到内存后的地址是没有关系的!编译程序的时候,就认为程序是按照0000000~FFFFFFFF进行编址的。
虚拟地址空间,不仅仅是OS会考虑,编译器也会考虑!虚拟地址与物理地址在页表会建立映射关系!
虚拟地址空间是什么?
一个可执行程序是如何加载到内存的?
为什么要有虚拟地址空间?
直接访问物理地址是不安全的,为访问内存添加了一层软硬件层,可以对转化过程进行审核,非法的访问,就可以被直接拦截了!其意义:
1.保护内存
2.通过地址空间,将进程管理与Linux内存管理进行功能模块的解耦!
3.让进程或程序以统一的视角看待内存,方便以统一的方式来编译、加载所有的可执行程序!简化进程本身的设计与实现!
上图是Linux2.6内核中进程队列的数据结构,之间关系也已经给大家画出来,方便大家理解6.1 一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的负载均衡问题6.2 优先级
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)实时优先级:0~99(不关心)6.3 活动队列
时间片还没有结束的所有进程都按照优先级放在该队列nr_active: 总共有多少个运行状态的进程queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!从该结构中,选择一个最合适的进程,过程是怎么的呢?1. 从0下表开始遍历queue[140]2. 找到第一个非空队列,该队列必定为优先级最高的队列3. 拿到选中队列的第一个进程,开始运行,调度完成!4. 遍历queue[140]时间复杂度是常数!但还是太低效了!bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!6.4 过期队列
过期队列和活动队列结构一模一样过期队列上放置的进程,都是时间片耗尽的进程当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算6.5 active指针和expired指针
active指针永远指向活动队列expired指针永远指向过期队列可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!6.6 总结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法