Linux:简单聊聊线程调度

目录

  • 一、什么是线程调度?
  • 二、什么时候会进行线程调度?
  • 三、什么原因会导致某个线程调度延迟?
  • 四、内核抢占和抢占式调度
  • 五、应用层线程优先级设置过高会导致什么问题?

   在工作中,线程调度这个词我们经常提及,但是关于其稍微详细一点的知识很多人并不知道。我也一样,所以简单学习整理一下,在此分享!

一、什么是线程调度?

  在 Linux 中,线程调度是由内核管理的过程。内核负责根据预定义的调度策略和优先级来决定哪个线程在给定的时间点运行。它的目标是提高系统的并发能力、公平性和响应性。下面是一些关于 Linux 线程调度的详细信息:

  调度策略:Linux 提供了几种线程调度策略,包括:

  • SCHED_OTHER(默认):基于时间片轮转的策略,允许多个线程共享 CPU 时间。
  • SCHED_FIFO:先进先出策略,优先级高的线程会一直运行,直到它主动放弃 CPU。
  • SCHED_RR:基于时间片轮转的策略,优先级高的线程运行一段时间后被抢占。
Linux的调度策略主要有以下几种:

2. O(1)调度策略:在Linux内核版本2.6之前,使用了O(1)调度策略。该策略利用了一个运行队列
数组,按照优先级将任务分配到不同的队列中,并使用时间片轮转的方式进行调度。然而,O(1)调
度策略在多核系统中存在性能问题,因此在2.6版本之后被淘汰。

3. CFS调度策略:从Linux内核版本2.6.23开始,引入了CFS(Completely Fair Scheduler,完
全公平调度器)调度策略。CFS通过红黑树的数据结构来维护任务队列,每个任务都被赋予一个虚拟
运行时间。CFS追求公平性,即使在多核系统中也可以保证任务的公平调度。

4. BFS调度策略:BFS(Brain Fuck Scheduler,天才调度器)是一种第三方的调度策略,主要
针对桌面应用和交互式应用的响应性能做了优化。BFS调度策略采用一种简单的先进先出(FIFO)
调度算法,并通过调整任务的优先级来提高交互式应用的响应速度。

5. 实时调度策略:Linux内核还支持实时调度策略,包括FIFO(先进先出)和RR(时间片轮转)。
实时调度策略主要用于对时间敏感的应用,如音频和视频处理。

关于Linux调度策略的历史演变,最早的Linux内核并没有较为复杂的调度策略,主要使用了简单
的时间片轮转调度算法。随着Linux的发展和应用场景的变化,人们对调度策略的需求也越来越高。
因此,Linux内核在不同的版本中不断优化和改进调度策略,引入了O(1)调度策略和CFS调度策略,
以满足不同应用的需求。此外,第三方的调度策略如BFS也为Linux用户提供了更多的选择。

  优先级:每个线程都被分配了一个优先级,较高优先级的线程将优先获得CPU的使用权。优先级的范围通常是0-139,其中0是最高优先级,139是最低优先级。0-99是实时线程。100-139为普通线程。该优先级数值是从内核角度来看的。应用层优先级是数值越大优先级越高。

  调度相关系统调用:在编写多线程程序时,可以使用以下系统调用来控制线程的行为:

在 Linux 系统中,线程调度相关的 API 函数主要包括以下几个:

1. sched_yield():该函数允许一个线程主动放弃 CPU 时间片,让系统调度器重新选择一
个新的可运行线程来执行。调用该函数后,当前线程会进入可运行状态并让出 CPU。

2. sched_setscheduler():该函数用于设置指定线程的调度策略。它接受三个参数:线程
标识符,调度策略和相关参数。常见的调度策略包括 SCHED_FIFO、SCHED_RR 和 SCHED_OTHER。

3. sched_getscheduler():该函数用于获取指定线程的当前调度策略。它接受一个参数:线程
标识符。

4. sched_get_priority_max()sched_get_priority_min():这两个函数分别
用于获取指定调度策略下的最大和最小优先级值。它们接受一个参数:调度策略。

5. pthread_setschedparam():该函数用于设置指定线程的调度参数,包括调度策略和优先级。
它接受三个参数:线程标识符,调度策略和优先级。

6. pthread_getschedparam():该函数用于获取指定线程的当前调度参数,包括调度策略和优先
级。它接受两个参数:线程标识符和一个用于存储调度参数的结构体。

这些 API 函数可以通过编程语言提供的相应接口来调用,如 C 语言中的 `<sched.h>` 头文件和 
POSIX 线程 (`pthread`) 相关的函数。它们允许开发者根据需求设置线程的调度策略和优先级,
以及与线程调度相关的其他操作。请注意,在使用这些函数时,需要具备相应的权限和了解相关的
调度策略和参数设置。
#include 
#include 
#include 
#include 

void *thread_func(void *arg)
{
    int thread_id = *(int *)arg;
    printf("Thread %d is running\n", thread_id);
    // 线程执行任务

    return NULL;
}

int main()
{
    pthread_t thread1, thread2;
    int thread1_id = 1, thread2_id = 2;
    int sched_policy;
    struct sched_param sched_param;

    // 创建线程1
    pthread_create(&thread1, NULL, thread_func, (void *)&thread1_id);

    // 创建线程2
    pthread_create(&thread2, NULL, thread_func, (void *)&thread2_id);

    // 设置线程1的调度策略为 SCHED_FIFO,优先级为 80
    sched_policy = SCHED_FIFO;
    sched_param.sched_priority = 80;
    pthread_setschedparam(thread1, sched_policy, &sched_param);

    // 获取线程1的当前调度策略和优先级
    pthread_getschedparam(thread1, &sched_policy, &sched_param);
    printf("Thread 1 - Policy: %d, Priority: %d\n", sched_policy, sched_param.sched_priority);

    // 设置线程2的调度策略为 SCHED_RR,优先级为 50
    sched_policy = SCHED_RR;
    sched_param.sched_priority = 50;
    pthread_setschedparam(thread2, sched_policy, &sched_param);

    // 获取线程2的当前调度策略和优先级
    pthread_getschedparam(thread2, &sched_policy, &sched_param);
    printf("Thread 2 - Policy: %d, Priority: %d\n", sched_policy, sched_param.sched_priority);

    // 等待线程1和线程2结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

  线程亲和性:Linux 还支持线程亲和性,允许将线程绑定到特定的 CPU 核心,以提高缓存利用和性能。

二、什么时候会进行线程调度?

  在多线程编程中,线程可能会因为以下几种情况而被切换调度:

  1. 时间片用完:操作系统为每个线程分配一个时间片,当线程的时间片用完时,操作系统会将调度权交给其他线程。
  2. 阻塞操作:当线程执行了一个阻塞操作,例如等待输入、等待磁盘读写等,操作系统会将线程切换到阻塞状态,同时调度其他可运行线程。
  3. 优先级调度:线程的优先级可能会影响调度。当高优先级的线程就绪时,操作系统可能会将其切换到运行状态,同时调度低优先级的线程。
  4. 睡眠和唤醒:线程可以通过调用 sleep() 或 wait() 等方法主动让出CPU,进入睡眠状态。当休眠时间到期或条件满足时,线程会被唤醒并重新调度。
  5. 中断处理:当线程遇到硬件中断时,操作系统可能会中断当前线程的执行,切换到中断处理程序的上下文。

  需要注意的是,线程切换调度具体的实现方式和调度算法取决于操作系统。不同的操作系统可能有不同的调度机制和策略。以上列出的情况只是一些常见的导致线程切换调度的操作,具体行为可能会因操作系统的不同而有所差异。

三、什么原因会导致某个线程调度延迟?

  1. 调度器繁忙: 其他正在运行的线程过多,导致线程b等待的时间增加。在高负载系统中,调度器可能需要更多的时间来决定要切换到哪个线程。

  2. 锁竞争: 线程b所需要的资源(如共享的锁)可能正在被其他线程使用,导致线程b需要等待。这可能会导致线程切换的延迟增加。

  3. I/O操作阻塞: 线程b可能因为等待某个I/O操作(如磁盘读取、网络请求等)而被阻塞。这可能导致线程b等待的时间增加,从而延长了整个线程切换的时间。

  4. 优先级调度: 线程b的优先级可能较低,导致它在其他线程之后才能得到执行,这可能会延长线程切换的时间。

  5. 系统负载: 系统的整体负载可能较重,包括CPU、内存、I/O等资源都处于高负载状态,这可能导致线程切换的延迟增加。

  6. 频繁中断: 如果在某个线程调度前,频繁发生了多次中断,系统会优先处理中断,此时并不会去调度线程。

  综上所述,线程调度延迟的增加可能与系统的负载、资源竞争、优先级、I/O阻塞等因素有关。针对具体场景,可以通过性能分析工具、系统监控工具等来深入分析和定位问题所在。

四、内核抢占和抢占式调度

  内核抢占和抢占式调度是两个概念,在 Linux 中它们有不同的含义和作用:

  1. 内核抢占(Kernel Preemption):指的是在内核代码执行期间,允许更高优先级的内核代码打断当前正在执行的内核代码。这种抢占可以确保即使在内核空间执行期间,也能及时响应高优先级的内核任务。内核抢占主要涉及操作系统内核本身的代码执行,确保了内核的实时响应性和稳定性。

  2. 抢占式调度(Preemptive Scheduling):指的是操作系统内核允许更高优先级的进程或线程抢占当前正在执行的进程或线程。这种抢占保证了即使用户空间代码正在执行,也能及时响应更高优先级的任务请求。抢占式调度主要是面向用户空间的任务调度,提高了系统的实时性和并发处理能力。

  因此,内核抢占和抢占式调度是针对不同层面的任务调度和响应机制,各自有着独特的作用和意义。内核抢占确保了操作系统内核对内核级任务的即时响应,而抢占式调度则提供了用户空间任务对 CPU 的抢占能力,保证了系统的并发处理能力和实时性能。

小知识学习:什么是高负载
  高负载是指系统或设备承担的工作量或负荷过大,超出了其正常或设计能力范围的情况。在计算机领域,高负载通常指的是系统所承受的并发请求或任务量非常大,导致系统性能下降,响应时间延长,甚至出现系统崩溃或死机等问题。高负载可能由于资源不足、处理能力不足、网络拥堵、数据库负载过大等原因引起。

五、应用层线程优先级设置过高会导致什么问题?

  在 Linux 应用层将线程优先级设置得过高(比如设为 80-99 之间的数值)可能会导致以下问题:

  1. 系统整体性能下降: 设置线程的优先级过高会导致该线程相对于其他线程更容易获得 CPU 时间片,这可能会导致其他任务得到的 CPU 时间变少,从而影响系统整体的响应能力和性能。

  2. 饥饿问题: 高优先级线程有可能将其他低优先级线程“饿死”,即低优先级线程无法获得足够的CPU时间来执行,这可能导致系统中的某些任务无法得到有效执行,导致系统性能不均衡。

  3. 系统不稳定: 如果大量线程都将优先级设置得过高,导致 CPU 时间持续被高优先级线程占用,可能导致系统变得不稳定,甚至无法响应其他任务(包括系统关键任务)的请求。

  4. 实时性能下降: 如果系统中存在实时任务,将非实时任务的优先级设置得过高可能会导致实时任务无法按时得到执行,从而影响实时性能。

  因此,建议在设置线程优先级时,要谨慎考虑,并确保综合考虑系统的整体负载情况和各个线程的重要性,避免将线程优先级设置得过高,特别是在应用层。优先保证系统的整体稳定性和公平性,避免出现过度竞争和系统性能下降的问题。

  在 Linux 中,将应用层线程的优先级设置为最高值 99 通常不会直接影响系统内核线程的运行。内核线程通常具有更高的特权级别,并受到操作系统内核的管理和保护,因此不太容易受到应用层线程优先级设置的直接影响。

  欢迎大家指导和交流!如果我有任何错误或遗漏,请立即指正,我愿意学习改进。期待与大家一起进步!

你可能感兴趣的:(Linux,API编程,linux,服务器,功能测试,嵌入式)