linux内核的syslets补丁

linux的系统调用及其丰富,但是却都是同步的,虽然2.6内核新增加了异步io,但是对于套接字等却以及非直接读写io却不可用,于是开发者就有了一 个想法,既然一些任务可以由内核的xxlet或者内核线程(工作队列)来异步执行,比如tasklet,那么能否建立一套异步执行系统调用的机制呢?这就 是syslets的由来。
syslets按照常规思想来理解就是建立一个新的线程来执行被阻塞的系统调用,然后被阻塞的线程直接返回用户空间,等到新线程完成系统调用后用事件或者信号来通知用户空间或者由用户空间来轮询。果真是这样吗?如果是的话,那么改动就大了去了,再说这也不是linux的风格,linux旨在提供一种将系统 调用设置成异步的机制,并没有说要抛弃同步调用,另外linux的风格就是一点一点的模块正交积累,互不影响。于是可以肯定内核补丁肯定不是按照以上方式实现syslets的,那么怎么实现的呢?我分析完这个补丁真相就大白了(保留+,省略了-)。
syslets补丁用到了一个叫做sys_indirect的系统调用,它实际上就是对所有系统调用的包装。在用户空间需要一个系统调用的时候不再直接调用之了,而是间接调用indirect,且看:

asmlinkage long sys_indirect(struct indirect_registers __user *userregs,...

if (copy_from_user(&regs, userregs, sizeof(regs)))

return -EFAULT;

if (paramslen > sizeof(union indirect_params))

return -EINVAL;

if (copy_from_user(&current->indirect_params, userparams, paramslen)) {

result = -EFAULT;

goto out;

}

if (unlikely(syslet_args_present(&current->indirect_params))) {

result = syslet_pre_indirect(); //前置工作

if (result == 0) {

result = call_indirect(&regs); //调用系统调用函数

result = syslet_post_indirect(result); //收尾工作

}

goto out;

}

......

return result;

这里重要的是syslet_pre_indirect,call_indirect和syslet_post_indirect,我们一个一个分析,在分析之前,我们必须熟悉数据结构,每个task_struct中增加了几个字段:
struct task_struct {
spinlock_t syslet_lock;
struct list_head syslet_tasks; //这个队列保留所有这个task_struct的syslets
unsigned syslet_ready:1, //这几个位域在下面会用到
syslet_return:1,
syslet_exit:1;
};
数据结构主要就是以上的这个,至于syslets本身的,看看字面就可以理解,就不多说了,首先看看syslet_pre_indirect:

int syslet_pre_indirect(void)

{

struct task_struct *cur = current;

struct syslet_ring __user *ring;

u32 elements;

int ret;

if (!syslet_frame_valid(&cur->indirect_params.syslet.frame)) {

ret = -EINVAL;

goto out;

}

ring = (struct syslet_ring __user __force *)(unsigned long)

cur->indirect_params.syslet.completion_ring_ptr;

if (get_user(elements, &ring->elements)) {

ret = -EFAULT;

goto out;

}

if (!is_power_of_2(elements)) {

ret = -EINVAL;

goto out;

}

if (list_empty(&cur->syslet_tasks)) { //如果一个可用的syslet也没有了

ret = create_new_syslet_task(cur); //创建一个新的syslet

if (ret) //如果ret返回错误,那么到out,后面可以看到,在do_fork的时候不管是父线程还是子线程,都会将返回值设置为0,如果这个ret不为0,那么它只可能是负数,即错误码。

goto out;

} else

ret = 0;

cur->syslet_ready = 1;

out:

return ret;

}

call_indirect 实际上就是调用了原始的系统调用,比如sys_read,但是在sys_read的过程当中可能会发生阻塞,然后怎么办呢?我们的初衷是想让程序异步返 回,而不是被阻塞,然而如果一被阻塞就返回,那么代码的改动太大,只要有阻塞的地方都要判断是否在异步系统调用中,如果是那么直接返回一个错误码,代表系统调用没有完成,返回用户空间之前还要建立或者唤醒一个工作队列之类的线程帮忙完成系统调用,这个工程十分庞大,根本就不是一个补丁所能完成,可能还要彻 底颠覆linux
的设计思想。于是我们可以换一种思路,就是阻塞的系统调用不返回,继续阻塞,返回的是新建立的线程,新线程返回前从原始线程接手一切,然后返回,这就相当于原始线程返回,不同的是线程号变了,但是无关紧要,原始线程阻塞前交待了“后事”,然后就长眠了,新线程返回后沿着原始线程没有走完的路继续前进,等到原始线程完成阻塞的时候,将会通知新线程,然后原始线程也返回,这里就应该注意了,原始线程已经将执行任务完全交给新线程了,那么原始线程会返回到哪里 呢?实际上
在异步系统调用开始的时候要初始化一个函数指针,就是原始线程返回的地点,一般在这个函数里面直接exit即可,因为任务已经转手,留着它,多余了。
现在明白了syslet的机理,那么就看看代码吧:

asmlinkage void __sched schedule(void)

{

...

prev = current;

if (unlikely(prev->syslet_ready)) {

if (prev->state && !(preempt_count() & PREEMPT_ACTIVE) && //prev->state为真代表被阻塞而不是事件片原因的调度(cfs已经没有事件片了)

(!(prev->state & TASK_INTERRUPTIBLE) ||

!signal_pending(prev)))

syslet_schedule(prev);//调度current的syslet,实际上就是要返回用户空间

}

...

}

void syslet_schedule(struct task_struct *cur)

{

struct task_struct *child = NULL;

spin_lock(&cur->syslet_lock);

child = first_syslet_task(cur);//cur的所有的syslet连接成一个链表,这里取出第一个,每一个代表一个task_struct。

if (child) {

move_user_context(child, cur); //这个函数具体结接了用户空间的工作,见下面。

set_user_frame(cur, &cur->indirect_params.syslet.frame);//设置原始线程,即被阻塞的线程的返回地址

cur->syslet_ready = 0;

child->syslet_return = 1;//告知新线程返回用户空间以代替原始线程继续革命。

}

spin_unlock(&cur->syslet_lock);

if (child)

wake_up_process(child);

}

void move_user_context(struct task_struct *dest, struct task_struct *src)

{

struct pt_regs *old_regs = task_pt_regs(src);

struct pt_regs *new_regs = task_pt_regs(dest);

union i387_union *tmp;

*new_regs = *old_regs;

...

}

当初在create_new_syslet_task中建立的那个线程就是syslet即新线程,该线程的线程函数是:

static long syslet_thread(void *data)

{

struct syslet_task_args args;

struct task_struct *cur = current;

struct syslet_task_entry entry = {

.task = cur,

.item = LIST_HEAD_INIT(entry.item),

};

args = *(struct syslet_task_args *)data;

spin_lock(&args.parent->syslet_lock);

list_add_tail(&entry.item, &args.parent->syslet_tasks);

spin_unlock(&args.parent->syslet_lock);

complete(args.comp);

for (;;) {

set_task_state(cur, TASK_INTERRUPTIBLE);

//cur->syslet_return 在syslet_schedule中被设置为1,这里检测到就退出了for循环,如果原始系统调用从来都没有被阻塞,那么建立的syslet实际上是多余 的,在exit的时候因为要杀死它的所有的syslet,因此还会延迟exit。

if (cur->syslet_return || cur->syslet_exit || signal_pending(cur))

break;

schedule();

}

set_task_state(cur, TASK_RUNNING);

spin_lock(&args.parent->syslet_lock);

list_del(&entry.item);

if (list_empty(&args.parent->syslet_tasks))

wake_up_process(args.parent);

spin_unlock(&args.parent->syslet_lock);

if (!cur->syslet_return)

do_exit(0);

return -ESYSLETPENDING; //返回用户空间,告知用户空间系统调用将在将来完成。

}

如 果原始线程阻塞了,那么它将等待,等到系统调用完成了,原始线程要调用syslet_post_indirect,该函数有两个作用,一是通知用户空间, 也就是它的代理,实际上此时它的代理已经全权接管了它的工作,通知系统调用完成了,二是返回用户空间,返回的位置以及堆栈的位置由库函数确定,这种实现确实有些ugly,但是也很灵活。
以上就是syslets的大致框架,可以看出,该实现的都实现了,但是总觉得不是那么规范,比如线程转交这件事我就咋看咋别扭,还有就是原始线程返回用户空间以后到底做些什么事,这是内核所无法确定的,内核一向要对用户空间的大政方针负责,这里实现好像违背了这个原则。总之,linux是灵活的,小颗粒的对象组成了稳定的系统内核,这个特点使得它无论实现什么特性都不是很难,因为它是拼装而成的,没有牵一发而动全身的“优点”,虽然这个补丁不是那么优秀, 但是我对syslets的前景还是抱有很大希望的,希望它能趋于完善。

你可能感兴趣的:(linux)