由于我们的硬件环境中cpu的个数有限,通常只有1个或者几个,但需要运行的进程/线程有几十上百个,甚至更多。为了实现所有进程/线程都能执行,linux操作系统需要实现进程调度,即将当前进程挂起,并选取下一个需要执行的进程运行。
进程分类
从进程调度需求角度考虑,根据进程运行特点可以分为交互式进程(I/O消耗型)和批处理进程(CPU消耗型)。
交互式进程(I/O消耗型)
有些进程大部分时间在提交I/O请求或者等待I/O请求,这样的进程会频繁地处于可运行状态,但每次运行时间都很短,大部分时间都是因等待更多的I/O请求而处于阻塞状态。这类进程被称为交互式进程,也叫I/O消耗型进程。这里的I/O资源是指任何可阻塞的资源。
典型的交互式进程如键盘输入、网络I/O读取。
- 对于键盘输入读取进程来说,其大部分时间都是在等待用户键入字符输入,处于阻塞状态。只有当用户操作按键时,该进程才会被唤醒去执行程序读取用户操作并进行相应处理。
- 网络数据监控进程也是同理,其调用recvfrom系统调用读取网卡中数据,但通常网卡中没有数据,因此其将大部分时间都处于阻塞状态,等待网卡数据的到来。只有网卡中有数据才会将其唤醒执行数据处理程序。
站在交互式进程角度,它希望系统调度程序能够较频繁地切换执行的进程,这样它的I/O响应速度就很快,符合自身需求。
批处理进程(CPU消耗型)
有些进程很少和用户互动,即很少需要I/O资源,大部分时间都是执行代码程序,除非被抢占,否则它们将一直运行。这类进程被称为批处理进程,也叫CPU消耗型进程。
典型的批处理进程如视频播放、matlab运行程序等。
- 进行视频播放时,大部分时间都是需要cpu去播放视频,基本没有I/O请求。
- 利用matlab模拟仿真时,需要cpu进行大量的计算,也基本没有I/O请求。
站在批处理进程角度,由于它不需要I/O请求资源,只需要一直运行CPU执行程序,因此它希望系统调度程序不要频繁地切换执行的进程,并且每个进程执行的时间较长更好,这样它运行更顺畅,系统的资源利用率也更高。
从上面可以看出,交互式进程和批处理进程对于系统调度需求是不一样的,因此系统最好能针对它们的调度采取不同的调度策略,这样系统的资源利用率、程序运行效率更高。其实,它们的调度策略分别对应CFS调度类中的SCHED_NORMAL和SCHED_BATCH,这个在后面会进一步讲解。
调度策略通常需要在两个矛盾的目标之间找到平衡:进程响应速度和最大的系统利用率。Linux为了保证交互式应用和桌面系统的性能,对进程的响应做了优化,更倾向于优先调度I/O消耗型进程。
调度方式分类
linux进程调度都是通过schedule函数实现。根据调度方式分为抢占式调度和主动调度。
抢占式调度
顾名思义,当前执行的进程还需要继续执行,但是根据调度算法需要进行进程调度抢占,切换成其他进程执行。抢占式调度通常在系统调用返回(ret_fast_syscall)、中断处理返回(ret_to_user)、异常处理返回(ret_to_user)时调用schedule函数进行。
/* 抢占式调度时机 */
ret_fast_syscall/ret_to_user--->do_work_pending--->schedule
抢占式调度通常有如下应用场景:
-
定时器中断处理检测时间片(Time Slice)
linux系统的抢占式调度最主要时通过定时器定时轮询处理完成进程的轮转调度,对应的流程如下。
- 硬件电路的定时器中断(滴答定时器,一般周期10ms)触发,执行对应的中断处理程序。
- 在对应的定时器中断处理程序中,根据进程所采取的调度策略判断当前进程是否需要被抢占。若需要被抢占,则将当前进程的内核栈thread_info中的flags成员设置需要重新调度标志TIF_NEED_RESCHED。
- 在系统调用返回、中断处理返回、异常处理返回时判断当前进程内核栈thread_info中flags成员是否已经设置了需要重新调度标志TIF_NEED_RESCHED。若设置了,则调用schedule函数进行进程调度切换。
-
睡眠进程被唤醒时
我们经常有进程因等待资源导致阻塞,进程处于睡眠状态,如等待资源、同步处理等。当等待的资源得到满足时,该进程将被唤醒,此时可能产生抢占式调度(如该进程优先级比当前正在执行的进程高)。对应的处理流程如下:
- 获取到信号量、消息队列等阻塞的资源时,进程将被唤醒。此时将该进程内核栈thread_info中的flags成员设置需要重新调度标志TIF_NEED_RESCHED。
- 在系统调用返回、中断处理返回、异常处理返回时判断当前进程内核栈thread_info中flags成员是否已经设置了需要重新调度标志。若设置了,则调用schedule函数进行进程调度切换。
主动调度
当进程自身不需要使用cpu时,主动调用schedule函数进行进程调度切换。
主动调度通常有如下应用场景:
-
进程在驱动程序中
- 如调用recvfrom函数读取网卡数据时,若网卡中没有数据,此时进程将阻塞等待,在对应的驱动程序中将放弃CPU,主动调用schedule函数进行进程调度切换。
- 如键盘输入监控进程在用户无按键输入时将阻塞,对应的键盘驱动函数也会主动放弃CPU,主动调用schedule函数进行进程调度切换。
-
进程状态发生变化时
- 如进程主动调用exit函数退出时,在exit函数中将主动调用schedule函数进行进程调度切换。
- 如进程主动调用sleep函数睡眠时,在sleep函数中将主动调用schedule函数进行进程调度切换。
-
进程获取资源阻塞时
- 如进程在获取信号量时,若信号量为不可用状态,此时进程将阻塞,在信号量获取函数semop中会主动调用schedule函数进行进程调度切换。
- 如进程在从消息队列获取消息时,若此时消息队列中没有消息,进程将被阻塞,在消息获取函数msgrcv中会主动调用schedule函数进行进程调度切换。
系统调度方式分类总结: