Linux进程管理

1.1 进程由来

操作系统进行资源分配和调度的一个独立单位,

1.2 进程描述符

进程是操作系统中调度的实体,需要对进程所必须拥有的资源做抽象描述,这种抽象描述叫进程控制块,Process Control Block PCB,需要描述的信息

  • 进程运行状态:就绪 运行 等待阻塞 僵尸
  • 程序计数器:记录当前进程运行到哪条指令了
  • cpu寄存器:主要保存当前运行的上下文,记录cpu所有必须保存下来的寄存器信息,一遍调度出去之后还能调度回来继续运行
  • cpu调度信息:进程优先级,调度队列,调度相关信息
  • 内存管理信息:进程使用的内存信息,如进程页表
  • 统计信息:进程运行时间等相关统计信息
  • 文件相关信息:进程打开的文件。

进程控制块,task_struct 数据结构很大,在进程生命周期内,进程要和内核很多模块进行交互,比如内存管理,进程调度以及文件系统。内核用链表task_list存放所有进程描述符,task_struct包含内容:

  • 进程属性相关信息
  • 进程间的关系
  • 进程调度相关信息
  • 内存管理相关信息
  • 文件管理相关信息
  • 信号的相关信息
  • 资源限制的相关信息

1.3 进程生命周期

  • TASK_RUNNING 就绪态
  • TASK_INTERRUPTIBLE 可中断睡眠态
  • TASK_UNINTERRUPTIBLE 不可中断态
  • __TASK_STOPOPED 终止态
  • EXIT_ZOMBIE 僵尸态

1.4 进程标识

Process Identifier pid bitmap机制,保证每个进程创建时都能分配到唯一的号码

线程组

1.5 进程间的家族关系

1.6 获取当前进程

2 进程的创建与终止

2.1 写时复制技术

创建新进程时复制父进程所拥有的所有资源, 写时复制 copy on write COW,父进程创建子进程时不需要复制进程地址空间的内容给子进程,只需要复制父进程的进程地址空间的页表给子进程,这样,子进程就可以共享相同的物理内存,当父子进程有一方需要修改某个物理页面的内容时,触发写保护的缺页异常,然后才把共享页面的内容复制出来,从而让父子进程拥有各自的副本。也就是说,地址空间以支付方式共享,当需要写入时才发生复制。

写时复制可以推迟甚至避免复制数据。fork() 创建一个新进程的开销变得很小,之前需要复制父进程整个地址空间, 现在只需要复制父进程的页表,开销很小。

2.2 fork()函数

子进程和父进程拥有各自独立的进程地址空间,但是共享物理内存资源,包括:进程上下文,进程栈,内存信息,打开的文件描述符,进程优先级,根目录,资源限制,控制中断等,创建期间,父子进程共享物理内存空间,当他们开始运行各自程序时,他们的进程地址空间开始分道扬镳,得益于写时复制技术的优势。

父子进程区别:

  • pid不同
  • 子进程不会继承父进程内存方面的锁
  • 子进程不会继承父进程的一些定时器
  • 不会继承父进程的信号量

2.3 vfork()函数

vfork vs fork

  1. fork()子进程拷贝父进程的 数据段 代码段, vfork() 子进程和父进程共享数据段
  2. fork() 父子进程执行次序不确定 ,vfork() 保证子进程先运行,调用exec或exit之前与父进程数据时共享的,调用exec或exit之后父进程才可能被调度执行。
  3. vfork保证子进程先运行,调用exec或exit之后父进程才可能被调度执行,如果在调用这两个函数之前子进程依赖父进程的进一步动作,会导致死锁。

2.4 clone函数

进程的四要素:
(1)有一段程序供其执行(不一定是一个进程所专有的),就像一场戏必须有自己的剧本。
(2)有自己的专用系统堆栈空间(私有财产)
(3)有进程控制块(task_struct)(“有身份证,PID”)
(4)有独立的存储空间。
缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线程

clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。

2.5 内核线程

内核经常需要在后台执行一些操作,这种任务就可以通过内核线程(kernle thread)完成,内核线程是独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。

2.6 do_fork()函数

操作系统通过系统调用fork(),vfork()和clone()函数来完成进程的创建,最终都调用了内核函数do_fork(),

2.7 终止进程

两种方式:

  • 主动终止,显式执行exit系统调用,或者主函数返回
    • main函数返回自动添加exit系统调用
    • 主动执行exit系统调用
  • 被动终止,接收到终止信号,或异常时终止
    • 进程收到一个自己不能处理的信号
    • 进程在内核态执行时产生了一个异常
    • 进程收到SIGKILL等终止信号
  • 终止时候释放占有的资源,并把消息告诉父进程
    • 先于父进程终止,子进程变成僵尸进程,直到父进程调用wait才算最终消亡,
    • 父进程之后终止,init进程称为子进程新的父进程

2.8 僵尸进程和托孤进程

僵尸进程产生的过程是:父进程调用 fork() 创建子进程后,子进程运行直至其终止,它立即从内存中移除,但进程描述符仍然保留在内存中(进程描述符占有极少的内存空间)。子进程的状态变成 EXIT_ZOMBIE,并且向父进程发送 SIGCHLD 信号,父进程此时应该调用 wait() 系 统调用来获取子进程的退出状态以及其它的信息。在 wait() 调用之后,僵尸进程就完全从内存中移除。因此一个僵尸进程存在于其终止到父进程调用 wait() 等函数这个时间的间隙,一般很快就消失,但如果编程不合理(父进程是个死循环),父进程从不调用 wait() 等系统调用来收集僵尸进程,那么这些进程会一直存在内存中。清除僵尸进程的两种方式:改写父进程,杀死父进程,子进程过继给1号进程init。

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了 init 进程(或者 systemd 进程)身上

2.9 进程0和进程1

进程0:别名 idle进程,swapper进程。 内核线程,系统创建的第一个进程,并且还是唯一一个没有通过kernel_thread以及它所创建的子类进程所创建的进程,在进程调度中起着重要作用。

进程1:init进程,初始化系统配置

3 进程调度

进程调度器:一个进程拥有处理器资源,其他进程只能在就绪队列runqueue队列中等待,等到处理器空闲才有机会获取处理器资源并运行。操作系统在众多的就绪进程中选择要给最合适的进程。一个进程运行过程有可能需要等待某些资源,比如磁盘操作完成,等待键盘输入,等待物理页面的分配。如果处理器和进程一起等待,很明显浪费处理器资源,所以一个进程在睡眠等待时,调度器可以调度其他进程来运行,提高处理器的利用率。

3.1 进程分类

  • cpu消耗型 CPU-Bound
    • 大部分时间用来执行代码上,一直占用cpu,while循环。大量数学计算 matlab
  • I/O消耗型 I/O-Bound
    • 大部分时间在提交I/O请求或者等待I/O请求。只需要很少的处理器计算资源即可,如需要键盘输入的进程,等待网络I/O的过程。

3.2 进程优先级和权重

经典的调度算法时基于优先级调度,nice值 0~99 实时进程,100~139 普通进程,task_struct 有四个成员描述进程优先级

  • static_prio 静态优先级,进程启动时分配,内核不存储nice值,而是static_prio,静态,不随时间而改变
  • normal_prio 基于静态优先级策略计算出来的优先级 ,创建时继承自父进程,普通进程等同static_prio,对于实时进程,会根据rt_priority,重新计算normal_prio
  • prio 保存进程的动态优先级
  • rt_priority

除了优先级表示进程的轻重缓急,调度器还有权重的概念来表示进程的优先级

3.3 调度策略

schedule policy,内核把相同的调度策略抽象成调度类,schedule class,不同的进程采用不同的调度策略,内核中默认实现了5个调度类,

调度类调度策略适用范围说明
stop最高优先级,比deadline进程优先级高- 可以抢占任何进程
内核线程,优先级最高
负载均衡机制的进程迁移,cpu热插拔,RCU等
deadlineSCHED_DEADLINE最高优先级实时进程,优先级-1用于调度有严格时间要求的实时进程,比如视频编解码
realtimeSCHED_FIFO
SCHED_RR
普通实时进程,0~99IRQ线程化
CFSSCHED_NORMAL
SCHED_BATCH
SCHED_IDLE
普通进程 100~139CFS调度
idle最低优先级就绪队列没有其他进程时进入idle调度类,让cpu进入低功耗模式

3.4 时间片

​ time slice,进程调度进来与被调度出去之间,所能持续运行的时间长度。很难确定多长的时间合适,太长导致交互型进程得不到及时响应,过短增大进程切换带来的处理器消耗。I/O消耗性型进程不需要很长的时间片,cpu消耗型希望时间片越长越好

3.5 经典调度算法

3.6 Linux O(n)调度算法

3.7 Linux O(1)调度算法

3.8 Linux CFS算法

3.9 进程切换

3.10 与进程相关的数据结构

4 多核调度

4.1 调度域和调度组

4.2 负载的计算

4.3 负载均衡算法

4.4 Per-CPU变量

  • 进程栈
    • 虚拟地址4G,最高1G内核空间 内核空间,所有进程共享,较低3G用户空间,每个进程通过系统调用进入内核态。
      • 程序段:可执行文件代码的内存映射
      • 数据段:可执行文件已初始化全局变量的内存映射
      • BSS段:未初始化全局变量或者静态变量
      • 堆区:存储动态内存分配,匿名的内存映射
      • 栈区: 进程用户空间栈,由编译器自动分配释放,存放函数参数值,局部变量的值 此即为进程栈
      • 映射段:任何内存映射文件
    • 进程栈初始化大小由编译器和链接器计算出来
  • 线程栈
    • 从内核角度看,并没有线程的概念,Linux把所有线程都当作进程来实现。它将线程和进程不加区分的统一到task_struct,线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎时进程和Linux线程唯一的区别。线程创建的时候加上CLONE_VM标记,这样线程的内存描述符将直接指向父进程的内存描述符。
  • 内核栈
    • 在每个进程的生命周期中,在执行系统调用陷入内核态之后, 内核代码所使用的栈并不是原先进程用户空间的栈,而是一个单独内核空间的栈,这个称作进程内核栈。进程内核栈在进程创建的时候通过slab分配器从thread_info_cache缓存池中分配出来,大小为THREAD_SIZE,一般来说就是一个页的大小4K
  • 中断栈
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值