解码线程调度与信号响应

Linux 线程调度策略

调度核心概念

线程是 Linux 系统调度的最小单位,进程作为线程的容器,可包含一个或多个线程。Linux 内核采用抢占式调度机制:高优先级线程可抢占正在运行的低优先级线程的 CPU 使用权;

image

同优先级线程则通过时间片轮转方式并发执行,每个线程执行固定时间后切换,实现多任务并行效果。

image

优先级分类

静态优先级

静态优先级是线程的固有属性,设置后不可更改,范围为 0~99,数值越大优先级越高。

  • 普通任务:静态优先级固定为 0,无法与系统任务竞争资源,仅能与其他普通任务通过动态优先级竞争。
  • 系统任务:静态优先级范围为 1~99,优先级高于普通任务,可随时抢占普通任务的 CPU 资源。
  • 继承调度属性控制:通过pthread_attr_setinheritsched()pthread_attr_getinheritsched()函数设置 / 获取线程继承属性,决定线程是否继承创建线程的调度属性。
#include <pthread.h>
/**
 * @brief 设置线程属性对象的继承调度属性
 * @param attr 线程属性对象指针,需先通过pthread_attr_init()初始化
 * @param inheritsched 继承策略:PTHREAD_INHERIT_SCHED(继承创建线程属性,忽略attr中调度属性)、PTHREAD_EXPLICIT_SCHED(使用attr中指定的调度属性)
 * @return 成功返回0,失败返回非零错误码
 */
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);

/**
 * @brief 获取线程属性对象的继承调度属性
 * @param attr 已初始化的线程属性对象指针
 * @param inheritsched 用于存储获取到的继承策略的指针
 * @return 成功返回0,失败返回非零错误码
 */
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);

动态优先级

动态优先级通过调整线程的 nice 值实现,系统会根据线程运行行为自动调整:

  • IO 消耗性任务:睡眠时间长、资源占用少,系统会逐步提高其优先级(降低 nice 值)。
  • CPU 消耗性任务:睡眠时间短、资源抢占频繁,系统会逐步降低其优先级(提高 nice 值)。
  • nice 值范围:-20~19,值越小优先级越高,默认值为 0;降低 nice 值(提高优先级)需 root 权限。
  • 手动调整函数:nice()函数用于修改进程(含所有线程)的 nice 值,对 SCHED_FIFO 和 SCHED_RR 策略的实时线程无效。
#include <unistd.h>
/**
 * @brief 修改调用进程的nice值,间接调整动态优先级
 * @param incr 要添加到当前nice值的增量,正数提高nice值(降低优先级),负数降低nice值(提高优先级)
 * @return 成功返回新的nice值 - NZER0,失败返回-1(需检查errno判断是否真的出错)
 * @note 仅对普通任务有效,实时任务(SCHED_FIFO/SCHED_RR)无影响;降低nice值需root权限
 * @warning nice值范围被限制在0~2*(NZER0)-1,超出范围会被截断到对应上限/下限
 */
int nice(int incr);

三种调度策略

SCHED_OTHER(分时调度策略)

  • 默认调度策略,采用 CFS(完全公平调度器)算法,为每个线程维护虚拟时钟 vruntime。
  • 调度逻辑:线程执行时 vruntime 递增,调度器始终选择 vruntime 最小的线程执行;高优先级线程 vruntime 增长慢,获得更多运行机会。
  • 限制:静态优先级必须设为 0,对应普通任务。

SCHED_FIFO(实时先到先服务策略)

  • 实时调度策略,按线程就绪顺序执行,线程一旦运行,直到被更高优先级线程抢占、主动放弃 CPU 或任务完成才释放控制权。
  • 同优先级特性:无时间片限制,高优先级线程运行时,同优先级线程无法抢占,实现资源互斥。
  • 优先级范围:1~99,需 root 权限运行

SCHED_RR(实时轮询调度策略)

  • 实时调度策略,基于时间片轮转,为每个线程分配固定时间片,超时后自动交出 CPU 控制权。
  • 调度逻辑:线程运行至时间片耗尽、被高优先级抢占或主动放弃 CPU 时切换,内核按 FIFO 规则选择下一个就绪线程。
  • 优势:避免单个线程长期占用资源;缺点:任务切换频繁,增加上下文切换开销。
  • 优先级范围:1~99,需 root 权限运行

调度策略设置函数

通过pthread_attr_setschedpolicy()pthread_attr_getschedpolicy()函数设置 / 获取线程调度策略,需配合 PTHREAD_EXPLICIT_SCHED 继承属性才能生效。

#include <pthread.h>
/**
 * @brief 设置线程属性对象的调度策略
 * @param attr 线程属性对象指针,需已初始化
 * @param policy 调度策略,支持SCHED_OTHER、SCHED_FIFO、SCHED_RR三种
 * @return 成功返回0,失败返回非零错误码(如EINVAL表示policy值无效)
 * @note 必须先通过pthread_attr_setinheritsched()设置为PTHREAD_EXPLICIT_SCHED,否则策略设置无效
 */
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

/**
 * @brief 获取线程属性对象的调度策略
 * @param attr 已初始化的线程属性对象指针
 * @param policy 用于存储获取到的调度策略的指针
 * @return 成功返回0,失败返回非零错误码
 */
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);

静态优先级设置函数

通过pthread_attr_setschedparam()pthread_attr_getschedparam()函数设置 / 获取线程静态优先级,需配合 PTHREAD_EXPLICIT_SCHED 继承属性。

#include <pthread.h>
/**
 * @brief 设置线程属性对象的调度参数(主要是静态优先级)
 * @param attr 线程属性对象指针,需已初始化
 * @param param 调度参数结构体指针,其中sched_priority成员指定静态优先级
 * @return 成功返回0,失败返回非零错误码
 * @note 需先设置继承属性为PTHREAD_EXPLICIT_SCHED,否则参数设置无效
 * @note SCHED_OTHER策略下,sched_priority必须设为0;SCHED_FIFO/SCHED_RR策略下,范围为1~99
 */
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);

/**
 * @brief 获取线程属性对象的调度参数
 * @param attr 已初始化的线程属性对象指针
 * @param param 用于存储调度参数的结构体指针,会填充当前的sched_priority值
 * @return 成功返回0,失败返回非零错误码
 */
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);

// 调度参数结构体定义
struct sched_param {
    int sched_priority; // 线程静态优先级
};

实操代码示例

创建 SCHED_FIFO 策略线程

#include <pthread.h>
#include <stdio.h>
void *task(void *arg) {
    // 线程任务逻辑
    while (1);
    return NULL;
}

int main(int argc, char const *argv[]) {
    pthread_attr_t attr;
    pthread_t thread;
    struct sched_param param;

    // 初始化线程属性对象
    pthread_attr_init(&attr);

    // 设置继承策略为不继承,使用attr中的调度属性
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

    // 设置调度策略为SCHED_FIFO
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);

    // 设置静态优先级为50(1~99范围)
    param.sched_priority = 50;
    pthread_attr_setschedparam(&attr, &param);

    // 创建线程
    pthread_create(&thread, &attr, task, NULL);

    // 主线程死循环,防止程序退出
    while (1);
    return 0;
}

同优先级 SCHED_FIFO 多线程示例

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
// 线程1:输出0~9
void *thread_num(void *arg) {
    while (1) {
        for (int i = 0; i < 10; i++) {
            printf("%d ", i);
        }
    }
    return NULL;
}

// 线程2:输出a~z
void *thread_char(void *arg) {
    while (1) {
        for (char c = 'a'; c <= 'z'; c++) {
            printf("%c ", c);
        }
    }
    return NULL;
}

int main() {
    pthread_attr_t attr;
    pthread_t t1, t2;
    struct sched_param param;

    // 初始化属性对象
    pthread_attr_init(&attr);
    // 设置不继承调度属性
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    // 设置SCHED_FIFO策略
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    // 设置相同优先级50
    param.sched_priority = 50;
    pthread_attr_setschedparam(&attr, &param);

    // 创建两个线程
    pthread_create(&t1, &attr, thread_num, NULL);
    pthread_create(&t2, &attr, thread_char, NULL);

    // 等待线程(此处用死循环替代)
    while (1);
    return 0;
}

Linux 线程信号响应

信号响应规则

进程收到信号时,默认由任意就绪线程响应,存在不确定性。实际开发中可指定特定线程处理信号,其他线程屏蔽该信号,确保信号处理逻辑可控。

信号发送函数

pthread_kill ():向指定线程发送信号

#include <signal.h>
#include <pthread.h>
/**
 * @brief 向同一进程中的指定线程发送信号
 * @param thread 目标线程的ID(pthread_t类型)
 * @param sig 要发送的信号编号(如SIGINT、SIGUSR1等,0表示仅做错误检查不发送信号)
 * @return 成功返回0,失败返回非零错误码(如EINVAL表示信号无效)
 * @note 信号是异步定向到目标线程,信号处理动作(如终止、暂停)影响整个进程
 */
int pthread_kill(pthread_t thread, int sig);

pthread_sigqueue ():向指定线程发送信号并附带数据

#include <signal.h>
#include <pthread.h>
/**
 * @brief 向同一进程中的指定线程发送信号,并附带数据
 * @param thread 目标线程的ID
 * @param sig 要发送的信号编号
 * @param value 附带的数据,union sigval类型(包含int sival_int或void *sival_ptr)
 * @return 成功返回0,失败返回非零错误码
 * @note 需定义_GNU_SOURCE宏才能使用,数据传递需确保线程间内存可访问
 */
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);

// 信号附带数据的联合体定义
union sigval {
    int sival_int;    // 整数类型数据
    void *sival_ptr;  // 指针类型数据
};

线程信号屏蔽函数

通过pthread_sigmask()函数屏蔽指定信号,避免线程响应不需要处理的信号,与sigprocmask()功能一致,但更适用于多线程环境(POSIX 标准指定)。

#include <signal.h>
/**
 * @brief 检查并修改线程的信号屏蔽集
 * @param how 信号屏蔽方式:SIG_BLOCK(添加信号到屏蔽集)、SIG_UNBLOCK(从屏蔽集移除信号)、SIG_SETMASK(设置屏蔽集为指定集合)
 * @param set 新的信号屏蔽集指针(NULL表示不修改屏蔽集,仅获取当前屏蔽集)
 * @param oldset 用于存储旧信号屏蔽集的指针(NULL表示不需要保存旧屏蔽集)
 * @return 成功返回0,失败返回非零错误码
 * @note 屏蔽信号仅对当前线程有效,其他线程不受影响
 */
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

实操代码示例(指定线程处理信号)

#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
// 信号处理函数:输出处理线程的ID
void sigint_handler(int sig) {
    printf("处理SIGINT信号的线程ID:%lu\n", pthread_self());
}

// 线程A:处理SIGINT信号
void *thread_a(void *arg) {
    // 设置SIGINT信号的处理函数
    signal(SIGINT, sigint_handler);
    // 线程A持续运行,等待信号
    while (1) {
        sleep(1);
    }
    return NULL;
}

// 线程B:屏蔽SIGINT信号
void *thread_b(void *arg) {
    sigset_t set;
    // 初始化信号集并添加SIGINT信号
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    // 屏蔽SIGINT信号
    pthread_sigmask(SIG_BLOCK, &set, NULL);
    // 线程B持续运行,不响应SIGINT
    while (1) {
        sleep(1);
    }
    return NULL;
}

// 线程C:屏蔽SIGINT信号(与线程B逻辑一致)
void *thread_c(void *arg) {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_BLOCK, &set, NULL);
    while (1) {
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t ta, tb, tc;
    // 创建三个线程
    pthread_create(&ta, NULL, thread_a, NULL);
    pthread_create(&tb, NULL, thread_b, NULL);
    pthread_create(&tc, NULL, thread_c, NULL);
    // 等待线程结束(此处用死循环替代)
    while (1) {
        sleep(1);
    }
    return 0;
}

说明

  • 信号处理函数需保持简洁,避免耗时操作,否则可能影响线程调度和其他信号处理。
  • 线程间信号屏蔽相互独立,一个线程屏蔽的信号不影响其他线程对该信号的响应。
  • 实时信号(SIGRTMIN~SIGRTMAX)支持排队,普通信号不支持,多次发送可能丢失。

扩展代码示例(线程与 CPU 核绑定)

通过sched_setaffinity()函数将线程绑定到指定 CPU 核,提高多核处理器利用率,减少线程切换开销。

// 必须在包含任何头文件前定义_GNU_SOURCE,启用sched_getcpu()等GNU扩展函数
#define _GNU_SOURCE
#include <sched.h>       // 包含CPU_ZERO/CPU_SET/sched_setaffinity/sched_getcpu的声明
#include <pthread.h>     // 包含pthread_self/pthread_create等线程函数
#include <stdio.h>       // 包含printf
#include <errno.h>       // 包含errno(错误码)
#include <string.h>      // 包含strerror(打印错误信息)
#include <unistd.h>      // 包含sleep(可选,用于测试)

// 线程函数:绑定CPU核并输出信息
void *task(void *arg) {
    cpu_set_t cpuset;     // 存储CPU亲和性集合的结构体
    int target_cpu = 0;   // 要绑定的CPU核(0表示第一个核,多核系统可改1/2等)

    // 初始化CPU集合(清空集合,避免垃圾值)
    CPU_ZERO(&cpuset);
    // 将目标CPU核加入集合(这里绑定到CPU 0)
    CPU_SET(target_cpu, &cpuset);

    // 设置当前线程的CPU亲和性(绑定到指定核)
    // 参数1:0表示“当前线程”(若传线程ID,需用gettid(),但pthread_t不直接对应tid)
    // 参数2:CPU集合的大小(字节数)
    // 参数3:CPU集合指针(包含要绑定的核)
    if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) {
        fprintf(stderr, "绑定CPU失败:%s(可能需要root权限)\n", strerror(errno));
        pthread_exit(NULL);  // 线程退出
    }

    // 验证绑定结果:获取当前线程实际运行的CPU核
    int current_cpu = sched_getcpu();
    if (current_cpu == -1) {
        fprintf(stderr, "获取当前CPU核失败:%s\n", strerror(errno));
        pthread_exit(NULL);
    }

    // 输出线程ID和绑定结果(pthread_self()返回线程的pthread_t标识)
    printf("线程(pthread ID:%lu)绑定结果:\n", pthread_self());
    printf("  目标CPU核:%d\n", target_cpu);
    printf("  实际运行CPU核:%d\n", current_cpu);
    printf("  绑定成功!\n");

    // 保持线程运行(避免线程立即退出,方便观察)
    while (1) {
        sleep(1);  // 每秒休眠一次,降低CPU占用
    }

    return NULL;
}

int main() {
    pthread_t thread;
    int ret;

    // 创建线程(注意:绑定CPU的操作在子线程内执行,更灵活)
    ret = pthread_create(&thread, NULL, task, NULL);
    if (ret != 0) {
        fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
        return 1;
    }

    // 等待子线程结束(避免主线程提前退出)
    pthread_join(thread, NULL);

    return 0;
}
posted @ 2025-11-19 21:27  YouEmbedded  阅读(0)  评论(0)    收藏  举报