回顾:
进程概念:进程就是运行中的程序,在系统角度,是对程序运行过程的动态描述(标识符,上下文数据,内存指针...),在linux下就是一个task_struct结构体,也被称为pcb--进程控制块;通过这个pcb系统对程序进行管理和调度。
进程状态:表示当前进程该如何被管理以及调度。
运行,可中断休眠,不可中断休眠,停止,僵尸.
僵尸进程:处于僵尸态的进程,一个退出运行但资源未被完全释放的进程。
产生:子进程先于父进程退出,为了保存退出返回值,因此没有完全释放资源。
危害:资源泄露。(一个用户所能创建的进程数量,内存资源...)
避免:进程等待。
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程。
特性:运行在后台,父进程成为1号进程。
孤儿进程退出后就不会成为僵尸进程。
进程创建
fork函数
头文件:#include<unisd.h>
函数原型:pid_t fork(void)
返回值:子进程返回0,父进程返回子进程的pid,出错返回0
进程调用fork后,内核将:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程至系统进程列表中
- fork返回,开始调度器调度
写时拷贝
通常父子代码共享,父子不在分别写入,数据也是共享的,但当任意一方试图写入时,便以写时拷贝的方式各自一份副本。
子进程复制了父进程的大部分信息,因此子进程有自己的变量,但是通过页表映射后与父进程访问的是同一物理地址,当这块内存空间中的数据即将要修改,则给子进程重新开辟内存空间,并拷贝数据。
作用:为了提升子进程的创建效率,避免不必要的内存消耗。
例:malloc函数:动态申请一块空间--其实只是先分配一个虚拟地址(物理地址并没有直接被开辟),当第一次修改空间数据时才会被分配。
环境变量
环境变量:保存了当前程序运行环境参数的变量
优点:即时生效;让运行环境配置更加灵活,通过环境变量可以给程序传递数据。
终端shell中的变量其实有两种:环境变量,普通变量
环境变量居有进程间的继承特性,但普通变量没有。
接口:char *getenv(char *name);
指令:
- env:查看所有环境变量
- echo:打印指定内容
- export:声明一个变量为环境变量
- set:查看所有变量
- unset:删除一个变量
在程序中访问环境变量:
- getenv()接口
- main函数第三个参数 mian(int argc,char* argv,char* env)
- 全局变量:extern char** environ
程序地址空间
程序地址空间:进程的虚拟地址空间。
系统为每一个进程运行通过mm_struct结构体(linux)下所描述的一个虚拟的,连续的,完整的地址空间,其实我们进程中访问内存时看到的地址都是虚拟地址,经过页表映射之后得到物理地址进而访问物理地址。
好处:
- 每一个进程都有一个完整独立的虚拟地址空间,则地址可以随便使用,不担心使用冲突,地址管理更加方便。
- 经过页表映射可以将数据存储在物理地址的任意位置,实现通过页表的离散式存储,提高物理内存的利用率。
- 在进行页表映射的之后可以进行访问权限的控制。
内存管理方式
- 分段式内存管理
- 分页式内存管理
- 段页式内存管理
分段式内存管理:将一个整体的地址空间划分为多个段(代码段,数据段,堆区,栈....)
作用:更加利于编译器对于地址的管理。
重要的两要素:段表,地址组成。
虚拟地址组成:段号,段中的偏移量。
段表:是一种数据结构,其中描述的信息,段号,物理内存的一个起始地址。
通过段号找到段表项,得到一块物理内存的起始地址。
物理内存起始地址+偏移量就是实际数据存储在物理内存中的位置。
分页式内存管理:将一个整体的地址空间划分为大量的小分页page(一般默认都是4096字节为一页)。
作用:实现数据的离散式存储,提高内存利用率。
两个关键要素:页表+虚拟地址组成
页表,页号,物理内存块起始地址,缺页中断位,访问权限位...
通过虚拟地址中的页号,在页表中找到对应的页表项,得到一个物理地址的起始地址,加上页内偏移就是实际访问位置。
不同于分段式管理,分段式利于地址管理,而分页式更倾向于碎片化管理,提高内存利用率以及内存访问控制。
段页式内存管理:先将地址空间分段,然后对每一分段进行分页式管理。
访问权限位:标记当前的地址能够进行什么样的操作
比如:0号地址,也就是NULL,这个地址既不可读,也不可写,因此一旦解引用或则修改内存就会报错。
比如:const修饰的常变量,具有常量特性,其实就是访问权限上只读。
缺页中断:当进程要访问一块内存的时候,经过页表映射是发现缺页中断位被置位(这个地址原先的数据不在内存中),则触发缺页中断。
内存交换:一个程序运行要占据大量内存,但是物理内存是有限的,意味这内存总有耗尽的时候,如果没有内存交换技术,则内存耗尽,则意味这新程序没法运行。因此产生了一些需要,按照某种特定规则,将内存中不常用的数据从内存中移出,存放到硬盘,然后腾出内存供当前程序使用。但当我们要访问数据时,会发现我们要访问的数据就是刚移出去的数据,则会触发缺页中断。
触发缺页中断后,就需要重新进行内存置换,将数据中交换分区换到内存中进行访问。
内存置换算法:内存不够的时候,把哪些数据移出放到交换分区上存储
经典算法:LRU--最久未使用 LFU--最少未使用
一旦产生大量的内存置换,运行效率就会降低,因为硬盘的吞吐量是非常小的。
交换分区是安装系统时分配好的,硬盘的一部分被分为交换分区,一部分被分为文件系统分区(文件存储),交换分区没有固定大小,一般是内存的2倍。
退出
如何中止一个进程呢?
- 在main函数中return。 仅在main函数中使用时退出运行程序。 return是中止一个函数,并返回一个数据;main函数是程序的入口函数,入口函数一旦退出,程序运行就会终止。
库函数:void exit(int retval) 在任意位置调用,都可以退出程序运行。系统调用接口是操纵系统向上层提供的用于访问内核的接口,功能相对单一。大佬针对经典场景,对系统调用接口进行封装,封装出了适用于典型场景的库函数。
系统调用接口:void _exit(int retval) 在任意位置调用,都可以退出程序运行。
exit和_exit的区别是在于程序运行前,是否会将缓冲区中的数据进行刷新写入文件中。
进程退出返回值的意义:
return和exit给与的数据其实就是进程的退出码。
作用:一个程序运行起来肯定是为了完成一个任务,但是这个任务完成的如何外界是如何得知呢?
因此就必须有这个程序的退出码,来表示当前进程处理的结果。