我只是想了解多线程环境,特别是如何在 c 中实现协作式环境(在 AVR 上,但出于兴趣,我想保持一般性)。
我的问题与线程切换本身有关:我很确定我可以在汇编程序中编写它,将所有寄存器刷新到堆栈中,然后保存 PC 以供以后返回。
如何在 c 中完成这样的事情?我被告知它可以做“一切”。
我意识到这是一个相当普遍的问题,因此任何有关此主题的信息的链接将不胜感激。
谢谢
我只是想了解多线程环境,特别是如何在 c 中实现协作式环境(在 AVR 上,但出于兴趣,我想保持一般性)。
我的问题与线程切换本身有关:我很确定我可以在汇编程序中编写它,将所有寄存器刷新到堆栈中,然后保存 PC 以供以后返回。
如何在 c 中完成这样的事情?我被告知它可以做“一切”。
我意识到这是一个相当普遍的问题,因此任何有关此主题的信息的链接将不胜感激。
谢谢
您可以在大多数系统上使用setjmp
/执行此操作longjmp
——这是我过去用于任务切换的一些代码:
void task_switch(Task *to, int exit)
{
int tmp;
int task_errno; /* save space for errno */
task_errno = errno;
if (!(tmp = setjmp(current_task->env))) {
tmp = exit ? (int)current_task : 1;
current_task = to;
longjmp(to->env, tmp); }
if (exit) {
/* if we get here, the stack pointer is pointing into an already
** freed block ! */
abort(); }
if (tmp != 1)
free((void *)tmp);
errno = task_errno;
}
这取决于sizeof(int) == sizeof(void *)
将指针作为参数传递给 setjmp/longjmp,但这可以通过使用句柄(索引到所有任务结构的全局数组中)而不是此处的原始指针或使用静态指针来避免。
当然,棘手的部分是jmpbuf
为新创建的任务设置对象,每个任务都有自己的堆栈。您可以为此使用带有 sigaltstack 的信号处理程序:
static void (*tfn)(void *);
static void *tfn_arg;
static stack_t old_ss;
static int old_sm;
static struct sigaction old_sa;
Task *current_task = 0;
static Task *parent_task;
static int task_count;
static void newtask()
{
int sm;
void (*fn)(void *);
void *fn_arg;
task_count++;
sigaltstack(&old_ss, 0);
sigaction(SIGUSR1, &old_sa, 0);
sm = old_sm;
fn = tfn;
fn_arg = tfn_arg;
task_switch(parent_task);
sigsetmask(sm);
(*fn)(fn_arg);
abort();
}
Task *task_start(int ssize, void (*_tfn)(void *), void *_arg)
{
Task *volatile new;
stack_t t_ss;
struct sigaction t_sa;
old_sm = sigsetmask(~sigmask(SIGUSR1));
if (!current_task) task_init();
tfn = _tfn;
tfn_arg = _arg;
new = malloc(sizeof(Task) + ssize + ALIGN);
new->next = 0;
new->task_data = 0;
t_ss.ss_sp = (void *)(new + 1);
t_ss.ss_size = ssize;
t_ss.ss_flags = 0;
if ((unsigned long)t_ss.ss_sp & (ALIGN-1))
t_ss.ss_sp = (void *)(((unsigned long)t_ss.ss_sp+ALIGN) & ~(ALIGN-1));
t_sa.sa_handler = newtask;
t_sa.sa_mask = ~sigmask(SIGUSR1);
t_sa.sa_flags = SA_ONSTACK|SA_RESETHAND;
sigaltstack(&t_ss, &old_ss);
sigaction(SIGUSR1, &t_sa, &old_sa);
parent_task = current_task;
if (!setjmp(current_task->env)) {
current_task = new;
kill(getpid(), SIGUSR1); }
sigaltstack(&old_ss, 0);
sigaction(SIGUSR1, &old_sa, 0);
sigsetmask(old_sm);
return new;
}
如果你想让它保持纯 C,我想你可能可以使用setjmp
and longjmp
,但我自己从未尝试过,我想可能有一些平台无法使用它(即某些寄存器/其他设置不是被拯救)。唯一的另一种选择是在汇编中编写它。
如前所述,setjmp/longjmp
它们是标准 C,甚至在 8 位 AVR 的 libc 中也可用。他们完全按照您在汇编程序中所说的那样做:保存处理器上下文。但是必须记住,这些功能的预期目的只是在控制流中向后跳转;在任务之间切换是一种滥用。无论如何它确实有效,而且看起来它甚至经常用于各种用户级线程库——比如 GNU Pth。但是,这仍然是对预期目的的滥用,需要小心。
正如 Chris Dodd 所说,您仍然需要为每个新任务提供一个堆栈。他使用sigaltstack()
了和其他与信号相关的函数,但这些函数在标准 C 中不存在,只存在于类 unix 环境中。例如,AVR libc 不提供它们。因此,作为替代方案,您可以尝试保留现有堆栈的一部分(通过声明一个大的本地数组或使用alloca()
)作为新线程的堆栈。请记住,主/调度线程将继续使用它的堆栈,每个线程都使用自己的堆栈,并且它们都会像堆栈通常那样增长和收缩,因此它们需要空间来这样做而不会相互干扰。
而且由于我们已经提到了类 unix 的非标准 C 机制,因此还有makecontext()/swapcontext()
and 家族,它们比setjmp()/longjmp()
. 名称确实说明了一切:上下文函数让您管理完整的进程上下文(包括堆栈),jmp函数让您只是跳来跳去 - 您必须破解其余的。
无论如何,对于 AVR,鉴于您可能没有操作系统可以提供帮助,也没有太多内存可以盲目地保留,您最好使用汇编程序进行切换和堆栈初始化。
根据我的经验,如果人们开始编写调度程序,他们很快就会开始想要网络堆栈、内存分配和文件系统等东西。走这条路几乎不值得。你最终花在编写自己的操作系统上的时间比花在实际应用程序上的时间还多。
您的项目朝这个方向发展的第一感觉,几乎总是值得将精力投入到现有的操作系统(linux、VxWorks 等)中。当然,这可能意味着如果 CPU 不能满足要求,您就会遇到问题。AVR 并不完全是大量的 CPU,并且在其上安装现有的操作系统对于主要操作系统来说从几乎不可能到棘手,尽管有一些小型操作系统(一些开源,请参阅http://en.wikipedia .org/wiki/List_of_real-time_operating_systems)。
因此,在项目开始时,您应该仔细考虑您可能希望如何将其发展到未来。这可能会影响您现在对 CPU 的选择,以免以后不得不在软件中做可怕的事情。