操作系统学习:Linux0.12文件异步IO

本文参考书籍

1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言  从实模式到保护模式
4.Linux内核设计的艺术
ps:基于x86硬件的pc系统

Linux0.12异步IO

Linux在需要同时使用多个文件描述符来访问数据会间歇传输的IO设备,如网络设备等,如果使用多个read(),wirte()调用来处理,当其中一个调用堵塞时,会引发整个阻塞,其他文件描述符对应的读写操作就会得不到及时处理。
解决这种问题的方法可有多仲,一种方法时对每个文件需要同时访问的文件描述符设置一个进程来处理,但这种方法需要对这些进程之间的通信进行协调,实现相对复杂。另一种方法时把所有文件描述符都设置成非瑟赛形式,并且在程序中循环检测各个文件描述符是否有数据可读或可写,但是这样循环检测会耗费大量处理器时间,因此多任务操作系统中并不提倡使用该方法。最后一种就是使用异步IO技术,其原理是当一个描述符可被访问操作时就让内核使用信号来通知进程,由于每个进程只有一个这种通知信号,因此若使用了多个文件描述符,则还是需要把各个文件描述符设置成非阻塞状态,并且在接受到这种信号时对每个描述符进行测试,以确定哪个描述符已准备好。
在Linux0.12中提供了select函数,最初select函数出现在BSD4.2操作系统中,可在支持BSD socket网络编程接口的操作系统中使用,主要用于处理需要有效地同时访问多个文件描述符或socket句柄的情况,该函数的主要工作原理就是让内核同时监听用户提供的多个文件描述符,如果文件描述符的状态没有发生变化就让调用进程进入睡眠状态,如果其中有一个描述符已经准备好可被访问,该函数就返回进程,并告诉进程是哪个或哪几个描述符已准备好。

select函数分析
int select(int width, fd_set * readfds, fd_set * writefds,
    fd_set * exceptfds, struct timeval * timeout);

该函数主要传入了五个参数,由于Linux0.12内核只提供3个参数的系统调用,因此在用户程序调用select()函数时,库文件中的selelct()函数会把第1个参数的地址作为指针传递给内核的系统调用sys_select()函数。该函数会把第1个参数的指针作为存放有所有参数的缓冲区指针进行处理,该函数处理时会进参数进行分解,再调用do_select函数进行处理,然后在do_selelct函数返回时再把结果写到这个用户数据缓冲区类中。
函数中的五个参数分别是width是随后给出的3个描述符集中最大描述符的数值再加1,readfds,writefds,exceptfds代表的是读操作描述符,写操作描述符和发生异常的描述符集,分别对应为fd_set结构,最后一个参数timeout用于指定进程在任意一个描述符准备好之前希望等待select函数返回的最长时间,它是类型为timeval结构的一个指针,当timeout为NULL时,表示会无限期地等待下去,直到描述符中指定的描述符有一个已准备好可操作为止,如果进程收到一个信号,等待进程将被中断,并且select会返回-1,当timeout不为NULL时,若其结构中的字段值均为0,则表示无需等待,立刻返回,如果指针结构中的两个字段中至少有一个不为0则表示selecyt函数会在返回之前等待一段时间,若在等待期间有描述符准备好,就立马返回,若没有一个文件描述符准备好,select函数返回0。

select函数代码

首先在用户态使用select时就会发生系统调用,最终会调用sys_select函数;

int sys_select( unsigned long *buffer )   // 参数buffer指向参数列表的第一个参数
{
/* Perform the select(nd, in, out, ex, tv) system call. */
    int i;                                  
    fd_set res_in, in = 0, *inp;          // 读操作描述符表
    fd_set res_out, out = 0, *outp;       // 写描述符表
    fd_set res_ex, ex = 0, *exp;          // 异常描述符表
    fd_set mask;                          // 处理的描述符表掩码
    struct timeval *tvp;                  // 处理时间结构指针
    unsigned long timeout;                
                                                   // 从用户数据区将 数据复制到变量中
    mask = ~((~0) << get_fs_long(buffer++));       // 获取掩码值
    inp = (fd_set *) get_fs_long(buffer++);        // 获取读描述符
    outp = (fd_set *) get_fs_long(buffer++);       // 获取写描述符
    exp = (fd_set *) get_fs_long(buffer++);        // 获取异常描述符
    tvp = (struct timeval *) get_fs_long(buffer);  // 获取时间

    if (inp)                                       // 如果输入有效
        in = mask & get_fs_long(inp);              // 如设置掩码则获取屏蔽之后取读描述符表
    if (outp)                                      // 若输出有效
        out = mask & get_fs_long(outp);            // 获取屏蔽之后的掩码
    if (exp)                                       // 若异常描述符有效
        ex = mask & get_fs_long(exp);              // 获取屏蔽之后的异常描述符
    timeout = 0xffffffff;                          // 初始化为最大值
    if (tvp) {                                     // 若有设置的时间值
        timeout = get_fs_long((unsigned long *)&tvp->tv_usec)/(1000000/HZ);  // 将微妙转换成秒
        timeout += get_fs_long((unsigned long *)&tvp->tv_sec) * HZ;          // 获取秒级时间
        timeout += jiffies;                        // 加上当前系统的滴答时间就是需要等待的时间
    }
    current->timeout = timeout;                    // 设置当前进程延时的时间
    cli();                                         // 关闭系统中断,为了避免竞争
    i = do_select(in, out, ex, &res_in, &res_out, &res_ex);   // 处理描述符
    if (current->timeout > jiffies)                // 如果当前进程延时时间大于系统当前滴答值
        timeout = current->timeout - jiffies;      // 则获取当前剩余时间
    else
        timeout = 0;                               // 如果延时时间已经小于当前系统滴答值则证明时间已经过了设置为0
    sti();                                         // 开启中断
    current->timeout = 0;                          // 设置当前进程的超时字段置0
    if (i < 0)                                     // 若处理返回结果小于0 则直接返回
        return i;
    if (inp) {                                     // 若可短描述符表有值
        verify_area(inp, 4);                       // 将描述符回写到用户缓冲区
        put_fs_long(res_in,inp);
    }
    if (outp) {                                    // 若有可写描述符表则回将数据返回
        verify_area(outp,4);
        put_fs_long(res_out,outp);
    }
    if (exp) {                                     // 若有异常描述符则回写
        verify_area(exp,4);
        put_fs_long(res_ex,exp);
    }
    if (tvp) {                                     // 如果时间需要回写则将事件转换后回写
        verify_area(tvp, sizeof(*tvp));
        put_fs_long(timeout/HZ, (unsigned long *) &tvp->tv_sec);
        timeout %= HZ;
        timeout *= (1000000/HZ);
        put_fs_long(timeout, (unsigned long *) &tvp->tv_usec);
    }
    if (!i && (current->signal & ~current->blocked))  // 若当前没有准备好的描述符,并且收到了某个非阻塞信号则返回中断错误号
        return -EINTR;
    return i;                                      // 否认这返回准备好的描述符值
}

该函数先将参数进行处理与参数转换工作,然后调用do_select函数去完成剩余的文件描述符的查找工作,等处理完成后就把处理结果再复制回用户空间中去。
此时主要的完成工作由do_select完成;

int do_select(fd_set in, fd_set out, fd_set ex,
    fd_set *inp, fd_set *outp, fd_set *exp)    
{
    int count;                                      // 已就绪的描述个数计数值
    select_table wait_table;                        // 等待表结构
    int i;
    fd_set mask;                                    // 掩码

    mask = in | out | ex;                           // 进行或操作女
    for (i = 0 ; i < NR_OPEN ; i++,mask >>= 1) {    // 循环判断描述符值,mask右移一位
        if (!(mask & 1))                            // 如果当前不在描述集中则下一位
            continue;
        if (!current->filp[i])                      // 如果若该文件未打开则返回出错
            return -EBADF;
        if (!current->filp[i]->f_inode)             // 如果该文件没有对应的i节点为空则返回错误值
            return -EBADF;
        if (current->filp[i]->f_inode->i_pipe)      // 若打开的文件i节点是管道描述符则有效继续下一位循环
            continue;
        if (S_ISCHR(current->filp[i]->f_inode->i_mode))   // 判断打开是否是字符设备
            continue;
        if (S_ISFIFO(current->filp[i]->f_inode->i_mode))  // 打开的是否是FIFO
            continue;
        return -EBADF;                              // 否则返回出错
    }
repeat:
    wait_table.nr = 0;                               // 设置计数值为0
    *inp = *outp = *exp = 0;
    count = 0;
    mask = 1;
    for (i = 0 ; i < NR_OPEN ; i++, mask += mask) {  // 遍历检查打开的文件描述符
        if (mask & in)                               // 检查是否在读操作描述符中
            if (check_in(&wait_table,current->filp[i]->f_inode)) { 
                *inp |= mask;                        // 如果在则将苗书集中设置对应位
                count++;                             // 设置已准备好描述符个数加1
            }
        if (mask & out)                              // 检查是否在写操作描述符集中
            if (check_out(&wait_table,current->filp[i]->f_inode)) { 
                *outp |= mask;                       // 若在则设置描述符对应位并加1
                count++;
            }
        if (mask & ex)                               // 检查是否在异常描述符集中
            if (check_ex(&wait_table,current->filp[i]->f_inode)) {
                *exp |= mask;                        // 若在则设置对应位并计数加1
                count++; 
            }
    }
    if (!(current->signal & ~current->blocked) &&          // 若在进程处理后没有已准备好的描述符并且此时进程没有收到非阻塞信号
        (wait_table.nr || current->timeout) && !count) {    // 并且此时等待的描述符或等待时间没有超时
        current->state = TASK_INTERRUPTIBLE;                // 把当前状态设置成可中断睡眠状态
        schedule();                                         // 执行调度函数
        free_wait(&wait_table);                             // 重新执行时唤醒相关等待队列上前后的任务
        goto repeat;                                        // 重新检查
    }
    free_wait(&wait_table);                                 // 若此时count不为0,或者收到了信号或者等待时间到并且没有需要等待的描述符,则唤醒队列
    return count;                                           // 返回已好描述符个数
}

do_select函数主要是先检查了描述符集中各个描述符的有效性,然后分别调用相关描述符检查函数对每个描述符进行检查,同时统计描述符中已经准备好的描述符个数,如果有任何一个准备好了就立刻返回,否则就进入睡眠状态,并在过了超时时间或者由于某个描述符所在等待队列上的进程被唤醒而使该函数继续运行下去。其中相关的检查函数的如下;

static int check_in(select_table * wait, struct m_inode * inode)
{
    struct tty_struct * tty;

    if (tty = get_tty(inode))                               // 是否是终端设备
        if (!EMPTY(tty->secondary))                         // 若是终端如果缓冲队列中有字符可读则返回1
            return 1;
        else
            add_wait(&tty->secondary->proc_list, wait);     // 否则添加到等待队列
    else if (inode->i_pipe)                                 // 如果是管道设备
        if (!PIPE_EMPTY(*inode))                            // 如果缓冲区中有数据可读
            return 1;                                       // 返回1
        else
            add_wait(&inode->i_wait, wait);                 // 添加管道到等待队列中
    return 0;                                               // 否则返回0
}

static int check_out(select_table * wait, struct m_inode * inode)
{
    struct tty_struct * tty;

    if (tty = get_tty(inode))                               // 检查是否是终端设备
        if (!FULL(tty->write_q))                            // 如果是则检查终端写缓冲区是否已经满了如果没满则返回1
            return 1;
        else
            add_wait(&tty->write_q->proc_list, wait);       // 添加到等待队列中
    else if (inode->i_pipe)                                 // 是否是管道设备
        if (!PIPE_FULL(*inode))                             // 检查管道设备写缓冲区是否已满
            return 1;                                       // 如果没满可写则返回1 
        else
            add_wait(&inode->i_wait, wait);                 // 添加到等待任务队列中
    return 0;
}

static int check_ex(select_table * wait, struct m_inode * inode)
{
    struct tty_struct * tty;

    if (tty = get_tty(inode))                               // 如果是终端设备则返回0
        if (!FULL(tty->write_q))
            return 0;
        else
            return 0;
    else if (inode->i_pipe)                                 // 如果是管道设备
        if (inode->i_count < 2)                             // 若此时管道文件有一个或都已关闭则返回1
            return 1;
        else
            add_wait(&inode->i_wait,wait);                  // 添加到等待队列中
    return 0;
}

对应do_select中的进程的等待队列的函数add_wait与free_wait函数如下;

static void add_wait(struct task_struct ** wait_address, select_table * p)
{
    int i;

    if (!wait_address)                                  // 判断是否有等待队列
        return;                                         // 如果没有则直接返回
    for (i = 0 ; i < p->nr ; i++)                       // 在select_table中查找
        if (p->entry[i].wait_address == wait_address)   // 如果当前等待队列指针已经设置则返回
            return;
    p->entry[p->nr].wait_address = wait_address;        // 设置队列中的等待队列的头指针
    p->entry[p->nr].old_task = * wait_address;          // 将等待表项的old_task指向等待队列头指针指向的任务
    *wait_address = current;                            // 让等待队列头指针指向当前任务
    p->nr++;                                            // 计数加1
}

static void free_wait(select_table * p)
{
    int i;
    struct task_struct ** tpp;

    for (i = 0; i < p->nr ; i++) {                      // 循环遍历select_table表
        tpp = p->entry[i].wait_address;                 // 获取队列中的队列等待任务
        while (*tpp && *tpp != current) {               // 如果tpp有效并且当前任务不是获取的队列任务
            (*tpp)->state = 0;                          // 则设置当队列任务为运行态
            current->state = TASK_UNINTERRUPTIBLE;      // 设置当前任务为不可中断等待
            schedule();                                 // 执行调度
        }
        if (!*tpp)                                      // 如果为空则打印错误信息
            printk("free_wait: NULL");
        if (*tpp = p->entry[i].old_task)                // 此时等待列表当前处理的等待队列头指针指向当前任务,若此时还有头任务则将头任务设置为运行状态
            (**tpp).state = 0;                      
    }
    p->nr = 0;                                          // 将等待表的计数nr置0 
}

这个等待队列的形成与sleep_on函数即调度中的睡眠函数需要保持兼容,在调度中的sleep_on函数是通过tmp指针指向等待任务队列的上一个任务,在select中将old_task指向了上一个任务,wait_address仅仅是保存了等待任务的地址,此时再free_wait函数中的old_task被唤醒后就将原任务设置成就绪态,从而达到了继续任务队列的唤醒工作。
至此,select对于文件描述符的相关操作分析完成。现在的select函数支持的功能更为强大,包括网络描述符的监听,其异步IO的实现方式也有poll,epoll等其他形式的实现,大家有兴趣可以自行学习。

猜你喜欢

转载自blog.csdn.net/qq_33339479/article/details/80788001