一、为什么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的一些说明中描述。