ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

实例观察优先级翻转和优先级继承现象

实例观察优先级翻转和优先级继承现象 1、解决优先级翻转的问题有两个优先级继承和优先级天花板。2、优先级翻转是个问题要尽量避免问题实在无法避免再考虑解决问题。3、linux内核支持优先级继承而不支持优先级天花板。4、用户态pthread_mutex即支持优先级天继承又支持优先级天花板优先级天花板在用户层实现。优先级继承实时调度策略和实时调度策略之间生效实时调度策略和普通调度 策略生效。优先级天花板实时调度策略和实时调度策略之间生效实时调度策略和普通调度策略之间不生效之所以不生效是因为优先级天花板会设置一个天花板优先级普通调度策略加锁时设置优先级会失败导致加锁失败。5、c中的std::mutex不支持优先级继承和优先级天花板而c语言中的pthread_mutex默认支持优先级继承。原因是两者底层均通过futex实现区别在于是否有PI标志前者没用后者使用了。优先级继承和优先级天花板测试代码#include iostream #include pthread.h #include sched.h #include chrono #include cstring #include cstdlib #include cerrno #include string // ---------- 全局同步变量用于线程启动同步 ---------- pthread_mutex_t cond_mutex PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER; bool started false; // ---------- 测试用的互斥量每个测试独立初始化 ---------- pthread_mutex_t test_mutex; // ---------- 线程参数 ---------- struct ThreadArg { int id; // 0:低, 1:中, 2:高 pthread_mutex_t* mutex; // 指向 test_mutex }; // ---------- 工具忙等指定秒数不主动让出CPU ---------- void busy_wait_seconds(double seconds) { auto start std::chrono::steady_clock::now(); auto end start std::chrono::durationdouble(seconds); while (std::chrono::steady_clock::now() end) { // 忙等保持CPU占用 } } // ---------- 低优先级线程 ---------- void* low_thread_func(void* arg) { ThreadArg* ta (ThreadArg*)arg; pthread_mutex_t* mutex ta-mutex; // 1. 先锁定测试互斥量 pthread_mutex_lock(mutex); std::cout [Low] 获得锁\n; // 2. 通知其他线程开始 pthread_mutex_lock(cond_mutex); started true; pthread_cond_broadcast(cond); pthread_mutex_unlock(cond_mutex); // 3. 持有锁并忙等 5 秒模拟长时间操作 std::cout [Low] 开始忙等 5 秒...\n; busy_wait_seconds(500.0); std::cout [Low] 忙等结束\n; // 4. 释放锁 pthread_mutex_unlock(mutex); std::cout [Low] 释放锁\n; return nullptr; } // ---------- 中优先级线程仅CPU消耗不涉及锁 ---------- void* mid_thread_func(void* /*arg*/) { // 等待开始信号 pthread_mutex_lock(cond_mutex); while (!started) { pthread_cond_wait(cond, cond_mutex); } pthread_mutex_unlock(cond_mutex); std::cout [Mid] 开始 CPU 密集计算5秒...\n; busy_wait_seconds(500.0); std::cout [Mid] 计算结束\n; return nullptr; } // ---------- 高优先级线程 ---------- void* high_thread_func(void* arg) { ThreadArg* ta (ThreadArg*)arg; pthread_mutex_t* mutex ta-mutex; // 等待开始信号 pthread_mutex_lock(cond_mutex); while (!started) { pthread_cond_wait(cond, cond_mutex); } pthread_mutex_unlock(cond_mutex); // 尝试获取锁记录阻塞等待时间 std::cout [High] 尝试获取锁...\n; auto start_time std::chrono::steady_clock::now(); pthread_mutex_lock(mutex); auto end_time std::chrono::steady_clock::now(); auto wait_ms std::chrono::duration_caststd::chrono::milliseconds(end_time - start_time).count(); std::cout [High] 获得锁等待了 wait_ms ms\n; pthread_mutex_unlock(mutex); std::cout [High] 释放锁\n; return nullptr; } // ---------- 辅助创建并设置实时调度线程 ---------- int create_thread(pthread_t* tid, void* (*func)(void*), void* arg, int priority) { pthread_attr_t attr; pthread_attr_init(attr); // 使用 SCHED_FIFO实时调度 int ret pthread_attr_setschedpolicy(attr, SCHED_FIFO); if (ret ! 0) { std::cerr 设置 SCHED_FIFO 失败: strerror(ret) std::endl; pthread_attr_destroy(attr); return ret; } if (priority 60 ) { struct sched_param param; param.sched_priority priority; ret pthread_attr_setschedparam(attr, param); if (ret ! 0) { std::cerr 设置优先级 priority 失败: strerror(ret) std::endl; pthread_attr_destroy(attr); return ret; } // 显式使用线程自身的调度属性不继承父线程 ret pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED); if (ret ! 0) { std::cerr 设置继承策略失败: strerror(ret) std::endl; pthread_attr_destroy(attr); return ret; } } ret pthread_create(tid, attr, func, arg); pthread_attr_destroy(attr); return ret; } // ---------- 运行测试 ---------- void run_test(int protocol, int ceiling_prio 0) { // 重置全局同步标志 started false; // 初始化测试互斥量并设置协议 pthread_mutexattr_t mutex_attr; pthread_mutexattr_init(mutex_attr); pthread_mutexattr_setprotocol(mutex_attr, protocol); if (protocol PTHREAD_PRIO_PROTECT) { pthread_mutexattr_setprioceiling(mutex_attr, ceiling_prio); } pthread_mutex_init(test_mutex, mutex_attr); pthread_mutexattr_destroy(mutex_attr); // 准备线程参数 ThreadArg low_arg{0, test_mutex}; ThreadArg high_arg{2, test_mutex}; pthread_t low_tid, mid_tid, high_tid; // 创建三个线程优先级低40中60高80 if (create_thread(low_tid, low_thread_func, low_arg, 40) ! 0 || create_thread(mid_tid, mid_thread_func, nullptr, 60) ! 0 || create_thread(high_tid, high_thread_func, high_arg, 80) ! 0) { std::cerr 创建线程失败请确保以 root 运行。\n; return; } // 等待所有线程结束 pthread_join(low_tid, nullptr); pthread_join(mid_tid, nullptr); pthread_join(high_tid, nullptr); // 清理 pthread_mutex_destroy(test_mutex); } // ---------- 主函数 ---------- int main(int argc, char* argv[]) { if (argc ! 2) { std::cerr 用法: argv[0] [inherit|ceiling]\n; return 1; } std::string mode argv[1]; if (mode inherit) { std::cout 测试优先级继承 (PTHREAD_PRIO_INHERIT) \n; run_test(PTHREAD_PRIO_INHERIT); } else if (mode ceiling) { std::cout 测试优先级天花板 (PTHREAD_PRIO_PROTECT) \n; // 天花板优先级设为 80与高线程相同 run_test(PTHREAD_PRIO_PROTECT, 80); } else { std::cerr 未知模式: mode \n; return 1; } return 0; }1 背景当两个优先级不同的线程同时存在于调度队列的时候我们预期的调度顺序是优先级高的线程先运行优先级低的线程后运行。优先级翻转的意思是当两个优先级不同的线程同时存在时高优先级的线程得不到调度而是低优先级的线程获得了执行的机会与预期是反着的所以称为优先级翻转。当讨论优先级翻转和优先级继承的时候更多的是在讨论linux内核实时补丁的时候。我们知道在没有打实时内核补丁的内核中使用自旋锁时是关闭抢占的。因为自旋锁在等锁的过程中是自旋忙等待会一直占用着 cpu所以自旋锁适用于加锁时间非常短的场景。但是时间的长短没有严格的规定是μs级ms级还是s级有人认为几微秒是非常短有人认为几秒是非常短所以这就不能保证spin lock的加锁时间是什么量级同时自旋锁在内核中的使用非常多在linux各子系统(内存、文件系统、调度、网络)中驱动中都有使用。这些都给自旋锁的时间带来了不确定性又由于加锁的时候关闭了内核抢占即使这个时候有更高优先级的线程被唤醒也不能抢占当前的任务所以会导致调度的不确定性。而在打了实时补丁的内核中在自旋锁加锁过程中是支持内核抢占的这也引入了本文中的优先级翻转问题。如下是优先级翻转的说明。有 3 个线程优先级分别是 low, mid, high其中 low 线程和 high 线程会抢同一个锁。① t1 时刻low 线程开始运行调用 spin_lock获得了锁。② t2 时刻mid 线程被唤醒由于 mid 线程比 low 线程优先级高所以 mid 抢占了 low这个时候 mid 线程得到 cpu 开始执行。③ t3 时刻high 线程被唤醒high 线程被唤醒之后也要调用 spin_lock 进行加锁但是这个时候锁被 low 线程拿着所以 high 线程只能睡眠等待 low 线程释放锁。此时还是 mid 线程得到了 cpu 并在运行拿着锁的 low 线程得不到运行也无法释放锁所以最应该得到运行的 high 线程也得不到运行。④ t4 时刻mid 线程运行完毕这个时候 low 线程可以继续运行。⑤ t5 时刻low 线程运行完毕释放锁这个时候 high 线程得到了锁可以运行了。⑤ t6 时刻high 线程访问临界区完毕释放自旋锁。下图中箭头表示时间轴红色的区域表示线程在运行。从下图中能看出来high 线程被唤醒时本应该很快得到 cpu 并运行但是由于自旋锁的原因需要等 mid 和 low 运行完毕之后才能得到运行。这就是优先级翻转。如下是优先级继承的说明。在 t3 时刻high 线程被唤醒也要调用自旋锁加锁这个时候自旋锁被 low 线程拿着但是 low 线程得不到执行cpu 被 mid 线程占用着。linux 内核中所做的事情就是在 t3 时刻将 low 线程的优先级调整为 high这样 low 线程就能尽快执行完毕释放自旋锁从而使得 high 线程得到执行。这就是优先级继承。2 内核线程优先级翻转和优先级继承2.1 优先级翻转优先级翻转的现象在没有打实时内核补丁的系统上可以观察到。如下是一个内核模块在模块中创建了 3 个线程调度策略均为 SCHED_FIFO优先级分别是 5(low), 10(mid), 15(high)。low 线程中调用了 mutex_lock 加锁之后是一个死循环mid 线程中是一个死循环high 线程中使用 mutex_lock 加锁。先启动 low 线程再启动 mid 线程最后启动 high 线程。#include linux/delay.h #include linux/init.h #include linux/kernel.h #include linux/kthread.h #include linux/module.h #include linux/sched.h #include linux/spinlock.h struct sched_attr { __u32 size; __u32 sched_policy; __u64 sched_flags; /* SCHED_NORMAL, SCHED_BATCH */ __s32 sched_nice; /* SCHED_FIFO, SCHED_RR */ __u32 sched_priority; /* SCHED_DEADLINE */ __u64 sched_runtime; __u64 sched_deadline; __u64 sched_period; /* Utilization hints */ __u32 sched_util_min; __u32 sched_util_max; }; struct set_sched_attr_func { int (*sched_setattr_nocheck)(struct task_struct *, const struct sched_attr *); }; // 在有些系统中内核模块中不能使用内核函数 sched_setattr_nocheck, // 可以在 /proc/kallsyms 中找到函数对应的地址 // 使用如下方式来使用这个函数 // 但是这种方式只适用于测试环境中 // 生产环境不见这么使用 struct set_sched_attr_func sched_func { .sched_setattr_nocheck 0xffffd515464e9d20 }; struct mutex test_mutex; static struct task_struct *init_thread; static struct task_struct *thread_low; static struct task_struct *thread_mid; static struct task_struct *thread_high; static volatile int exit_flag 0; static int thread_low_entry(void *data) { struct task_struct *task current; printk(thread low start, tid: %d\n, task-pid); mutex_lock(test_mutex); while (1) { if (exit_flag) { break; } } mutex_unlock(test_mutex); printk(therad low return\n); return 0; } static int thread_mid_entry(void *data) { struct task_struct *task current; printk(thread mid start, tid: %d\n, task-pid); while (1) { if (exit_flag) { break; } } printk(thread mid return\n); return 0; } static int thread_high_entry(void *data) { struct task_struct *task current; printk(thread high start, tid: %d\n, task-pid); mutex_lock(test_mutex); while (1) { if (exit_flag) { break; } } printk(thread high return\n); return 0; } static int init_thread_entry(void *data) { printk(init thread start\n); mutex_init(test_mutex); thread_low kthread_create(thread_low_entry, NULL, thread_low); if (IS_ERR(thread_low)) { printk(failed to create thread low\n); return -1; } struct sched_attr attr_low; memset(attr_low, 0, sizeof(struct sched_attr)); attr_low.sched_policy SCHED_FIFO; attr_low.sched_priority 5; sched_func.sched_setattr_nocheck(thread_low, attr_low); kthread_bind(thread_low, 1); wake_up_process(thread_low); ssleep(2); thread_mid kthread_create(thread_mid_entry, NULL, thread_mid); if (IS_ERR(thread_mid)) { printk(failed to create thread mid\n); return -1; } struct sched_attr attr_mid; memset(attr_mid, 0, sizeof(struct sched_attr)); attr_mid.sched_policy SCHED_FIFO; attr_mid.sched_priority 10; sched_func.sched_setattr_nocheck(thread_mid, attr_mid); kthread_bind(thread_mid, 1); wake_up_process(thread_mid); ssleep(30); thread_high kthread_create(thread_high_entry, NULL, thread_high); if (IS_ERR(thread_high)) { printk(failed to create thread high\n); return -1; } struct sched_attr attr_high; memset(attr_high, 0, sizeof(struct sched_attr)); attr_high.sched_policy SCHED_FIFO; attr_high.sched_priority 15; sched_func.sched_setattr_nocheck(thread_high, attr_high); kthread_bind(thread_high, 1); wake_up_process(thread_high); return 0; } static int pi_init(void) { printk(pi init\n); init_thread kthread_create(init_thread_entry, NULL, init_thread); if (IS_ERR(init_thread)) { printk(failed to create init thread\n); return -1; } wake_up_process(init_thread); printk(pi inited\n); return 0; } static void pi_exit(void) { printk(pi exit\n); exit_flag 1; ssleep(10); printk(pi exited\n); } module_init(pi_init); module_exit(pi_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(wyl); MODULE_DESCRIPTION(watch pi in rt kernel); MODULE_VERSION(0.1);从下边的截图可以看出来mid 线程一直占着 cpucpu 使用率接近 100%high 线程和 low 线程均得不到执行cpu 使用率为 0%。2.2 优先级继承优先级继承现象在打了实时内核补丁的系统上可以观察到。如下是一个内核模块在模块中创建了 3 个线程调度策略均为 SCHED_FIFO优先级分别是 5(low), 10(mid), 15(high)。low 线程中调用了 spin_lock 加锁之后是一个死循环mid 线程中是一个死循环high 线程中使用 spin_lock 加锁。先启动 low 线程再启动 mid 线程最后启动 high 线程。#include linux/delay.h #include linux/init.h #include linux/kernel.h #include linux/kthread.h #include linux/module.h #include linux/sched.h #include linux/spinlock.h struct sched_attr { __u32 size; __u32 sched_policy; __u64 sched_flags; /* SCHED_NORMAL, SCHED_BATCH */ __s32 sched_nice; /* SCHED_FIFO, SCHED_RR */ __u32 sched_priority; /* SCHED_DEADLINE */ __u64 sched_runtime; __u64 sched_deadline; __u64 sched_period; /* Utilization hints */ __u32 sched_util_min; __u32 sched_util_max; }; struct set_sched_attr_func { int (*sched_setattr_nocheck)(struct task_struct *, const struct sched_attr *); }; struct set_sched_attr_func sched_func { .sched_setattr_nocheck 0xffffb03161789d20 }; static spinlock_t test_spinlock; static struct task_struct *init_thread; static struct task_struct *thread_low; static struct task_struct *thread_mid; static struct task_struct *thread_high; static volatile int exit_flag 0; static int thread_low_entry(void *data) { struct task_struct *task current; printk(thread low start, tid: %d\n, task-pid); spin_lock(test_spinlock); while (1) { if (exit_flag) { break; } } spin_unlock(test_spinlock); printk(therad low return\n); return 0; } static int thread_mid_entry(void *data) { struct task_struct *task current; printk(thread mid start, tid: %d\n, task-pid); while (1) { if (exit_flag) { break; } } printk(thread mid return\n); return 0; } static int thread_high_entry(void *data) { struct task_struct *task current; printk(thread high start, tid: %d\n, task-pid); spin_lock(test_spinlock); while (1) { if (exit_flag) { break; } } printk(thread high return\n); return 0; } static int init_thread_entry(void *data) { printk(init thread start\n); spin_lock_init(test_spinlock); thread_low kthread_create(thread_low_entry, NULL, thread_low); if (IS_ERR(thread_low)) { printk(failed to create thread low\n); return -1; } struct sched_attr attr_low; memset(attr_low, 0, sizeof(struct sched_attr)); attr_low.sched_policy SCHED_FIFO; attr_low.sched_priority 5; sched_func.sched_setattr_nocheck(thread_low, attr_low); kthread_bind(thread_low, 1); wake_up_process(thread_low); ssleep(2); thread_mid kthread_create(thread_mid_entry, NULL, thread_mid); if (IS_ERR(thread_mid)) { printk(failed to create thread mid\n); return -1; } struct sched_attr attr_mid; memset(attr_mid, 0, sizeof(struct sched_attr)); attr_mid.sched_policy SCHED_FIFO; attr_mid.sched_priority 10; sched_func.sched_setattr_nocheck(thread_mid, attr_mid); kthread_bind(thread_mid, 1); wake_up_process(thread_mid); ssleep(30); thread_high kthread_create(thread_high_entry, NULL, thread_high); if (IS_ERR(thread_high)) { printk(failed to create thread high\n); return -1; } struct sched_attr attr_high; memset(attr_high, 0, sizeof(struct sched_attr)); attr_high.sched_policy SCHED_FIFO; attr_high.sched_priority 15; sched_func.sched_setattr_nocheck(thread_high, attr_high); kthread_bind(thread_high, 1); wake_up_process(thread_high); return 0; } static int pi_init(void) { printk(pi init\n); init_thread kthread_create(init_thread_entry, NULL, init_thread); if (IS_ERR(init_thread)) { printk(failed to create init thread\n); return -1; } wake_up_process(init_thread); printk(pi inited\n); return 0; } static void pi_exit(void) { printk(pi exit\n); exit_flag 1; ssleep(10); printk(pi exited\n); } module_init(pi_init); module_exit(pi_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(wyl); MODULE_DESCRIPTION(watch pi in rt kernel); MODULE_VERSION(0.1);在 high 线程启动之前 low 线程的优先级显示为 -6cpu 使用率为 0%。在 high 线程启动后low 线程的优先级被修改为与 high 线程保持一致由 -6 改为了 -16。从下图可以看出这个时候 low 线程得到了执行cpu 使用率接近于 100%。high 线程的优先级显示为 -16。3 用户态线程优先级翻转和优先级继承在用户态使用 pthread_mutex_t mutex 时可以设置属性 PTHREAD_PRIO_INHERIT 来设置使用这个 mutex 的线程是支持优先级继承的。观察现象与内核态线程类似。本人测试中 PTHREAD_PRIO_INHERIT 的使用不需要打实时内核补丁普通的系统中也生效。#ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include linux/types.h #include sched.h #include stdio.h #include string.h #include sys/syscall.h #include sys/types.h #include unistd.h #include pthread.h #define BIND_CPU_CORE 2 pthread_mutex_t mutex; pthread_mutexattr_t mutex_attr; int set_fifo(int prio) { struct sched_param sp {.sched_priority prio}; int policy SCHED_FIFO; return sched_setscheduler(0, policy, sp); } int32_t set_affinity() { cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(BIND_CPU_CORE, cpuset); if (sched_setaffinity(0, sizeof(cpuset), cpuset) ! 0) { printf(bind cpu error\n); return -1; } return 0; } void *fifo_low(void *data) { set_fifo(5); set_affinity(); printf(fifo low\n); sleep(1); printf(fifo low, before lock\n); pthread_mutex_lock(mutex); printf(fifo low, after lock\n); while (1) ; } void *fifo_mid(void *data) { set_fifo(10); set_affinity(); printf(fifo mid\n); sleep(1); while (1) ; } void *fifo_high(void *data) { set_fifo(15); set_affinity(); printf(fifo high\n); sleep(1); printf(fifo high, before lock\n); pthread_mutex_lock(mutex); printf(fifo high, after lock\n); while (1) ; } int main() { pthread_t fifo_tid1; pthread_t fifo_tid2; pthread_t fifo_tid3; pthread_mutexattr_init(mutex_attr); pthread_mutexattr_setprotocol(mutex_attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(mutex, mutex_attr); sleep(5); pthread_create(fifo_tid1, NULL, fifo_low, NULL); sleep(5); pthread_create(fifo_tid2, NULL, fifo_mid, NULL); sleep(30); pthread_create(fifo_tid3, NULL, fifo_high, NULL); sleep(1000); return 0; }4优先级继承和优先级天花板优先级继承pthread_mutexattr_init(mutex_attr);pthread_mutexattr_setprotocol(mutex_attr, PTHREAD_PRIO_INHERIT);pthread_mutex_init(mutex, mutex_attr);如下代码可以观察到优先级继承现象。low线程首先获取到锁之后mid执行之后high要获取锁此时low的优先级调整到high的优先级。#ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include linux/types.h #include sched.h #include stdio.h #include string.h #include sys/syscall.h #include sys/types.h #include unistd.h #include pthread.h #include mutex #define BIND_CPU_CORE 2 pthread_mutex_t mutex; pthread_mutexattr_t mutex_attr; int set_fifo(int prio) { struct sched_param sp {.sched_priority prio}; int policy SCHED_FIFO; return sched_setscheduler(0, policy, sp); } int32_t set_affinity() { cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(BIND_CPU_CORE, cpuset); if (sched_setaffinity(0, sizeof(cpuset), cpuset) ! 0) { printf(bind cpu error\n); return -1; } return 0; } void *fifo_low(void *data) { pthread_setname_np(pthread_self(), low); set_fifo(5); set_affinity(); printf(fifo low\n); sleep(1); printf(fifo low, before lock\n); pthread_mutex_lock(mutex); int count 0; for (int i 0; i 60; i) { count; printf(low count %d\n, count); sleep(1); } printf(fifo low, after lock\n); while (1) ; } void *fifo_mid(void *data) { pthread_setname_np(pthread_self(), mid); set_fifo(10); set_affinity(); printf(fifo mid\n); sleep(1); while (1) ; } void *fifo_high(void *data) { pthread_setname_np(pthread_self(), high); set_fifo(15); set_affinity(); printf(fifo high\n); sleep(1); printf(fifo high, before lock\n); pthread_mutex_lock(mutex); printf(fifo high, after lock\n); while (1) ; } int main() { pthread_t fifo_tid1; pthread_t fifo_tid2; pthread_t fifo_tid3; pthread_mutexattr_init(mutex_attr); pthread_mutexattr_setprotocol(mutex_attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(mutex, mutex_attr); sleep(5); pthread_create(fifo_tid1, NULL, fifo_low, NULL); sleep(5); pthread_create(fifo_tid2, NULL, fifo_mid, NULL); sleep(30); pthread_create(fifo_tid3, NULL, fifo_high, NULL); sleep(1000); return 0; }优先级继承适用于实时线程之间以及实时线程和普通线程之间不适用于普通线程之间。线程设置为了普通调度策略那么说明对实时性没有要求那么也没有必要进行优先级继承。优先级天花板假如用户知道使用锁的线程的最高优先级是30那么可以设置优先级天花板是30。那么不管哪个线程获取到锁或者在等待锁那么线程的优先级都是调整为30。pthread_mutex_t 默认支持优先级继承std::mutex不支持优先级继承在linux下pthread_mutex_t如果不通过pthread_mutexattr_setprotocol设置协议那么默认为优先级继承协议。c中的std::mutex不支持优先级继承所以在使用c开发应用时如果对实时性有要求那么可以使用pthred_mutex_t。
返回列表