文章目录
1 操作系统
1.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
1.2设计操作系统的目的
①对下:与硬件交互,管理所有的软硬件资源
②对上:为用户程序(应用程序)提供一个良好的执行环境
理解:操作系统就是一款纯正“搞管理”的软件,对软硬件资源进行管理
如何理解管理:即先描述被管理对象(struct),然后组织被管理对象(数据结构)
2进程
2.1 进程是什么
进程=对应的代码和数据(程序加载进内存)+进程对应的PCB结构体
2.2 如何管理进程
运行中的系统会存在大量的进程,操作系统会如何管理这些进程呢?
仍然是先描述,再组织
描述是指:进程在形成之初,操作系统就会为其创建进程控制块PCB,PCB用于描述进程,里面存储着进程的所有属性,可以理解为进程属性的集合。
linux下的PCB叫做叫做 task_struct
组织是指:通过双链表的形式将大量的进程链接起来。这也再一次对应了操作系统进行管理的本质
2.2查看进程
例如:要获取PID为1的进程信息,你需要/proc/1这个文件夹
2.3 程序中获取自己的pid
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
printf("pid: %d\n",getpid());
printf("ppid:%d\n",getppid());
}
return 0;
}
2.4 创建一个进程
fork()函数,创建一个子进程
返回值 创建失败返回-1 ,创建成功给父进程返回子进程的pid,给子进程返回0
fork之后,父子进程代码共享,数据各自开辟空间
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int ret=fork();
if(ret<0)
{
perror("fork");
return 1;
}
else if(ret==0)//子进程
{
while(1)
{
printf("I am child .pid: %d , ppid:%d,ret: %d\n ",getpid(),getppid(),ret );
sleep(1);
}
}
else//父进程
{
while(1)
{
printf("I am father.pid: %d,ppid:%d,ret: %d\n",getpid(),getppid(),ret);
sleep(1);
}
}
return 0;
}
理解:
①父子进程被创建出来,谁先执行不一定,由操作系统的调度器来决定
②创建子进程成功后,为什么会有两个返回值?
在fork函数内部,当开始return的时候,核心代码已经执行完毕,所以此时已经有了父子进程,父子各自会执行自己的return语句,也就是给父进程返回的是子进程的pid,给子进程返回0
2.5 进程状态
状态 | 解释 |
---|---|
新建态 | 进程刚被创建 |
运行态 | task_struct结构体在运行队列中排队 |
阻塞态 | 等待非cpu资源就绪(阻塞队列) |
挂起态 | 内存不足时,OS通过适当的置换进程的代码和数据到磁盘 |
linux下的进程
状态 | 解释 |
---|---|
R | 对应运行态 |
S | 对应阻塞态,可中断睡眠 |
D | 睡眠状态,磁盘睡眠,深度睡眠,不可被中断,不可以被被动唤醒 |
T | 暂停状态,进行调试 |
X | 终止态 |
Z | 僵尸状态,一个进程已经退出,但是还不允许操作系统释放(PCB未释放,代码和数据可以被释放),处于一个被检测的状态 |
僵尸进程
①僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程
②僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
代码演示
#include<stdio.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
}
else if(id==0)
{
while(1)
{
printf("I am child pid: %d,ppid: %d\n",getpid(),getppid());
sleep(3);
break;
}
exit(0);
}
else{
while(1)
{
printf("I am parent pid: %d,ppid: %d\n",getpid(),getppid());
sleep(1) ;
}
}
return 0;
}
僵尸进程的危害
维护退出状态本身就是要用数据去维护,所以这个状态信息就会一直保存在task_struct中,即如果Z状态一直不退出,那么PCB就一直要维护。那么当父进程创建了很多子进程,一直不回收,就会造成内存资源的浪费,即内存泄露。
再来认识下另外一种进程-孤儿进程
如果父进程退出,子进程还在,子进程就叫孤儿进程
孤儿进程会被1号进程领养
来段代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
}
else if(id==0)
{
while(1)
{
printf("I am child\n");
sleep(1);
}
}
else{
while(1)
{
printf("I am father\n");
sleep(3);
break;
}
exit(0);
}
return 0;
}
2.6 进程优先级
2.6.1为什么要有优先级
因为cpu是有限的,进程太多,需要通过某种方式来竞争资源
2.6.2 什么是优先级
用一些数据来确认谁先获得某种资源,谁后获得
2.6.3 linux下优先级的做法
优先级=老的优先级(PRI)+nice值(NI)
PRI:进程的优先级,也就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
NI:表示进程可被执行的优先级的修正数值,当nice值为负值的时候,那么该程序的优先级值将变小,即其优先级会变高,则其越快被执行,nice 值为正值时则相反。
所以,调整进程优先级,在Linux下,就是调整进程nice值(因为PRI值每次都是80),nice其取值范围是-20至19,一共40个级别。
2.8 其他概念
① 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
②独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
③并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行
④并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
3进程地址空间
3.1 初识地址空间
这是我们写代码的时候的空间布局图
先在linux下验证下
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_unval;
int g_val=100;
int main(int argc,char* argv[],char*env[])
{
const char* str="hello";
printf("code addr:%p\n",main);
printf("init global addr:%p\n",&g_val);
printf("uninit global addr:%p\n",&g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem);
printf("heap addr: %p\n", heap_mem1);
printf("heap addr: %p\n", heap_mem2);
printf("heap addr: %p\n", heap_mem3);
printf("test staic addr: %p\n", &test);
printf("stack addr: %p\n", &heap_mem);
printf("stack addr: %p\n", &heap_mem1);
printf("stack addr: %p\n", &heap_mem2);
printf("stack addr: %p\n", &heap_mem3);
printf("read only string addr: %p\n", str);
int i,j;
for( i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for( j = 0; env[j]; j++)
{
printf("env[%d]: %p\n",j, env[j]);
}
return 0;
}
仔细观察堆区和栈区,可以发现堆区和栈区是相对而生,
堆是向高地址方向生长,栈是向低地址方向生长
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_val=100;
int main()
{
pid_t id=fork();
if(id==0)
{
g_val=200;
printf("child[%d]:%d,%p\n",getpid(),g_val,&g_val);
}
else
{
printf("parent[%d]:%d,%p\n",getpid(),g_val,&g_val);
}
return 0;
}
查看运行结果,发现了一个很奇怪的问题,父子进程,g_val的输出地址是一样的,但是
变量的值却不一样。
所以得出如下结论
①变量的值不一样,所以父子进程输出的变量绝对不是同一个变量
②打印出的地址值是一样的,说明这个地址并不是实际的物理地址。
在linux下,这种地址叫做虚拟地址(进程虚拟地址空间)
地址空间是一种内核数据结构,每一个进程都拥有一个进程地址空间,它里面通过起始和结束下标来确定各个区域的划分。而操作系统通过页表和MMU(内存管理单元)将虚拟地址和物理地址关联起来。
页表是操作系统提供的一张映射表,其左侧存放虚拟地址,右侧存放实际地址。
这张图就可以很好的解释之前的问题,在创建子进程的时候,会先以父进程为模板,继承父进程的代码数据以及进程数据结构,所以父子进程的g_val的虚拟地址是一样的,但当子进程要修改数据的时候,操作系统才会在物理内存上开辟一块新的空间,通过页表映射机制,将子进程g_val的实际地址与虚拟地址联系起来。这也叫做写时拷贝。
3.2进程地址空间的意义
①有效保护了物理内存中的所有合法数据以及进程,内核的相关数据(因为地址空间和页表都是OS创建并维护的,所以想要使用地址空间和页表进行维护,也一定要在OS的监管下,如果是非法的访问或者映射,OS都会识别,并终止这个进程)
②内存管理模块和进程管理模块完成了解耦合
因为地址空间的存在,所以我们使用new或者malloc申请空间的时候,实际是在地址空间上申请的,物理内存可以一个字节都先不分配,当真正对物理地址空间访问的时候,再执行相关的算法,申请内存,内存访问是由操作系统完成的,进程几乎0感知,这种延迟分配的策略,会提高整机的效率,并且内存管理模块和进程模块做到了没有任何关系。
③内存分布有序化
因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角的所有内存分布,都是有序的。
④实现进程独立性
因为有地址空间的存在,每一个进程都认为自己拥有4GB的空间,不需要知道其他进程的存在,只通过页表映射到不同的区域,来实现进程的独立性。