[ English | 简体中文 ]

调度管理 API

openvela 提供符合 POSIX 标准的任务调度接口,支持多种调度策略和任务管理功能。

头文件:#include <sched.h>

openvela 实现说明

  • 调度策略:支持 SCHED_FIFO(先进先出)、SCHED_RR(轮转,需 CONFIG_RR_INTERVAL > 0)、SCHED_SPORADIC(零星,需 CONFIG_SCHED_SPORADIC)和 SCHED_OTHER(映射到 SCHED_FIFO)。
  • 优先级范围:通常 1~255,数值越大优先级越高。优先级 0 保留给 idle 任务。
  • 返回值风格:task_* 系列返回负的错误码(如 -EINVAL),sched_* 系列遵循 POSIX 标准返回 -1 并设置 errno
  • SMP 支持:CPU 亲和性接口需要启用 CONFIG_SMP
  • task vs pthreadtask_create() 创建 openvela 原生任务,pthread_create() 创建 POSIX 线程。两者底层共享调度器,但 task 不支持 pthread 特有功能(如 TSD、cleanup handler)。

任务管理

task_create

int task_create(const char *name, int priority, int stack_size,
                main_t entry, char * const argv[]);

创建一个新任务并使其就绪。新任务从 entry 函数开始执行,可以接收参数数组 argv。任务创建后立即处于就绪状态,根据优先级和调度策略决定何时运行。

pthread_create() 不同,task_create() 是 openvela 特有的轻量级任务创建接口,创建的任务不是 POSIX 线程,而是 openvela 原生任务。任务栈由系统自动分配和管理。

参数

  • name 任务名称(字符串),用于调试和识别。最大长度由 CONFIG_TASK_NAME_SIZE 配置决定。名称可以为 NULL,但建议提供有意义的名称以便调试。
  • priority 任务优先级(整数)。有效范围取决于调度策略:
    • 实时优先级:通常 1-255(可通过 sched_get_priority_min/max() 查询)
    • 数值越大,优先级越高
    • 优先级 0 通常保留给 idle 任务
    • 建议使用 SCHED_PRIORITY_DEFAULT 或根据系统需求设置
  • stack_size 任务栈大小(字节)。必须足够容纳局部变量、函数调用和中断处理。推荐至少 2048 字节,复杂任务可能需要更大栈。栈大小会自动对齐到系统要求的边界。
  • entry 任务入口函数,类型为 main_t,签名为 int (*)(int argc, char *argv[])。函数返回时任务终止,返回值作为任务退出状态。
  • argv 传递给任务的参数数组(字符串指针数组),必须以 NULL 结尾。类似于 main() 函数的 argv。参数字符串会被复制,原字符串可以在调用后释放。如果不需要参数,可以传递 NULL 或空数组 {NULL}

返回值

  • 成功:返回新任务的 PID(进程 ID,正整数)。可用于后续的任务控制操作(如 task_delete()sched_setparam() 等)。
  • 失败:返回负的错误码:
    • -EINVAL:参数无效(如优先级超出范围、栈大小为 0)
    • -ENOMEM:内存不足,无法分配任务控制块或栈
    • -EAGAIN:系统资源不足,达到任务数量限制

注意

  • 任务 vs 线程task_create() 创建的是 openvela 原生任务,不是 POSIX 线程。任务比线程更轻量,但不支持某些 pthread 特性(如线程局部存储、线程清理函数等)。对于 POSIX 兼容性,应使用 pthread_create()
  • 栈分配:栈由系统自动分配(通常从堆中),任务终止时自动释放。如果需要使用预分配的栈,使用 task_create_with_stack()
  • 参数传递argv 数组及其字符串会被复制到任务的上下文中,因此调用者可以在函数返回后释放或修改原参数。但要注意,参数是浅拷贝(指针本身复制,指针指向的数据不复制)。
  • 任务调度:任务创建后立即处于就绪状态。如果新任务优先级高于当前任务,会立即抢占当前任务(抢占式调度)。
  • 任务终止:入口函数返回后任务自动终止。也可以调用 exit()task_delete() 或接收信号(如 SIGKILL)终止。
  • 资源管理:任务终止后,系统会自动清理其资源(栈、任务控制块等),但不会清理任务分配的其他资源(如打开的文件、分配的内存等),需要任务自己负责清理。
  • 初始调度策略:新任务的调度策略默认为 SCHED_FIFO(或系统默认策略),可以在创建后使用 sched_setscheduler() 修改。
  • 典型用法
    char *argv[] = {"arg1", "arg2", NULL};
    int pid = task_create("my_task", 100, 4096, task_main, argv);
    if (pid < 0) {
        printf("Failed to create task: %d\n", pid);
    }
    
  • 与 fork() 的区别:不同于 fork()task_create() 不复制父任务的地址空间,新任务从指定入口函数开始执行,不共享父任务的代码段以外的资源。

POSIX 兼容性:openvela 扩展接口(非 POSIX 标准)。

task_create_with_stack

int task_create_with_stack(const char *name, int priority,
                           void *stack, int stack_size,
                           main_t entry, char * const argv[]);

使用预分配的栈创建一个新任务。与 task_create() 类似,但允许调用者提供栈内存,而不是由系统自动分配。这在需要精确控制内存布局、使用特殊内存区域(如共享内存、DMA 可访问内存)或优化启动性能时非常有用。

预分配栈给予程序员更多控制权,但也带来了更多责任(如栈大小验证、内存对齐、生命周期管理)。

参数

  • name 任务名称,用于调试和标识。字符串会被复制,因此可以是临时缓冲区。最大长度通常由 CONFIG_TASK_NAME_SIZE 定义(如 31 字符 + NULL)。如果为 NULL,任务将有一个自动生成的名称。
  • priority 任务优先级,数值越大优先级越高。有效范围通常为 1 到 255,可以通过 sched_get_priority_min() / sched_get_priority_max() 查询。优先级决定任务的调度顺序。
  • stack 指向预分配栈内存的指针。必须:
    • 非 NULL:不能为 NULL,否则返回错误
    • 足够大小:至少为 stack_size 字节
    • 正确对齐:通常需要对齐到架构要求的边界(如 8 字节或 16 字节)
    • 可写:栈内存必须可读写
    • 生命周期管理:调用者负责在任务终止后释放栈内存(如果动态分配)
  • stack_size 栈大小(字节)。必须满足:
    • 最小要求:至少为 PTHREAD_STACK_MIN(通常几百字节)
    • 任务需求:足够容纳任务的局部变量、函数调用深度、中断/异常处理
    • 对齐:某些架构可能要求大小也对齐(如 8 字节的倍数)
  • entry 任务入口函数,签名为 int main(int argc, char *argv[])。不能为 NULL,否则返回错误。
  • argv 传递给任务的参数数组(类似 main()argv)。数组必须以 NULL 指针结尾。可以为 NULL,表示无参数(等同于空数组)。

返回值

  • 成功:返回新任务的 PID(正整数)
  • 失败:返回负的错误码:
    • -EINVAL:参数无效(如 stack 为 NULL、entry 为 NULL、priority 超出范围)
    • -ENOMEM:内存不足(虽然栈已提供,但任务控制块等仍需分配)
    • -EAGAIN:系统任务数已达上限(CONFIG_MAX_TASKS

注意

  • 与 task_create 的区别
    • task_create:系统自动分配和释放栈
    • task_create_with_stack:调用者提供栈,并负责释放
  • 栈生命周期管理
    • 栈内存必须在任务的整个生命周期内保持有效
    • 任务终止后,调用者负责释放栈内存(如果是动态分配的)
    • 如果栈是静态数组或全局变量,无需显式释放
  • 典型用法(动态分配栈)
    void *stack = malloc(8192);
    if (stack == NULL) {
        perror("malloc");
        return -1;
    }
    
    int pid = task_create_with_stack("worker", 100, stack, 8192,
                                     worker_func, NULL);
    if (pid < 0) {
        perror("task_create_with_stack");
        free(stack);
        return -1;
    }
    
    // ... 等待任务结束 ...
    waitpid(pid, NULL, 0);
    free(stack);  // 释放栈
    
  • 静态栈示例
    static uint8_t worker_stack[4096] __attribute__((aligned(16)));
    
    int pid = task_create_with_stack("worker", 100, worker_stack,
                                     sizeof(worker_stack), worker_func, NULL);
    
  • 栈方向:某些架构栈向下增长,某些向上增长。openvela 会自动处理栈方向,调用者只需提供起始地址和大小。
  • 栈对齐:确保栈地址正确对齐(通常 8 字节或 16 字节),否则可能导致未定义行为或性能下降:
    void *stack = aligned_alloc(16, 8192);  // 16 字节对齐
    
  • 栈溢出保护:预分配栈不自动提供溢出保护(guard page)。如果需要,应在栈顶/底额外分配保护页,并设置为不可访问:
    void *stack_with_guard = malloc(8192 + 4096);  // 多分配一页保护
    mprotect(stack_with_guard, 4096, PROT_NONE);   // 保护页不可访问
    void *usable_stack = (char*)stack_with_guard + 4096;
    task_create_with_stack("worker", 100, usable_stack, 8192, worker_func, NULL);
    
  • 共享内存栈:可以使用共享内存作为栈,实现跨进程的栈共享(高级用法,需要仔细同步):
    int shm_fd = shm_open("/worker_stack", O_CREAT | O_RDWR, 0666);
    ftruncate(shm_fd, 8192);
    void *stack = mmap(NULL, 8192, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    task_create_with_stack("worker", 100, stack, 8192, worker_func, NULL);
    
  • 性能考虑:使用预分配栈可以减少任务创建时的内存分配开销,提高启动性能。在需要频繁创建/销毁任务的场景中(如任务池),可以维护一个栈缓存池。
  • 调试建议:在调试阶段,可以在栈边界填充魔数(如 0xDEADBEEF),然后定期检查,及早发现栈溢出:
    uint32_t *stack_end = (uint32_t*)((char*)stack + stack_size - sizeof(uint32_t));
    *stack_end = 0xDEADBEEF;
    // ... 任务运行 ...
    if (*stack_end != 0xDEADBEEF) {
        printf("Stack overflow detected!\n");
    }
    
  • 实时系统优化:在实时系统中,使用预分配栈可以避免动态分配的不确定性,提高任务创建的确定性和速度。
  • 陷阱
    • 栈过小会导致栈溢出,难以调试(通常表现为随机崩溃或数据损坏)
    • 忘记释放动态分配的栈会导致内存泄漏
    • 栈未对齐可能导致未定义行为(某些架构会崩溃,某些只是性能下降)
    • 在任务仍在运行时释放栈会导致严重错误

POSIX 兼容性:openvela 扩展接口(非 POSIX 标准,类似于某些 RTOS 的接口)。

task_delete

int task_delete(pid_t pid);

删除(终止)指定的任务,释放其占用的系统资源。与 pthread_cancel()kill(pid, SIGKILL) 类似,但这是 openvela 的原生接口,更直接和高效。

任务删除是强制性的,不经过正常的清理流程(如 atexit 处理程序),应谨慎使用。通常应优先使用协作式终止机制(如设置退出标志,让任务自行退出)。

参数

  • pid 要删除的任务 PID。特殊值 0 表示删除调用任务自身(等同于 exit())。必须是有效的任务 PID。

返回值

  • 成功:返回 0(OK
  • 失败:返回负的错误码:
    • -EINVALpid 无效(如为负值)
    • -ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • -EPERM:调用者没有权限删除目标任务(取决于系统配置)

注意

  • 强制终止:任务被立即终止,不会执行清理代码(如 pthread_cleanup_push 注册的处理程序、atexit 回调等)。这可能导致资源泄漏(如未释放的内存、未关闭的文件、未解锁的互斥锁)。
  • 资源清理:内核会自动回收任务的核心资源(如栈内存、任务控制块),但应用层资源(如堆内存、打开的文件)可能不会自动清理。
  • 删除自身:如果 pid 为 0,任务会删除自己,永不返回(类似调用 exit(0)):
    task_delete(0);
    // 这行代码永远不会执行
    
  • 与 pthread_cancel 的区别
    • task_delete() 立即强制终止,无取消点概念
    • pthread_cancel() 在下一个取消点才生效,允许清理
    • task_delete() 是 openvela 扩展,pthread_cancel() 是 POSIX 标准
  • 典型用法
    int pid = task_create("worker", 100, 2048, worker_func, NULL);
    // ... 任务运行 ...
    
    // 终止任务
    if (task_delete(pid) == 0) {
        printf("Task %d deleted\n", pid);
    } else {
        perror("task_delete");
    }
    
  • 互斥锁陷阱:如果被删除的任务正持有互斥锁,其他等待该锁的任务将永远阻塞(死锁)。应确保任务在被删除前释放所有锁。
  • 子任务清理:删除父任务不会自动删除其子任务。如果需要清理整个任务树,必须显式删除所有子任务。
  • 替代方案
    • 协作式退出:设置退出标志,让任务自行检查并退出
    • pthread_cancel:使用 POSIX 取消机制,允许清理处理程序运行
    • 信号:发送 SIGTERM,让任务捕获并优雅退出
  • 实时系统注意:在实时系统中,强制删除任务可能影响系统的确定性和可预测性,应谨慎使用。
  • 调试建议:在调试阶段,可以在任务入口和退出点添加日志,帮助跟踪任务生命周期。
  • 批量删除:如果需要删除多个任务,应按依赖顺序删除(先删除子任务,后删除父任务),避免悬空引用。

POSIX 兼容性:openvela 扩展接口(非 POSIX 标准)。

task_restart

int task_restart(pid_t pid);

重启指定的任务,任务将使用原始的入口点、参数、优先级和栈大小重新开始执行。这相当于先终止任务,然后用相同参数重新创建。

任务重启是一种特殊的恢复机制,用于处理任务异常或需要重置任务状态的场景。与删除后重新创建相比,重启保留了原始任务的配置信息。

参数

  • pid 要重启的任务 PID。必须是有效的任务 PID(不能为 0,因为不能重启自己)。

返回值

  • 成功:返回 0(OK
  • 失败:返回负的错误码:
    • -EINVALpid 无效(如为 0 或负值)
    • -ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • -EPERM:调用者没有权限重启目标任务(取决于系统配置)
    • -ENOMEM:内存不足,无法重启任务

注意

  • 不能重启自己:不能用 task_restart(0)task_restart(getpid()) 重启自己,因为重启会销毁当前执行上下文。如果尝试这样做,通常会返回 -EINVAL
  • 任务状态重置:重启后,任务的所有状态(包括局部变量、堆栈内容、寄存器)都会重置,就像刚刚创建一样。任务会从入口函数开始执行。
  • 保留配置:重启后的任务保留原始配置:
    • 任务名称(name)
    • 优先级(priority)
    • 栈大小(stack_size)
    • 入口函数(entry)
    • 入口参数(argv)
  • PID 保持不变:重启后,任务的 PID 保持不变,其他任务引用此 PID 仍然有效。
  • 典型用法
    int pid = task_create("monitor", 150, 2048, monitor_task, NULL);
    
    // ... 任务运行一段时间后检测到异常 ...
    
    // 重启任务
    if (task_restart(pid) == 0) {
        printf("Task %d restarted\n", pid);
    } else {
        perror("task_restart");
    }
    
  • 与 task_delete + task_create 的区别
    • task_restart:PID 不变,保留原始配置,一步完成
    • task_delete + task_create:PID 改变,需要重新指定所有参数,两步操作
  • 资源清理:重启前,任务持有的资源(如打开的文件、分配的内存、持有的锁)可能不会自动释放,这可能导致资源泄漏。应在任务设计时考虑异常恢复机制。
  • 互斥锁陷阱:如果任务正持有互斥锁,重启会导致锁永远无法释放(死锁)。应确保任务在重启前释放所有锁,或使用鲁棒互斥锁(robust mutex)。
  • 看门狗场景:常用于看门狗系统,当检测到任务无响应或异常时,自动重启任务恢复服务:
    void watchdog_task(void *arg) {
        while (1) {
            if (check_task_health(worker_pid) == FAILED) {
                printf("Worker unhealthy, restarting...\n");
                task_restart(worker_pid);
            }
            sleep(5);
        }
    }
    
  • 重启计数:在生产系统中,应限制重启次数,避免陷入无限重启循环:
    int restart_count = 0;
    const int MAX_RESTARTS = 3;
    
    if (task_restart(pid) == 0) {
        restart_count++;
        if (restart_count >= MAX_RESTARTS) {
            printf("Max restarts reached, giving up\n");
            task_delete(pid);
        }
    }
    
  • 异步操作:重启是异步的,函数返回时任务可能还在初始化中。如果需要等待任务完成初始化,应使用额外的同步机制(如信号量)。
  • 调试建议:在任务入口函数中添加日志,记录每次启动时间和原因,有助于分析重启历史。
  • 实时系统影响:频繁重启任务可能影响系统的实时性和可预测性,应通过改进任务健壮性来减少重启需求。

POSIX 兼容性:openvela 扩展接口(非 POSIX 标准)。

任务取消

task_setcancelstate

int task_setcancelstate(int state, int *oldstate);

设置调用任务的取消状态(cancel state),控制任务是否可以被取消。与 pthread_setcancelstate() 类似,但适用于所有任务(不仅限于 pthread)。

取消状态是任务取消机制的一部分,允许任务临时禁止取消,保护关键代码段不被中断。

参数

  • state 新的取消状态,有效值:
    • TASK_CANCEL_ENABLE (0):允许取消(默认),任务可以响应取消请求
    • TASK_CANCEL_DISABLE (1):禁止取消,任务忽略取消请求(请求被推迟)
  • oldstate 如果非 NULL,用于接收之前的取消状态。如果不关心旧值,可以传 NULL

返回值

  • 成功:返回 0(OK
  • 失败:返回负的错误码:
    • -EINVALstate 参数无效(不是 TASK_CANCEL_ENABLETASK_CANCEL_DISABLE

注意

  • 保护关键区域:在执行不可中断的关键操作时(如更新共享数据结构、持有互斥锁),应禁用取消:
    task_setcancelstate(TASK_CANCEL_DISABLE, NULL);
    // 关键操作,不能被取消中断
    update_critical_data();
    task_setcancelstate(TASK_CANCEL_ENABLE, NULL);
    
  • 推迟取消:当取消状态为 TASK_CANCEL_DISABLE 时,取消请求不会丢失,而是被推迟。当取消状态重新设置为 TASK_CANCEL_ENABLE 时,如果有待处理的取消请求,任务会在下一个取消点被取消。
  • 取消类型交互:取消状态与取消类型(task_setcanceltype())共同决定取消行为:
    • 状态=ENABLE, 类型=DEFERRED:在取消点才取消(默认,最安全)
    • 状态=ENABLE, 类型=ASYNCHRONOUS:任何时候都可以取消(危险)
    • 状态=DISABLE:无论类型如何,都不会取消
  • 典型用法(保存旧状态)
    int oldstate;
    task_setcancelstate(TASK_CANCEL_DISABLE, &oldstate);
    // 关键操作
    critical_section();
    task_setcancelstate(oldstate, NULL);  // 恢复原状态
    
  • 与互斥锁配合
    pthread_mutex_lock(&mutex);
    task_setcancelstate(TASK_CANCEL_DISABLE, NULL);
    
    // 受保护的操作
    modify_shared_data();
    
    task_setcancelstate(TASK_CANCEL_ENABLE, NULL);
    pthread_mutex_unlock(&mutex);
    
  • 默认状态:新创建的任务默认取消状态为 TASK_CANCEL_ENABLE,允许被取消。
  • 不影响信号:取消状态只影响通过 task_delete() 或类似机制的取消,不影响信号(如 SIGTERM)的处理。
  • 与 pthread 的兼容性:在 pthread 线程中,task_setcancelstate()pthread_setcancelstate() 通常是等价的,操作同一底层状态。
  • 嵌套禁用:可以多次调用 TASK_CANCEL_DISABLE,但每次都应对应一次 TASK_CANCEL_ENABLE(或恢复旧状态),否则可能导致永久禁用取消:
    int old1, old2;
    task_setcancelstate(TASK_CANCEL_DISABLE, &old1);  // 第一次禁用
    task_setcancelstate(TASK_CANCEL_DISABLE, &old2);  // 第二次禁用(无效果)
    task_setcancelstate(old2, NULL);  // 恢复到old2(仍然禁用)
    task_setcancelstate(old1, NULL);  // 恢复到old1(可能启用)
    
    更简单的做法是只在最外层操作取消状态。
  • 清理处理程序:即使禁用了取消,清理处理程序(pthread_cleanup_push)仍然会在任务正常退出时执行。
  • 性能考虑:设置取消状态是轻量操作,但应避免在紧密循环中频繁切换,以免影响性能。

POSIX 兼容性:类似 pthread_setcancelstate(),但适用于所有任务类型。

task_setcanceltype

int task_setcanceltype(int type, int *oldtype);

设置调用任务的取消类型(cancel type),控制取消请求何时生效。与 pthread_setcanceltype() 类似,但适用于所有任务(不仅限于 pthread)。

取消类型决定了任务响应取消请求的时机,影响任务取消的安全性和响应性。

参数

  • type 新的取消类型,有效值:
    • TASK_CANCEL_DEFERRED (0):延迟取消(默认),仅在取消点(cancellation point)才响应取消请求,如 pthread_testcancel()sleep()read() 等阻塞调用
    • TASK_CANCEL_ASYNCHRONOUS (1):异步取消,任务可以在任意时刻被取消(危险,可能导致资源泄漏或数据不一致)
  • oldtype 如果非 NULL,用于接收之前的取消类型。如果不关心旧值,可以传 NULL

返回值

  • 成功:返回 0(OK
  • 失败:返回负的错误码:
    • -EINVALtype 参数无效(不是 TASK_CANCEL_DEFERREDTASK_CANCEL_ASYNCHRONOUS

注意

  • 默认类型(DEFERRED)最安全:延迟取消是默认且推荐的类型,它只在明确定义的取消点才生效,允许任务在取消前完成当前操作并清理资源。
  • 异步取消的危险性TASK_CANCEL_ASYNCHRONOUS 极其危险,因为任务可能在任何时刻被取消:
    • 可能在持有互斥锁时被取消,导致死锁
    • 可能在更新数据结构的中途被取消,导致数据不一致
    • 可能在分配内存后、保存指针前被取消,导致内存泄漏
    • 只有非常特殊的代码(如纯计算任务,不访问共享资源)才应使用异步取消
  • 取消点:常见的取消点包括:
    • task_testcancel() / pthread_testcancel():显式取消点
    • 阻塞的系统调用:sleep()usleep()read()write()recv()send()
    • 同步原语:pthread_cond_wait()sem_wait()
    • 某些库函数:printf()(可能)
  • 典型用法(临时启用异步取消)
    int oldtype;
    task_setcanceltype(TASK_CANCEL_ASYNCHRONOUS, &oldtype);
    // 纯计算任务,不访问共享资源
    perform_long_computation();
    task_setcanceltype(oldtype, NULL);  // 恢复原类型
    
    但通常更好的做法是在循环中定期调用 task_testcancel()
  • 推荐做法(在循环中添加取消点)
    while (processing) {
        process_chunk();
        task_testcancel();  // 定期检查取消请求
    }
    
    这比使用异步取消更安全,且仍能及时响应取消。
  • 与取消状态交互:取消类型与取消状态共同决定取消行为:
    • 状态=ENABLE, 类型=DEFERRED:在取消点才取消(默认,最安全)
    • 状态=ENABLE, 类型=ASYNCHRONOUS:任何时候都可以取消(危险)
    • 状态=DISABLE:无论类型如何,都不会取消
  • 清理处理程序:无论取消类型如何,任务被取消时都会执行清理处理程序(pthread_cleanup_push 注册)。但异步取消可能在不一致的状态下触发清理,导致问题。
  • 实时系统考虑:在实时系统中,异步取消可能影响系统的可预测性,应避免使用。优先使用延迟取消或协作式退出机制。
  • 与 pthread 的兼容性:在 pthread 线程中,task_setcanceltype()pthread_setcanceltype() 通常是等价的。
  • 默认值:新创建的任务默认取消类型为 TASK_CANCEL_DEFERRED(延迟取消)。
  • 避免混用:不要在同一任务中混用延迟和异步取消类型,这会使代码难以理解和维护。选择一种类型并坚持使用。
  • 异步安全代码:如果必须使用异步取消,确保任务代码是异步安全的(async-cancel-safe),类似于信号处理程序的要求:
    • 不调用非异步安全的函数(如 malloc()printf()
    • 不访问共享数据(或使用原子操作)
    • 不持有任何锁

POSIX 兼容性:类似 pthread_setcanceltype(),但适用于所有任务类型。

task_testcancel

void task_testcancel(void);

创建一个显式的取消点(cancellation point)。如果有待处理的取消请求,且任务的取消状态为允许(TASK_CANCEL_ENABLE),则任务将在此处被取消并终止。

这是延迟取消机制的核心,允许任务在安全的位置响应取消请求,确保资源正确释放和状态一致性。

参数

无参数。

返回值

  • 无返回值(如果任务被取消,函数永不返回)
  • 如果没有待处理的取消请求,或取消状态为禁止,函数正常返回

注意

  • 显式取消点:此函数是程序员主动创建的取消点,与隐式取消点(如 sleep()read())不同。显式取消点提供了更精确的控制,允许任务在安全的位置检查取消请求。
  • 典型用法(长时间循环)
    while (processing) {
        process_data_chunk();
        task_testcancel();  // 定期检查取消请求
    }
    
    这确保任务能及时响应取消,同时保证每次循环迭代完整完成。
  • 取消条件:任务仅在以下所有条件都满足时被取消:
    1. 有待处理的取消请求(通过 task_delete() 或类似机制触发)
    2. 取消状态为 TASK_CANCEL_ENABLE(默认)
    3. 取消类型为 TASK_CANCEL_DEFERRED(默认),或者无论类型(如果状态为 ENABLE)
  • 清理处理程序:如果任务被取消,会执行清理处理程序(pthread_cleanup_push 注册),然后终止。确保在关键资源(如互斥锁)使用时注册清理处理程序:
    pthread_mutex_lock(&mutex);
    pthread_cleanup_push((void(*)(void*))pthread_mutex_unlock, &mutex);
    
    // 可能被取消的操作
    while (condition) {
        process_data();
        task_testcancel();
    }
    
    pthread_cleanup_pop(1);  // 正常退出也解锁
    
  • 不影响异步取消:如果取消类型为 TASK_CANCEL_ASYNCHRONOUS,任务可能在任何时刻被取消,不仅限于取消点。
  • 与 pthread_testcancel 的兼容性task_testcancel()pthread_testcancel() 通常是等价的,可以互换使用。
  • 性能考虑:检查取消请求是轻量操作,但应避免在极紧密的循环中调用(如每次循环耗时微秒级),以免影响性能。可以每处理 N 个项目后调用一次:
    for (int i = 0; i < items; i++) {
        process_item(i);
        if (i % 100 == 0) task_testcancel();  // 每100次检查一次
    }
    
  • 取消安全的位置:应在以下位置调用 task_testcancel()
    • 不持有任何互斥锁
    • 所有数据结构处于一致状态
    • 没有未释放的临时资源
  • 实时系统影响:在实时系统中,取消点会增加任务的响应时间不确定性(虽然很小)。如果需要极高的确定性,可以在任务设计时避免取消机制,改用协作式退出。
  • 调试建议:在调试取消相关问题时,可以在 task_testcancel() 前后添加日志,跟踪取消点的执行:
    printf("Before testcancel\n");
    task_testcancel();
    printf("After testcancel (not cancelled)\n");
    
  • 与退出标志的比较
    • task_testcancel():内核机制,更轻量,与清理处理程序集成
    • 退出标志:用户态机制,更灵活,但需要手动管理清理
    // 退出标志方式
    volatile bool should_exit = false;
    while (!should_exit) {
        process_data();
    }
    
  • 无操作返回:如果没有取消请求,函数立即返回,几乎无开销(只是检查一个标志位)。

POSIX 兼容性:类似 pthread_testcancel(),但适用于所有任务类型。

调度策略与参数

sched_setscheduler

int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);

设置指定任务的调度策略和调度参数(如优先级)。这是控制任务调度行为的主要接口,允许在运行时动态调整任务的调度特性。

调度策略决定了任务如何竞争 CPU 时间,不同策略适用于不同类型的任务(实时、批处理、交互等)。修改调度策略通常需要适当的权限。

参数

  • pid 目标任务的 PID。特殊值 0 表示调用任务自身。必须是有效的任务 PID。
  • policy 新的调度策略,有效值包括:
    • SCHED_FIFO (0):先进先出实时调度,无时间片,适合硬实时任务
    • SCHED_RR (1):轮转实时调度,有时间片,适合需要公平性的实时任务
    • SCHED_SPORADIC (2):零星调度,适合周期性实时任务(需要 CONFIG_SCHED_SPORADIC
    • SCHED_OTHER (3):标准分时调度,映射到 SCHED_FIFO 或 SCHED_RR
    • SCHED_NORMAL (3):SCHED_OTHER 的别名
    • SCHED_BATCH (4):批处理调度(如果支持)
    • SCHED_IDLE (5):空闲调度,最低优先级(如果支持)
  • param 指向 struct sched_param 结构的指针,包含调度参数。至少需要设置 sched_priority 字段(基本优先级)。对于 SCHED_SPORADIC,还需要设置零星服务器参数(低优先级、补充周期、初始预算、最大补充次数)。

返回值

  • 成功:返回任务之前的调度策略(SCHED_FIFOSCHED_RR 等)
  • 失败:返回 -1 并设置 errno
    • EINVALpolicy 无效,或 param 中的优先级超出该策略允许的范围
    • ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • EPERM:调用者没有权限修改目标任务的调度策略(通常需要超级用户权限或同一用户)
    • EFAULTparam 指向无效内存

注意

  • 优先级范围:不同调度策略有不同的有效优先级范围,可以通过 sched_get_priority_min(policy)sched_get_priority_max(policy) 查询。设置超出范围的优先级会导致 EINVAL 错误。
  • 策略切换影响
    • 切换到更高优先级的策略可能导致任务立即抢占当前任务
    • 切换到较低优先级可能导致任务被其他任务抢占
    • 策略切换不会改变任务的就绪状态,已阻塞的任务仍然阻塞
  • SCHED_SPORADIC 参数:使用此策略时,必须在 param 中设置:
    • sched_ss_low_priority:预算耗尽后的低优先级
    • sched_ss_repl_period:补充周期
    • sched_ss_init_budget:初始预算
    • sched_ss_max_repl:最大待处理补充次数(<= SS_REPL_MAX
  • 实时调度策略:SCHED_FIFO 和 SCHED_RR 是实时策略,通常需要提升权限。实时任务可能影响系统响应性,应谨慎使用。
  • 调度策略继承:子任务(通过 fork()task_create() 创建)通常继承父任务的调度策略和优先级,除非在创建时指定或稍后修改。
  • 典型用法
    struct sched_param param;
    param.sched_priority = 150;  // 设置高优先级
    
    int old_policy = sched_setscheduler(0, SCHED_FIFO, &param);
    if (old_policy < 0) {
        perror("sched_setscheduler");
    } else {
        printf("Changed from policy %d to SCHED_FIFO\n", old_policy);
    }
    
  • 查询当前策略:使用 sched_getscheduler(pid) 查询任务当前的调度策略。
  • 仅修改优先级:如果只想修改优先级而不改变策略,使用 sched_setparam(),它更高效且语义更清晰。
  • 原子性:策略和参数的修改是原子的,不会出现中间状态。
  • 对运行任务的影响:如果修改当前正在运行的任务(pid=0),调度器会立即重新评估任务优先级,可能导致任务被抢占。

POSIX 兼容性:兼容 POSIX 同名接口。

sched_getscheduler

int sched_getscheduler(pid_t pid);

查询指定任务的当前调度策略。这是一个轻量级查询接口,用于获取任务的调度策略类型(如 SCHED_FIFO、SCHED_RR 等)。

调度策略决定了任务的调度行为,了解当前策略有助于调试和监控任务的实时特性。

参数

  • pid 目标任务的 PID。特殊值 0 表示查询调用任务自身的调度策略。必须是有效的任务 PID。

返回值

  • 成功:返回任务当前的调度策略(非负整数):
    • SCHED_FIFO (0):先进先出实时调度
    • SCHED_RR (1):轮转实时调度
    • SCHED_SPORADIC (2):零星调度(如果支持)
    • SCHED_OTHER (3):标准分时调度
    • SCHED_BATCH (4):批处理调度(如果支持)
    • SCHED_IDLE (5):空闲调度(如果支持)
  • 失败:返回 -1 并设置 errno
    • ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • EINVAL:参数 pid 为负值

注意

  • 只读查询:此函数不修改任务状态,是纯查询操作,开销很小。
  • 配合使用:通常与 sched_getparam() 配合使用,以获取完整的调度信息(策略 + 参数)。
  • 典型用法
    int policy = sched_getscheduler(0);  // 查询自己的策略
    if (policy >= 0) {
        const char *policy_names[] = {"SCHED_FIFO", "SCHED_RR", "SCHED_SPORADIC", "SCHED_OTHER"};
        printf("Current policy: %s\n", policy_names[policy]);
    } else {
        perror("sched_getscheduler");
    }
    
  • 任务诊断:在调试实时系统时,可以用此函数验证任务是否运行在预期的调度策略下。
  • 监控工具:系统监控工具常用此函数显示任务的调度策略,帮助分析系统调度行为。
  • 修改策略:如果需要修改调度策略,使用 sched_setscheduler()
  • 策略名称映射:可以使用 switch 或数组将返回的整数值映射为策略名称,提高可读性。

POSIX 兼容性:兼容 POSIX 同名接口。

sched_setparam

int sched_setparam(pid_t pid, const struct sched_param *param);

修改指定任务的调度参数(主要是优先级),但不改变调度策略。这是调整任务优先级的标准接口,比 sched_setscheduler() 更轻量,语义更清晰。

优先级是调度系统中最重要的参数,决定了任务在同一策略下的执行顺序。动态调整优先级是实时系统中常见的需求,例如实现优先级继承或优先级天花板协议。

参数

  • pid 目标任务的 PID。特殊值 0 表示修改调用任务自身的参数。必须是有效的任务 PID。
  • param 指向 struct sched_param 结构的指针,包含新的调度参数。主要字段:
    • sched_priority:新的优先级值(必需),必须在当前调度策略的有效范围内
    • 对于 SCHED_SPORADIC 策略,还包括 sched_ss_low_prioritysched_ss_repl_periodsched_ss_init_budgetsched_ss_max_repl 等零星服务器参数

返回值

  • 成功:返回 0
  • 失败:返回 -1 并设置 errno
    • EINVALparam 中的优先级超出当前策略允许的范围,或 SCHED_SPORADIC 参数无效
    • ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • EPERM:调用者没有权限修改目标任务的调度参数(通常需要超级用户权限或同一用户)
    • EFAULTparam 指向无效内存

注意

  • 保持策略不变:此函数只修改调度参数,不改变调度策略。如果需要同时修改策略和参数,使用 sched_setscheduler()
  • 优先级范围:每种调度策略有其有效的优先级范围,可以通过 sched_get_priority_min()sched_get_priority_max() 查询。超出范围会导致 EINVAL 错误。
  • 立即生效:优先级修改立即生效,调度器会重新评估任务优先级:
    • 如果新优先级更高,任务可能立即抢占当前任务
    • 如果新优先级更低,任务可能被其他高优先级任务抢占
    • 对于阻塞任务,新优先级在任务恢复运行时生效
  • 优先级继承:在实现互斥锁的优先级继承协议时,通常使用此函数临时提升低优先级任务的优先级,避免优先级反转。
  • 典型用法
    struct sched_param param;
    param.sched_priority = 100;  // 设置新优先级
    
    if (sched_setparam(0, &param) == 0) {
        printf("Priority changed to %d\n", param.sched_priority);
    } else {
        perror("sched_setparam");
    }
    
  • 查询当前参数:使用 sched_getparam() 获取任务当前的调度参数,然后修改需要改变的字段。
  • 实时系统调优:在实时系统中,根据任务的实际执行情况动态调整优先级,可以优化系统响应性和吞吐量。
  • 权限要求:修改其他任务的优先级通常需要特权。在嵌入式系统中,通常所有任务运行在同一权限级别,这一限制可能较宽松。
  • 对运行任务的影响:修改当前运行任务(pid=0)的优先级可能立即触发重新调度,如果系统中有更高优先级的就绪任务。
  • SCHED_SPORADIC 参数:对于零星调度任务,param 中的其他字段(如 sched_ss_low_priority)也可以通过此函数修改,实现动态调整零星服务器行为。

POSIX 兼容性:兼容 POSIX 同名接口。

sched_getparam

int sched_getparam(pid_t pid, struct sched_param *param);

查询指定任务的当前调度参数(主要是优先级)。这是获取任务优先级和其他调度参数的标准接口,常用于监控、调试和动态调度决策。

调度参数包括基本优先级以及特定策略相关的参数(如零星调度的补充周期),了解这些参数有助于理解任务的调度行为。

参数

  • pid 目标任务的 PID。特殊值 0 表示查询调用任务自身的参数。必须是有效的任务 PID。
  • param 指向 struct sched_param 结构的指针,用于接收查询结果。函数会填充此结构:
    • sched_priority:任务的基本优先级(始终设置)
    • 对于 SCHED_SPORADIC 策略,还包括 sched_ss_low_prioritysched_ss_repl_periodsched_ss_init_budgetsched_ss_max_repl 等零星服务器参数
    • 其他策略(SCHED_FIFO、SCHED_RR、SCHED_OTHER)通常只设置 sched_priority

返回值

  • 成功:返回 0,并在 param 中填充任务的调度参数
  • 失败:返回 -1 并设置 errno
    • ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • EINVAL:参数 pid 为负值
    • EFAULTparam 指向无效内存(NULL 或不可写)

注意

  • 只读查询:此函数不修改任务状态,是纯查询操作,开销很小。
  • 完整调度信息:通常与 sched_getscheduler() 配合使用,获取完整的调度信息(策略 + 参数):
    int policy = sched_getscheduler(0);
    struct sched_param param;
    sched_getparam(0, &param);
    printf("Policy: %d, Priority: %d\n", policy, param.sched_priority);
    
  • 典型用法
    struct sched_param param;
    if (sched_getparam(0, &param) == 0) {
        printf("Current priority: %d\n", param.sched_priority);
    } else {
        perror("sched_getparam");
    }
    
  • 动态调整参考:在动态调整优先级前,先用此函数获取当前参数,然后修改特定字段,最后用 sched_setparam() 应用更改。
  • 监控工具:系统监控工具常用此函数显示任务的优先级,帮助分析调度行为和诊断优先级反转等问题。
  • 零星调度参数:对于 SCHED_SPORADIC 策略的任务,此函数返回完整的零星服务器配置,包括预算、补充周期等。
  • 参数初始化:在调用前无需初始化 param 结构,函数会完全覆盖其内容。但确保 param 指向有效内存。
  • 任务诊断:在调试实时系统时,可以定期查询关键任务的优先级,验证优先级继承等机制是否正常工作。
  • 原子性:查询操作是原子的,返回的参数是一致的快照,不会出现部分更新的情况。

POSIX 兼容性:兼容 POSIX 同名接口。

调度控制

sched_yield

int sched_yield(void);

主动放弃 CPU,使调用任务重新进入就绪队列,允许调度器选择其他同等或更高优先级的任务运行。这是一种协作式调度机制,用于实现任务间的公平性和响应性。

对于 SCHED_FIFO 策略,任务会被移到其优先级队列的末尾;对于 SCHED_RR 策略,效果类似于时间片到期。这允许相同优先级的任务有机会运行。

参数

无参数。

返回值

  • 成功:返回 0(OK
  • 失败:返回 -1 并设置 errno(通常总是成功)

注意

  • 协作式调度:此函数是协作式多任务的关键,允许任务主动让出 CPU,提高系统整体响应性。
  • 不降低优先级sched_yield() 不改变任务优先级,只是暂时放弃执行权。任务仍然在相同优先级的就绪队列中。
  • 策略相关行为
    • SCHED_FIFO:任务移到其优先级队列的末尾,如果有其他同优先级任务就绪,它们会先运行
    • SCHED_RR:类似 SCHED_FIFO,且时间片计数器重置
    • 单任务情况:如果没有其他同等或更高优先级的就绪任务,调用任务会立即继续运行(yield 无效果)
  • 典型用法
    while (processing) {
        // 处理一批数据
        process_data_chunk();
        
        // 主动让出 CPU,允许其他任务运行
        sched_yield();
    }
    
  • 提高响应性:在长时间运行的循环中定期调用 sched_yield(),可以避免低优先级任务饿死,提高系统交互性。
  • 轮询优化:在轮询循环中使用 sched_yield() 可以减少 CPU 占用,给其他任务更多运行机会:
    while (!flag_is_set()) {
        sched_yield();  // 避免空转占用 CPU
    }
    
    但通常更好的方案是使用阻塞式等待(如信号量、条件变量)。
  • 与 sleep 的区别
    • sched_yield() 不保证任务会被阻塞,如果没有其他就绪任务,会立即继续运行
    • sleep() / usleep() 会使任务至少休眠指定时间,期间不消耗 CPU
  • 实时系统注意:在实时系统中,过度使用 sched_yield() 可能导致不确定性,应谨慎使用。优先使用显式同步机制(互斥锁、条件变量、信号量)。
  • 高优先级任务:如果调用任务是系统中优先级最高的任务,sched_yield() 通常无实际效果(立即返回继续运行)。
  • 时间片重置:对于 SCHED_RR 策略,sched_yield() 会重置时间片,相当于任务自愿放弃当前时间片。
  • 不可中断:即使调用 sched_yield() 后立即返回,也会发生上下文切换检查,调度器会重新评估调度决策。

POSIX 兼容性:兼容 POSIX 同名接口。

sched_get_priority_max

int sched_get_priority_max(int policy);

查询指定调度策略允许的最大(highest)优先级值。不同调度策略有不同的优先级范围,此函数用于获取有效的优先级上限,确保设置的优先级值在合法范围内。

在 openvela 中,数值越大表示优先级越高(与 Linux 相同,但与某些 RTOS 相反)。了解优先级范围对于正确配置实时任务至关重要。

参数

  • policy 调度策略,有效值包括:
    • SCHED_FIFO (0):先进先出实时调度
    • SCHED_RR (1):轮转实时调度
    • SCHED_SPORADIC (2):零星调度(如果支持)
    • SCHED_OTHER (3):标准分时调度
    • 其他系统支持的策略

返回值

  • 成功:返回指定策略的最大优先级值(正整数,通常为 255)
  • 失败:返回 -1 并设置 errno
    • EINVALpolicy 参数无效或不支持

注意

  • 配合使用:通常与 sched_get_priority_min() 配合使用,获取完整的优先级范围:
    int min_prio = sched_get_priority_min(SCHED_FIFO);
    int max_prio = sched_get_priority_max(SCHED_FIFO);
    printf("SCHED_FIFO priority range: %d to %d\n", min_prio, max_prio);
    
  • openvela 默认范围:在 openvela 中,大多数策略的优先级范围通常为 1 到 255,其中:
    • 1:最低优先级(通常是 IDLE 任务)
    • 255:最高优先级(紧急实时任务)
    • 100-200:典型应用任务优先级范围
  • 策略无关性:在 openvela 中,通常所有实时策略(SCHED_FIFO、SCHED_RR、SCHED_SPORADIC)共享相同的优先级空间,因此最大值相同。
  • 参数验证:在调用 sched_setparam()sched_setscheduler() 前,使用此函数验证优先级是否在有效范围内,避免 EINVAL 错误:
    int new_priority = 200;
    if (new_priority <= sched_get_priority_max(SCHED_FIFO)) {
        struct sched_param param = {.sched_priority = new_priority};
        sched_setscheduler(0, SCHED_FIFO, &param);
    }
    
  • 可移植性:不同操作系统的优先级范围可能不同,使用此函数可以编写可移植的代码,避免硬编码优先级值。
  • 典型用法
    int max = sched_get_priority_max(SCHED_FIFO);
    if (max < 0) {
        perror("sched_get_priority_max");
    } else {
        printf("Max priority for SCHED_FIFO: %d\n", max);
    }
    
  • 实时任务配置:在配置关键实时任务时,通常使用接近最大值的优先级,以确保任务能抢占其他任务。
  • 优先级分层:在复杂系统中,可以将优先级范围分为几个层次(如系统层、驱动层、应用层),每层使用不同的优先级子范围。

POSIX 兼容性:兼容 POSIX 同名接口。

sched_get_priority_min

int sched_get_priority_min(int policy);

查询指定调度策略允许的最小(lowest)优先级值。不同调度策略有不同的优先级范围,此函数用于获取有效的优先级下限,确保设置的优先级值在合法范围内。

在 openvela 中,数值越小表示优先级越低。最小优先级通常留给后台任务或空闲任务使用。

参数

  • policy 调度策略,有效值包括:
    • SCHED_FIFO (0):先进先出实时调度
    • SCHED_RR (1):轮转实时调度
    • SCHED_SPORADIC (2):零星调度(如果支持)
    • SCHED_OTHER (3):标准分时调度
    • 其他系统支持的策略

返回值

  • 成功:返回指定策略的最小优先级值(正整数,通常为 1)
  • 失败:返回 -1 并设置 errno
    • EINVALpolicy 参数无效或不支持

注意

  • 配合使用:通常与 sched_get_priority_max() 配合使用,获取完整的优先级范围:
    int min_prio = sched_get_priority_min(SCHED_RR);
    int max_prio = sched_get_priority_max(SCHED_RR);
    printf("SCHED_RR priority range: [%d, %d]\n", min_prio, max_prio);
    
  • openvela 默认值:在 openvela 中,最小优先级通常为 1(优先级 0 有时保留给系统或特殊用途)。
  • IDLE 任务:系统空闲任务(IDLE task)通常运行在最小优先级,仅在没有其他任务就绪时运行。
  • 后台任务:低优先级后台任务(如日志记录、统计)通常使用接近最小值的优先级,避免影响前台任务。
  • 参数验证:在设置优先级前,使用此函数验证优先级是否在有效范围内:
    int new_priority = 5;
    if (new_priority >= sched_get_priority_min(SCHED_FIFO) &&
        new_priority <= sched_get_priority_max(SCHED_FIFO)) {
        struct sched_param param = {.sched_priority = new_priority};
        sched_setparam(0, &param);
    }
    
  • 可移植性:不同操作系统的优先级范围可能不同,使用此函数可以编写可移植的代码,避免硬编码优先级值。
  • 典型用法
    int min = sched_get_priority_min(SCHED_FIFO);
    if (min < 0) {
        perror("sched_get_priority_min");
    } else {
        printf("Min priority for SCHED_FIFO: %d\n", min);
    }
    
  • 优先级分配策略:在设计系统时,应避免使用最小优先级(除非是真正的后台任务),以免任务长时间得不到 CPU 时间。
  • 策略无关性:在 openvela 中,通常所有实时策略共享相同的优先级空间,因此最小值也相同。
  • 调试工具:使用这些函数可以编写通用的优先级检查工具,验证系统中所有任务的优先级配置是否合理。

POSIX 兼容性:兼容 POSIX 同名接口。

sched_rr_get_interval

int sched_rr_get_interval(pid_t pid, struct timespec *interval);

查询使用 SCHED_RR(轮转)调度策略的任务的时间片长度。时间片是 SCHED_RR 策略中,任务在被强制让出 CPU 前可以连续运行的最大时间。

此函数用于了解系统的调度时间粒度,有助于调优实时应用的性能和响应性。

参数

  • pid 目标任务的 PID。特殊值 0 表示查询调用任务自身的时间片。必须是有效的任务 PID。
  • interval 指向 struct timespec 结构的指针,用于接收时间片长度。函数会填充此结构:
    • tv_sec:秒部分(通常为 0,因为时间片通常小于 1 秒)
    • tv_nsec:纳秒部分(例如 10,000,000 纳秒 = 10 毫秒)

返回值

  • 成功:返回 0(OK),并在 interval 中填充时间片长度
  • 失败:返回 -1 并设置 errno
    • ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • EINVALpid 为负值
    • EFAULTinterval 指向无效内存(NULL 或不可写)
    • ENOSYS:系统不支持此功能(SCHED_RR 未启用)

注意

  • 仅适用于 SCHED_RR:时间片概念仅对 SCHED_RR 策略有意义。对于 SCHED_FIFO,任务运行直到阻塞或被更高优先级任务抢占,没有时间片限制。
  • 系统级配置:时间片长度通常是系统级配置(编译时或启动时设置),不能针对单个任务修改。查询不同任务的时间片通常返回相同的值。
  • 典型值:在 openvela 中,默认时间片通常为 10 毫秒(10,000,000 纳秒),但可以通过配置选项调整(如 CONFIG_RR_INTERVAL)。
  • 典型用法
    struct timespec ts;
    if (sched_rr_get_interval(0, &ts) == 0) {
        long ms = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
        printf("Time slice: %ld ms\n", ms);
    } else {
        perror("sched_rr_get_interval");
    }
    
  • 性能调优:了解时间片长度有助于调优任务设计:
    • 如果任务的关键操作时间接近时间片长度,可能需要优化算法或考虑使用 SCHED_FIFO
    • 如果多个同优先级 SCHED_RR 任务需要公平共享 CPU,应确保它们的工作单元小于时间片
  • 实时性分析:时间片是实时系统响应时间分析的重要参数,影响任务的最坏响应时间。
  • 时间片耗尽:当 SCHED_RR 任务的时间片耗尽时,任务被移到其优先级队列末尾,时间片重新计数。
  • 调用 sched_yield():对于 SCHED_RR 任务,调用 sched_yield() 会重置时间片计数器。
  • 非 SCHED_RR 任务:即使任务当前不是 SCHED_RR 策略,此函数通常也会成功返回系统默认的时间片值,但此值对非 SCHED_RR 任务无实际意义。

POSIX 兼容性:兼容 POSIX 同名接口。

sched_lock

void sched_lock(void);

禁止任务调度(抢占)。调用后,当前任务不会被其他同优先级或更高优先级的任务抢占,直到调用 sched_unlock() 恢复调度。支持嵌套调用,每次 sched_lock() 必须对应一次 sched_unlock()

参数

无参数。

返回值

无返回值。

注意

  • 嵌套支持sched_lock() 维护一个锁计数器,每次调用递增,sched_unlock() 递减。只有计数器归零时才真正恢复调度。
  • 中断不受影响sched_lock() 只禁止任务级抢占,不禁止中断。中断处理程序仍然可以执行。
  • 与关中断的区别
    • sched_lock():禁止任务切换,中断仍可响应
    • enter_critical_section():禁止中断,更强的保护但延迟更大
  • 典型用法
    sched_lock();
    // 临界区:不会被其他任务抢占
    update_shared_data();
    sched_lock();  // 嵌套调用
    do_more_work();
    sched_unlock(); // 计数器减 1,仍然锁定
    sched_unlock(); // 计数器归零,恢复调度
    
  • SMP 注意:在多核系统中,sched_lock() 只保护当前 CPU 上的调度,其他 CPU 上的任务仍可运行。如需跨核保护,应使用自旋锁或其他 SMP 同步机制。
  • 避免长时间持有:长时间禁止调度会影响系统实时性,应尽量缩短临界区。

POSIX 兼容性:openvela/NuttX 扩展接口(非 POSIX 标准)。

sched_unlock

void sched_unlock(void);

恢复任务调度(抢占)。递减调度锁计数器,当计数器归零时恢复正常调度。必须与 sched_lock() 配对使用。

参数

无参数。

返回值

无返回值。

注意

  • 每次 sched_unlock() 对应一次 sched_lock(),不能多调用。
  • 计数器归零时,如果有更高优先级的任务就绪,会立即发生任务切换。

POSIX 兼容性:openvela/NuttX 扩展接口(非 POSIX 标准)。

sched_lockcount

int sched_lockcount(void);

查询当前任务的调度锁嵌套计数。返回值表示 sched_lock() 被调用但尚未被 sched_unlock() 匹配的次数。

参数

无参数。

返回值

返回当前任务的调度锁计数(非负整数)。0 表示调度未被锁定。

注意

  • 主要用于调试,验证 sched_lock() / sched_unlock() 是否正确配对。
  • 典型用法:
    int count = sched_lockcount();
    if (count > 0) {
        printf("Scheduler locked, count=%d\n", count);
    }
    

POSIX 兼容性:openvela/NuttX 扩展接口(非 POSIX 标准)。

CPU 亲和性

sched_getcpu

int sched_getcpu(void);

获取调用任务当前正在其上运行的 CPU 核心编号。此函数仅在 SMP(对称多处理)系统中有意义,用于查询任务与 CPU 的绑定关系。

在多核系统中,了解任务运行在哪个 CPU 上,有助于性能分析、调试和优化 CPU 亲和性策略。

参数

无参数。

返回值

  • 成功:返回当前运行的 CPU 编号(非负整数,从 0 开始编号)
    • 0:第一个 CPU 核心
    • 1:第二个 CPU 核心
    • ...依此类推
  • 失败:返回 -1 并设置 errno(通常不会失败)
    • ENOSYS:系统不支持 SMP(未启用 CONFIG_SMP

注意

  • SMP 系统专用:此函数仅在启用 CONFIG_SMP 配置的多核系统中有效。在单核系统中,通常总是返回 0。
  • 瞬时值:返回的 CPU 编号是查询时的瞬时值。在抢占式多任务系统中,任务可能在函数返回后立即被迁移到其他 CPU。
  • 典型用法
    int cpu = sched_getcpu();
    if (cpu >= 0) {
        printf("Running on CPU %d\n", cpu);
    } else {
        perror("sched_getcpu");
    }
    
  • 性能分析:在性能分析工具中,记录任务运行的 CPU 有助于分析 CPU 负载分布和任务迁移频率。
  • 调试工具:在调试 CPU 亲和性问题时,可以定期查询 CPU 编号,验证任务是否运行在预期的 CPU 上。
  • 配合亲和性:通常与 sched_setaffinity()sched_getaffinity() 配合使用:
    cpu_set_t set;
    CPU_ZERO(&set);
    CPU_SET(2, &set);  // 绑定到 CPU 2
    sched_setaffinity(0, sizeof(set), &set);
    
    int cpu = sched_getcpu();
    assert(cpu == 2);  // 验证绑定成功
    
  • 不可靠用于同步:不应依赖此函数实现同步机制,因为返回值可能在使用前失效。
  • NUMA 系统:在 NUMA(非统一内存访问)架构中,了解任务运行的 CPU 有助于优化内存访问模式,减少跨节点内存访问。
  • 热点分析:如果多个任务频繁运行在同一 CPU,可能表明负载不均衡,需要调整亲和性或优先级。
  • 中断上下文:此函数也可以在中断处理程序中调用,查询中断在哪个 CPU 上被处理。
  • 与线程绑定:在多线程应用中,可以将不同线程绑定到不同 CPU,然后用此函数验证绑定效果。

POSIX 兼容性:兼容 Linux 扩展接口(非 POSIX 标准,但广泛支持)。

sched_setaffinity

int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

设置任务的 CPU 亲和性掩码(affinity mask),指定任务允许运行的 CPU 核心集合。此功能仅在 SMP(对称多处理)系统中有效,需要启用 CONFIG_SMP 配置。

CPU 亲和性允许将任务绑定到特定的 CPU 核心,这在优化缓存局部性、减少上下文切换开销、隔离关键任务等场景中非常有用。

参数

  • pid 目标任务的 PID。特殊值 0 表示设置调用任务自身的亲和性。必须是有效的任务 PID。
  • cpusetsize mask 指向的 CPU 集合的大小(字节)。通常使用 sizeof(cpu_set_t)。此参数允许未来扩展支持更多 CPU。
  • mask 指向 CPU 亲和性掩码的指针(cpu_set_t 类型)。位图中每一位对应一个 CPU:
    • 位为 1:任务允许在该 CPU 上运行
    • 位为 0:任务不允许在该 CPU 上运行 使用 CPU_ZERO()CPU_SET()CPU_CLR() 等宏操作此掩码

返回值

  • 成功:返回 0(OK
  • 失败:返回 -1 并设置 errno
    • ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • EINVALmask 指定的 CPU 集合无效(例如全为 0,或包含不存在的 CPU)
    • EFAULTmask 指向无效内存(NULL 或不可读)
    • EPERM:调用者没有权限修改目标任务的亲和性(通常需要超级用户权限或同一用户)
    • ENOSYS:系统不支持 SMP(未启用 CONFIG_SMP

注意

  • SMP 系统专用:此函数仅在多核系统(CONFIG_SMP 启用)中有效。单核系统通常返回 ENOSYS
  • 立即生效:亲和性修改立即生效。如果任务当前运行在不在新掩码中的 CPU 上,调度器会立即将其迁移到允许的 CPU 之一。
  • 典型用法
    cpu_set_t set;
    CPU_ZERO(&set);           // 清空掩码
    CPU_SET(0, &set);         // 允许 CPU 0
    CPU_SET(1, &set);         // 允许 CPU 1
    
    if (sched_setaffinity(0, sizeof(set), &set) == 0) {
        printf("Affinity set to CPU 0 and 1\n");
    } else {
        perror("sched_setaffinity");
    }
    
  • 绑定到单个 CPU
    cpu_set_t set;
    CPU_ZERO(&set);
    CPU_SET(2, &set);  // 只允许 CPU 2
    sched_setaffinity(0, sizeof(set), &set);
    
  • 性能优化
    • 缓存局部性:将任务绑定到特定 CPU 可以提高缓存命中率,减少缓存失效
    • 减少迁移开销:避免任务在 CPU 间频繁迁移,降低上下文切换成本
    • 负载隔离:将关键实时任务绑定到专用 CPU,避免其他任务干扰
  • NUMA 系统:在 NUMA 架构中,应将任务绑定到靠近其访问内存的 CPU,减少跨节点内存访问延迟。
  • 权限要求:修改其他任务的亲和性通常需要特权。在嵌入式系统中,这一限制可能较宽松。
  • 子任务继承:子任务(通过 fork()task_create() 创建)通常继承父任务的 CPU 亲和性。
  • 配合使用:通常与 sched_getaffinity() 配合,先查询当前亲和性,修改后设置回去:
    cpu_set_t set;
    sched_getaffinity(0, sizeof(set), &set);
    CPU_CLR(3, &set);  // 移除 CPU 3
    sched_setaffinity(0, sizeof(set), &set);
    
  • 验证设置:设置后可以用 sched_getcpu() 验证任务是否运行在预期的 CPU 上。
  • 陷阱
    • 如果 mask 全为 0(不允许任何 CPU),会返回 EINVAL
    • 如果指定的 CPU 超出系统范围(如系统只有 4 核,但设置了 CPU 5),会返回 EINVAL
    • 过度绑定可能导致负载不均衡,某些 CPU 过载而其他 CPU 空闲
  • 动态调整:在运行时根据系统负载动态调整亲和性,可以实现灵活的负载均衡策略。

POSIX 兼容性:兼容 Linux 扩展接口(非 POSIX 标准,但广泛支持)。

sched_getaffinity

int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);

查询任务的 CPU 亲和性掩码(affinity mask),获取任务允许运行的 CPU 核心集合。此功能仅在 SMP(对称多处理)系统中有效,需要启用 CONFIG_SMP 配置。

通过查询 CPU 亲和性,可以了解任务的 CPU 绑定策略,用于监控、调试和动态调整亲和性。

参数

  • pid 目标任务的 PID。特殊值 0 表示查询调用任务自身的亲和性。必须是有效的任务 PID。
  • cpusetsize mask 指向的 CPU 集合的大小(字节)。通常使用 sizeof(cpu_set_t)
  • mask 指向 CPU 亲和性掩码的指针,用于接收查询结果。函数会填充此 cpu_set_t 结构,其中:
    • 位为 1:任务允许在该 CPU 上运行
    • 位为 0:任务不允许在该 CPU 上运行

返回值

  • 成功:返回 0(OK),并在 mask 中填充任务的 CPU 亲和性掩码
  • 失败:返回 -1 并设置 errno
    • ESRCH:指定的任务不存在(PID 无效或任务已终止)
    • EINVALcpusetsize 过小,无法容纳系统的 CPU 数量
    • EFAULTmask 指向无效内存(NULL 或不可写)
    • ENOSYS:系统不支持 SMP(未启用 CONFIG_SMP

注意

  • SMP 系统专用:此函数仅在多核系统(CONFIG_SMP 启用)中有效。单核系统通常返回 ENOSYS
  • 只读查询:此函数不修改任务状态,是纯查询操作,开销很小。
  • 典型用法
    cpu_set_t set;
    if (sched_getaffinity(0, sizeof(set), &set) == 0) {
        printf("Task can run on CPUs: ");
        for (int i = 0; i < CPU_SETSIZE; i++) {
            if (CPU_ISSET(i, &set)) {
                printf("%d ", i);
            }
        }
        printf("\n");
    } else {
        perror("sched_getaffinity");
    }
    
  • 检查特定 CPU
    cpu_set_t set;
    sched_getaffinity(0, sizeof(set), &set);
    if (CPU_ISSET(2, &set)) {
        printf("Task can run on CPU 2\n");
    }
    
  • 修改前查询:在修改亲和性前,通常先查询当前亲和性,然后基于当前值进行修改:
    cpu_set_t set;
    sched_getaffinity(0, sizeof(set), &set);
    CPU_CLR(1, &set);  // 移除 CPU 1
    sched_setaffinity(0, sizeof(set), &set);
    
  • 监控工具:系统监控工具常用此函数显示任务的 CPU 绑定情况,帮助分析负载分布。
  • 调试亲和性问题:如果任务性能异常,可以查询亲和性,检查是否被意外绑定到负载过重的 CPU。
  • 统计 CPU 数量
    cpu_set_t set;
    sched_getaffinity(0, sizeof(set), &set);
    int count = CPU_COUNT(&set);
    printf("Task can run on %d CPUs\n", count);
    
  • 默认亲和性:新创建的任务默认可以运行在所有 CPU 上(除非父任务有限制)。查询时会看到所有 CPU 位都为 1。
  • CPU_SETSIZE 常量CPU_SETSIZE 定义了 cpu_set_t 支持的最大 CPU 数量(通常为 1024),但实际系统 CPU 数量可能更少。
  • CPU 集合操作宏
    • CPU_ZERO(&set):清空集合
    • CPU_SET(cpu, &set):添加 CPU
    • CPU_CLR(cpu, &set):移除 CPU
    • CPU_ISSET(cpu, &set):检查 CPU 是否在集合中
    • CPU_COUNT(&set):统计集合中的 CPU 数量
    • CPU_EQUAL(&set1, &set2):比较两个集合是否相等
  • 可移植性:不同系统的 cpu_set_t 大小可能不同,始终使用 sizeof(cpu_set_t) 而不是硬编码大小。
  • 与 getcpu 配合
    int cpu = sched_getcpu();
    cpu_set_t set;
    sched_getaffinity(0, sizeof(set), &set);
    assert(CPU_ISSET(cpu, &set));  // 当前 CPU 应该在亲和性集合中
    

POSIX 兼容性:兼容 Linux 扩展接口(非 POSIX 标准,但广泛支持)。

sched_cpucount

int sched_cpucount(const cpu_set_t *set);

统计 CPU 集合中包含的 CPU 数量。等价于 Linux 的 CPU_COUNT() 宏。

参数

  • set 指向 CPU 集合。

返回值

返回集合中被设置的 CPU 数量。

注意

  • 在非 SMP 系统中,宏定义为始终返回 1。
  • 典型用法:
    cpu_set_t set;
    sched_getaffinity(0, sizeof(set), &set);
    printf("Can run on %d CPUs\n", sched_cpucount(&set));
    

POSIX 兼容性:兼容 Linux 扩展接口(非 POSIX 标准)。

调试与诊断

sched_backtrace

int sched_backtrace(pid_t tid, void **buffer, int size, int skip);

获取指定任务的调用栈回溯信息。将栈帧地址存入 buffer 数组,用于调试和崩溃分析。

参数

  • tid 目标任务的 PID。0 表示当前任务。
  • buffer 指向指针数组,用于存储栈帧地址。
  • size buffer 数组的最大容量(元素个数)。
  • skip 跳过的栈帧数(从栈顶开始),用于过滤调试框架本身的栈帧。

返回值

返回实际获取的栈帧数(非负整数)。如果返回值等于 size,可能还有更多栈帧未获取。

注意

  • 需要启用 CONFIG_SCHED_BACKTRACE 配置。未启用时,宏定义为返回 0。
  • 获取其他任务的调用栈时,目标任务应处于阻塞状态,否则结果可能不准确。

POSIX 兼容性:openvela/NuttX 扩展接口(非 POSIX 标准)。

sched_dumpstack

void sched_dumpstack(pid_t tid);

打印指定任务的调用栈到系统日志。内部调用 sched_backtrace() 获取栈帧,然后格式化输出。

参数

  • tid 目标任务的 PID。0 表示当前任务。

返回值

无返回值。

注意

  • 主要用于调试和崩溃分析,输出到系统日志(syslog)。
  • 需要启用 CONFIG_SCHED_BACKTRACE 配置。

POSIX 兼容性:openvela/NuttX 扩展接口(非 POSIX 标准)。