current任务 在昨天schar_read函数里的好几个地方,我们都用到了current宏定义,这个宏定义代表当前运行中的进程,是一个任务结构的数据项。 也就是说,我们在schar_read里处理的那个叫做
bcurrent的东西代表的就是正在读设备操作的当前进程。 任务结构有许多元素——它完整的清单可以在
linux/sched.h头里看到,下面我只列出与这个例子有关的项:
struct task_struct{
....
int sigpending;
....
pid_t pid;
....
sigset_t signal, blocked;
.......
}
正如看到的,系统里的所有的任务其实是链接在一个双向列表里的。state给出的是进程的当前状态,即它是正在运行、被停止、还是正在被切换。pid是该进程的程序标识码。signal保存着与发送给该进程的一切信号有关的信息,而blocked是进程本身决定屏蔽的信号掩码。最后,sigpending保存着与是否有一个非阻塞信号发送给了该里程有关的信息,signal_pending函数检查的就是这个变量。因此,如果signal_pending返回的真值,我们就通知VFS让它重新开始数据传输过程。
等待队列 没有数据可读的时候,我们利用等待队列把current任务设置为休眠状态,当有新的数据拷贝到schar设备时,我们再它唤醒。这就解放了系统,使它们可以去运行其它的进程。操作系统的进程调度器在我们唤醒任务之前是不会去考虑它的,而我们只有在使它休眠的条件得到了满足的前提下才会去唤醒它。这使内核代码对那些访问自己的用户空间应用程序拥有了极大的控制权。
schar_read就使用了这个
技术,下面看一下等待队列的数据结构定义:
struct wait_queue{
struct task_struct *task;
struct wait_queue *next;
}
这个结构比较明显,就不多讲了,task元素把被“催眠”的进程的有关信息保存在一个任务结构中, 而next是指向一等待队列中下一个条目的指针。如何让进程休眠呢,这就需要下面两条命令。
void interruptible_sleep_on(struct wait_queue **P)
long interruptilble_sleep_on_timeout(struct wait_queue **p, long timeout)
这两个宏命令的作用是把进程“催眠”为某个状态,但它们允许被信号唤醒。_timeout变体在内部调用了schedule_timeout,使作为其调用参数的等待队列能够让进程在时间到了的时候自动苏醒。这需要以后讲到的如何设定倒计时时间的。
void sleep_on(struct wait_queue **P)
long sleep_on_timeout(struct wait_queue **p, long timeout)
这两个宏定义的语法与上面的两个函数是完全一样的,只不过,进程的休眠状态被设置成为TASK_INTERRUPTIBLE.
可以在kernel/sched.c里查到它们 唤醒进程也是比较简单的,也不外乎下面两种方式:
wake_up_interruptible(struct wait_queue **p)
wake_up(struct wait_queue **P)
这两个由_wake_up延伸两个宏定义。前一个只能唤醒可中断休眠进程,而后一个可以唤醒两种休眠进程。但明确地被停止了执行(比如发送一个SIGSTOP信号)的进程不会被唤醒。
当schar设备里没有数据可提供时,它会把读数据进程催眠在自己的等待队列里。而当有一个写数据进程提供了足够多的数据,从而能够满足进程请求时,休眠进程将被 一个定时器唤醒。
interruptible_sleep_on(&schar_wq);
if(signal_pending(current))
return -EINTR;
这个结构在内核的许多地方都可以看到。我们把current进程催眠了,但允许它在信号的触发下苏醒过来。在interruptible_sleep_on成功之后,进程或者因为wake_up_interruptible调用而被唤醒,或者因为接收到一个信号而苏醒。如果后一种情况,signal_pending先要返回“1”,我们就会以利用返回一个“-EINTR”错误的办法来激发 一个中断调用,VFS会根据这个中断重新启动读数据进程。如果我们使用sleep_on函数简单地催眠了进程,进程就会进入不可中断的休眠以等待数据;在这种情况下,即使是一个SIGKILL信号了清能消除它。
就像我们前面介绍的那样,在定时器处理器和schar_write两个地方分别唤醒相应的读数据进程。
wake_up_interruptible(&schar_wq);
这个调用会把休眠在队列中的所有读数据进程都唤醒,这样做是否合理要看具体情况而定。 但这里我们只能满足一个读进程,所以我们是否应该唤醒所有的读进程让它们竞争数据呢? Schar设备可以通过设备的每次打开分别建立一个等待队列的办法来解决空上问题。这样做并不涉及什么新概念,只是让设备可以拥有自己的数据。
文件操作write:向设备写入数据 相对而言,schar_write就简单多了,它给schar_pool增加count个字节,再修改file->f_pos的值以反映出有多少数据被写到设备(实际上是读 到设备的内部缓冲区里)。之所以说它比读 操作简单的多就是因为schar不需要对写到设备里来的这些数据进行处理,它只要照单全收就可以了。 除此之外,那就是唤醒读数据的进程,如下面代码:
static ssize_t schar_write(struct file *file, const char *buf)
{
schar_pool +=count;
schar_data_written +=count;
//if we were really writing- modify the file position to reflect the amoun of data written
file->f_position += count;
if(copy_from_user(schar_buffer, buf,count))
return -EFAULT;
//wake up reading process, if any
if(schar_pool>0) {
wake_up_interruptible(&schar_wq);
wake_up_interruptible(&schar_poll_read);
}
MSG("trying to write %u bytes, %ld bytes in queue now\n", (unsigned) count, schar_pool);
//return the data written
retun count;
}
非阻塞性读操作 提供数据服务的驱动程序必须区分阻塞和非阻塞两种打开方式。如果没有足够的数据满足一个请求,我们通常的做法就是使进程进入休眠状态,而一旦有了足够数据,就再把它唤醒。但如果设备是以非阻塞方式打开 的,我们就不能像刚才那样做了; 我们必须尽可能多的供应数据,而进程在没有数据可读的时候会立刻返回而不是进入休眠状态。 给schar_read函数加上下面阴影部分的代码就实现了非阻塞读操作了;
static ssize_t schar_read(struct file *file, char *buf, size-t count, loft_t *offset)
...
while(count> schar_pool){
//if the device is opened non blocking satisfy what we can of the request and don't put the process to sleep.
if (file->f_flags & O_NONBLOCK){
if(schar_pool>0){
file->f_pos += schar_pool;
if(copy_to_user(buf,schar_buffer,schar_pool))
return -EFAULT;
count = schar_length;
schar_pool = 0;
return count;
}else{
return -EAGAIN;
}
MSG("putting process with pid %u to sleep\n", current->pid);
//go to sleep, but wake up on signals
interruptible_sleep_on(&schar_wq);
if(signal_pending(current)){
MSG("pid %u got signal\n", (unsigned) current->pid);
//tell vfs about the signal
return -EINTR;
}
}
}
...
}
schar_read检查f_flag以确定设备当前是以什么方式打开的。如果应用程序请求的数据比我们数据池里的多,就先把里面有的返回给应用程序,等数据池空了时再返回一个“-EAGAIN”错误。 这就暗示读数据进程应该过一会再来试试自己的请求。