交互shell设置为/dev/console之后提示job control turned off(下)

一、为什么ash本身从串口读取没问题
这个是我之后突然又想到的一个问题,为什么直接继承init的console文件描述符之后,作为交互式的ash没有出现read失败,而单单密码的读取会出现失败呢?如果是读取用户命令行输入的时候修改了文件描述符的属性,那么在读取密码的时候就应该也没有问题,这个就比较诡异了。
二、ash输入读取
在busybox-1.14.2\shell\ash.c文件中,对于用户输入的执行命令为
preadfd-->>>read_line_input--->>>nonblock_safe_read
这个函数的注释比这个函数的实现要多不少,而且我也是一个很喜欢看注释的人,特别是那些比较人性化的注释,这里的注释说到了我前一篇文章《文件在多大程度、多大范围共享》中说明的fcntl对于系统级文件描述符的影响,但是作者这里提出了一个非常好的兼容实现方法, 那就是首先尝试直接从其中读取,但是会做额外判断,如果返回值是EAGAIN(也就是O_NONBLOCK属性串口读取而串口中没有输入数据时的返回值),则通过poll系统调用阻塞(而不是通过read阻塞),并且把这个<read,poll>放在一个循环中,从而在数据到来的时候被唤醒 ,这样就是一个非常好的用户态兼容实现。

/* Suppose that you are a shell. You start child processes.
 * They work and eventually exit. You want to get user input.
 * You read stdin. But what happens if last child switched
 * its stdin into O_NONBLOCK mode?
 *
 * *** SURPRISE! It will affect the parent too! ***
 * *** BIG SURPRISE! It stays even after child exits! ***
 *
 * This is a design bug in UNIX API.
 *      fcntl(0, F_SETFL, fcntl(0, F_GETFL, 0) | O_NONBLOCK);
 * will set nonblocking mode not only on _your_ stdin, but
 * also on stdin of your parent, etc.
 *
 * In general,
 *      fd2 = dup(fd1);
 *      fcntl(fd2, F_SETFL, fcntl(fd2, F_GETFL, 0) | O_NONBLOCK);
 * sets both fd1 and fd2 to O_NONBLOCK. This includes cases
 * where duping is done implicitly by fork() etc.
 *
 * We need
 *      fcntl(fd2, F_SETFD, fcntl(fd2, F_GETFD, 0) | O_NONBLOCK);
 * (note SETFD, not SETFL!) but such thing doesn't exist.
 *
 * Alternatively, we need nonblocking_read(fd, ...) which doesn't
 * require O_NONBLOCK dance at all. Actually, it exists:
 *      n = recv(fd, buf, len, MSG_DONTWAIT);
 *      "MSG_DONTWAIT:
 *      Enables non-blocking operation; if the operation
 *      would block, EAGAIN is returned."
 * but recv() works only for sockets!
 *
 * So far I don't see any good solution, I can only propose
 * that affected readers should be careful and use this routine,
 *  which detects EAGAIN and uses poll() to wait on the fd .
 *  Thankfully, poll() doesn't care about O_NONBLOCK flag .
 */
ssize_t FAST_FUNC nonblock_safe_read(int fd, void *buf, size_t count)
{
    struct pollfd pfd[1];
    ssize_t n;

    while (1) {
        n =  safe_read( fd, buf, count); 这里的safe_read也比较简单,就是判断了read系统调用是否是被信号打断而提前返回
        if (n >= 0 || errno != EAGAIN)
            return n;
        /* fd is in O_NONBLOCK mode. Wait using poll and repeat */
        pfd[0].fd = fd;
        pfd[0].events = POLLIN;
         safe_poll (pfd, 1, -1);
    }
}
三、密码读取
我们看一下密码的读取,其接口为
busybox-1.14.2\libbb\bb_askpass.c
char* FAST_FUNC bb_ask(const int fd, int timeout, const char *prompt)

    /* On timeout or Ctrl-C, read will hopefully be interrupted,
     * and we return NULL */
    if ( read( fd, passwd, sizeof_passwd - 1) > 0) { 这个是一个赤裸裸的read,由于标准输入设置为O_NONBLOCK,所以此处返回值为负数-EAGAIN
        ret = passwd;
        i = 0;
        /* Last byte is guaranteed to be 0
           (read did not overwrite it) */
        do {
            if (passwd[i] == '\r' || passwd[i] == '\n')
                passwd[i] = '\0';
        } while (passwd[i++]);
    }
四、衍生的问题
这里可以衍生出一些有意思的现象。比如说如果没有判断read的返回值就直接操作这个指针,很可能会出现指针访问异常。你条件反射性的启动调试器,奇怪的是这个问题不再复现,因为gdb使用了readline库,readline设置标准输入为阻塞模式,然后子进程会集成这个标志位,然后读入的时候就会正常,这就是不复现的情况。更有甚者,只要运行一次gdb,什么都不做,串口同样会被设置为非阻塞,同样之后不再复现。
五、poll的内核实现及一个竞争问题
作为一个喜欢一口气看下去的程序员,我看了一下内核的poll实现,的确没有啥好说的,应该还是比较直观的。但是想到了一个问题:对于poll系统调用,它可以提供多个轮询的fd,而内核会在所有这些文件的等待队列中挂上一个代理(就是通过__pollwait将一个wait_queue_t实例添加到文件等待队列中,然后当文件可读或者可写的时候将自己唤醒)。直观的想,这里有一个竞争情况。假设说用户要poll10个文件,内核四处奔波,在前五个文件的等待队列中挂载了代理,然后在不辞劳苦的继续挂载,但是在挂载第六个文件的时候, 之前已经挂载的第三个文件准备就绪(考虑内核抢占以及多核情况),开始唤醒等待实例,而此时sys_poll调用者还在忙着挂载接下来的5个文件等待队列,此时这次唤醒是否会丢失
do_sys_poll-->>do_poll
for (;;) {
        struct poll_list *walk;
        long __timeout;

         set_current_state(TASK_INTERRUPTIBLE ); 这里在没有挂载任何等待队列之前,先把当前线程设置为睡眠状态。这一点对可能的“唤醒丢失”问题有关键作用
        for (walk = list; walk != NULL; walk = walk->next) {
            struct pollfd * pfd, * pfd_end;

            pfd = walk->entries;
            pfd_end = pfd + walk->len;
            for (; pfd != pfd_end; pfd++) {
                /*
                 * Fish for events. If we found one, record it
                 * and kill the poll_table, so we don't
                 * needlessly register any other waiters after
                 * this. They'll get immediately deregistered
                 * when we break out and return.
                 */
                if (do_pollfd(pfd, pt)) {
                    count++;
                    pt = NULL;
                }
            }
        }
……
        __timeout =  schedule_timeout (__timeout);
}
从代码里看,假设出现这种情况,就绪文件会把执行poll的任务设置为可运行状态,也就是将这个循环开始的set_current_state(TASK_INTERRUPTIBLE);设置的进程状态设置为TASK_RUNNING,此时poll的确还是不依不饶的将接下来的五个队列挂上,并且也的确会在接下来的schedule_timeout中让出控制权,但是此时的唤醒没有丢失,因为在schedule_timeout函数中,调度器会发现当前任务依然是可运行状态,所以当前进程依然有机会再次获得调度权(如果它有足够高的优先级或者满足调度切换算法的话),所以不会丢失“唤醒”。
六、todo
可以看到的是,在bb_ask函数中
(read(fd, passwd, sizeof_passwd - 1) > 0)
其中
enum { sizeof_passwd = 128 };
也就是read系统调用传入的参数大小是128,那么假设用户的密码只有12个字符,那么这个read函数将会何时返回,内核如何返回,内核相关处理代码及逻辑在哪里?这些由于和tty关系比较紧密,所以放入tty的一些说明中描述。

你可能感兴趣的:(shell)