tty_driver-line_discipline
1. line discipline
line discipline 介于 TTY 层和具体的串口驱动 ( 比如 serial8250 ) 之间。
发送数据时,应用程序通过系统调用向 TTY 设备文件写入数据,进而调用 TTY 层驱动程序执行写操作。 TTY 层驱动程序调用 line discipline 的写函数,根据 TTY 设置的参数对写入的数据进行格式化,然后通过具体的串口驱动发送。
接收数据时,具体的串口驱动收到数据后,根据 TTY 的设置参数对数据进行处理,
根据内核中 drivers/tty/n_tty.c 中的注释,读和写可以视为消费者和生产者。两个过程操作时需要用到 line discipline ,对输入和输出进行格式化。
一个 line discipline 由两部分组成:
-
line discipline 包含的所有操作,对应数据结构 struct tty_ldisc_ops ,
TTY 使用的 line discipline 保存在变量 tty_ldiscs 中,这个变量通过函数tty_register_ldisc
添加新成员。// drivers/tty/tty_ldisc.c static struct tty_ldisc_ops *tty_ldiscs[NR_LDISCS]; tty_register_ldisc(int disc, struct tty_ldisc_ops *new_ldisc) { tty_ldiscs[disc] = new_ldisc; } // drivers/tty/n_tty.c #define N_TTY 0 static struct tty_ldisc_ops n_tty_ops = { .magic = TTY_LDISC_MAGIC, .name = "n_tty", .open = n_tty_open, .close = n_tty_close, .flush_buffer = n_tty_flush_buffer, .read = n_tty_read, .write = n_tty_write, .ioctl = n_tty_ioctl, .set_termios = n_tty_set_termios, .poll = n_tty_poll, .receive_buf = n_tty_receive_buf, .write_wakeup = n_tty_write_wakeup, .receive_buf2 = n_tty_receive_buf2, }; n_tty_init -> tty_register_ldisc(N_TTY, &n_tty_ops) // kernel/printk/printk.c console_init -> n_tty_init
-
line discipline 所有操作使用的参数,对应的数据结构是 struct n_tty_data ,保存在 struct tty_struct 的 disc_data 成员中:
struct n_tty_data { /* producer-published */ size_t read_head; // 当前 write 操作的 index size_t commit_head; size_t canon_head; // canonical 模式 write 操作的 index size_t echo_head; size_t echo_commit; size_t echo_mark; DECLARE_BITMAP(char_map, 256); /* private to n_tty_receive_overrun (single-threaded) */ unsigned long overrun_time; int num_overrun; /* non-atomic */ bool no_room; /* must hold exclusive termios_rwsem to reset these */ unsigned char lnext:1, erasing:1, raw:1, real_raw:1, icanon:1; unsigned char push:1; /* shared by producer and consumer */ char read_buf[N_TTY_BUF_SIZE]; // 保存数据的 buf DECLARE_BITMAP(read_flags, N_TTY_BUF_SIZE); // flag = 1 表示 EOL unsigned char echo_buf[N_TTY_BUF_SIZE]; // 保存 echo 数据的 buf /* consumer-published */ size_t read_tail; // 当前 read 操作的 index size_t line_start; // 新行的 index /* protected by output lock */ unsigned int column; unsigned int canon_column; size_t echo_tail; struct mutex atomic_read_lock; struct mutex output_lock; };
disc_data 包含 line discipline 对输入和输入执行所做的处理,即根据 TTY 的参数处理数据,其中的部分参数介绍如下:
icanon 指明终端的工作模式:
canonical 模式下
-
数据按行进行输入,输入以下行分隔符 ( NL , EOL , EOL2 ,行首的 EOF ) 表示一个输入行。 EOF 的情况,
read
返回的缓冲区包含行分隔符。 -
开启了行编辑 ( ERASE , KILL ;如果设置了 IEXTEN ,还包括 WERASE , REPRINT , LNEXT ) 。一次
read
最多返回一行输入;如果read
请求的字节数少于当前输入行的字节数,只会读取请求的字节数,剩余的字符由下一次read
获取。 -
行的最大长度为 4096 个字符 ( 包括结尾的行分隔符 ) ,大于 4096 字符的部分会被截断。 4095 以后的字符依然进行输入处理 ( 比如 ISIG , ECHO* 处理 ) ,但是 4095 后的字符到行分隔符之间的部分都会被抛弃。保证终端总是能够接收多行,读取至少一行。
noncanonical 模式下
输入立即可得 ( 不需要用户输入行分隔符 ) ,不会执行输入处理操作,行编辑也会关闭。读缓冲区只会接受 4095 个字符 —— 如果切换到 canonical 模式,留有一个行分隔符的空间。 MIN ( c_cc[VMIN]
) 和 TIME ( c_cc[VTIME]
) 决定何时 read
完成,包括以下四种情况:
-
MIN == 0 , TIME == 0 ( polling read )
如果有数据可用,read
立即返回,返回的数据少于可用的字节数,或者请求的字节数;如果没有数据可用,返回 0 。 -
MIN > 0 , TIME == 0 ( blocking read )
read
阻塞,直到有 MIN 字节可用,返回最多请求的字节数。 -
MIN == 0 , TIME > 0 ( read with timeout )
TIME 指明计时器的值,以秒记。调用read
时计时器开始。至少有一字节数据,或者计时器超时后read
返回。如果超时没有任何输入,read
返回 0 。如果调用read
时数据已经可用,执行方式和调用read
后立即收到数据一样。 -
MIN > 0 , TIME > 0 ( read with interbyte timeout )
TIME 指明计时器的值,以秒记。收到第一个字节启动后,每次收到新的字节,计时器都会重置。read
在满足以下条件时返回:- 收到了 MIN 个字节
- interbyte 计时器超时
- 收到了
read
请求的字节数
由于计时器在收到第一个字节后才会开始,因此至少会收到一个字节。如果调用 read
时数据已经可用,执行方式和调用 read
后立即收到数据一样。
POSIX 没有指明 O_NONBLOCK 文件状态标志是否在 MIN 和 TIME 设置之前生效。如果设置了 O_NONBLOCK ,无论 MIN 和 TIME 如何设置, canonical 模式的 read
可能会立即返回。而且,如果没有数据可用, POSIX 允许 noncanonical 模式的 read
操作返回 0 , -1 或者 EAGAIN 。
Raw mode
cfmakeraw
设置终端工作在 raw 模式下,和老的版本 7 终端驱动类似:一个字符一个字符的输入,关闭回响,关闭终端的所有输入输出字符的特殊处理,终端的属性设置如下:
termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
| INLCR | IGNCR | ICRNL | IXON);
termios_p->c_oflag &= ~OPOST;
termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
termios_p->c_cflag &= ~(CSIZE | PARENB);
termios_p->c_cflag |= CS8;
Line control
#include <termios.h>
#include <unistd.h>
int tcsendbreak(int fd, int duration);
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);
void cfmakeraw(struct termios *termios_p);
speed_t cfgetispeed(const struct termios *termios_p);
speed_t cfgetospeed(const struct termios *termios_p);
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
int cfsetspeed(struct termios *termios_p, speed_t speed);
如果终端使用异步串行数据发送, tcsendbreak
持续发送一个连续的 0 值比特流。如果持续的时间 drain 为 0 ,发送至少 0.25s 的 0 值比特,不超过 0.5s 。如果持续时间不为 0 ,发送 0 值比特一段跟实现相关的时间。
如果终端没有使用异步串行数据发送, tcsendbreak
直接返回。
tcdrain
等待所有输出到 fd 指明的对象发送完成。
tcflush
丢弃写入到 fd 指明的、但是还没有发送的数据;或者收到但还没有读取的数据,取决于 queue_selector :
- TCIFLUSH —— flush 收到但没有读取的数据
- TCOFLUSH —— flush 写入但没有发送的数据
- TCIOFLUSH —— flush 收到但没有读取、写入但没有发送的数据
tcflow
挂起 fd 指明的数据发送或者接收,取决于 action :
- TCOOF —— 挂起输出
- TCOON —— 重启挂起的输出
- TCIOFF —— 发送一个 STOP 字符,停止终端向系统发送数据
- TCION —— 发送一个 START 字符,开始终端向系统发送数据
打开一个终端文件时,默认输入和输出都是挂起的。
1.1. n_tty_write
对于 TTY 文件,使用的 static struct tty_ldisc_ops 为 n_tty_ops ,其中 write
函数为 n_tty_write
。
tty_write
到 n_tty_write
的整个函数调用路径为: tty_write
-> do_tty_write
-> n_tty_write
。
static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
const unsigned char *buf, size_t nr)
{
const unsigned char *b = buf;
// 定义一个等待队列项 wait ,成员函数为 woken_wake_function
DEFINE_WAIT_FUNC(wait, woken_wake_function);
int c;
ssize_t retval = 0;
/* Job control check -- must be done at start (POSIX.1 7.1.1.4). */
if (L_TOSTOP(tty) && file->f_op->write != redirected_tty_write) {
retval = tty_check_change(tty);
if (retval)
return retval;
}
down_read(&tty->termios_rwsem);
/* Write out any echoed characters that are still pending */
process_echoes(tty);
/*
* 清除 wait 的 WQ_FLAG_EXCLUSIVE 标志,
* 添加 wait 到等待队列 write_wait
*/
add_wait_queue(&tty->write_wait, &wait);
while (1) {
// 当前进程处于挂起状态,设置返回值为 -ERESTARTSYS
if (signal_pending(current)) {
retval = -ERESTARTSYS;
break;
}
// tty 文件处于挂起状态,或者 pty 的另一端没有使用者
if (tty_hung_up_p(file) || (tty->link && !tty->link->count)) {
retval = -EIO;
break;
}
// 开启了实现相关 ( 对于特殊字符的处理方法不同 ) 的输出处理标志
if (O_OPOST(tty)) {
while (nr > 0) {
// 对 b 内的数据进行输出处理并输出
ssize_t num = process_output_block(tty, b, nr);
/*
* 只有 process_output_block 调用 uart_write 函数
* 会返回负值,即 -EL3HLT ,意味着在端口被关闭后调用了
* uart_write 函数。
*/
if (num < 0) {
// serial 条件不可能成立
if (num == -EAGAIN)
break;
retval = num;
goto break_out;
}
b += num;
nr -= num;
if (nr == 0)
break;
c = *b;
/*
* process_ouput 先调用 tty_write_room 获取
* 可用的空间,然后调用 do_output_char 输出字符
* 可用空间为 0 时返回值小于 0 。
*/
if (process_output(c, tty) < 0)
break;
b++; nr--;
}
/*
* 上面 while 中的 break 条件成立才会执行,
* 即 uart_flush_chars -> uart_start
*/
if (tty->ops->flush_chars)
tty->ops->flush_chars(tty);
} else {
struct n_tty_data *ldata = tty->disc_data;
while (nr > 0) {
mutex_lock(&ldata->output_lock);
// 对输出数据不进行处理直接输出
c = tty->ops->write(tty, b, nr);
mutex_unlock(&ldata->output_lock);
if (c < 0) {
retval = c;
goto break_out;
}
if (!c)
break;
b += c;
nr -= c;
}
}
if (!nr)
break;
if (file->f_flags & O_NONBLOCK) {
retval = -EAGAIN;
break;
}
up_read(&tty->termios_rwsem);
wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
down_read(&tty->termios_rwsem);
}
break_out:
// 设置当前线程为运行状态,将 wait 从 write_wait 中删除
remove_wait_queue(&tty->write_wait, &wait);
// 数据还没有写完,并且 async ,设置 TTY_DO_WRITE_WAKEUP
if (nr && tty->fasync)
set_bit(TTY_DO_WRITE_WAKEUP, &tty->flags);
up_read(&tty->termios_rwsem);
// 返回写的字节数,或者错误代码
return (b - buf) ? b - buf : retval;
}
1.1.1. process_output_block
内核中注释说明函数的功能如下:
进行 OPOST 处理后,输出一块字符,返回输出的字符数。
这个函数用于加速处理块输出数据时的块控制台写操作,只会处理正常找到的简单情况,帮助控制台驱动生成块级符号,从而提升性能。
static ssize_t process_output_block(struct tty_struct *tty,
const unsigned char *buf, unsigned int nr)
{
struct n_tty_data *ldata = tty->disc_data;
int space;
int i;
const unsigned char *cp;
mutex_lock(&ldata->output_lock);
/*
* 如果 tty->ops->write_room 定义,就调用,比如
* uart_write_room:
* 从 tty->driver_data 获取 uart_state,进而获取 xmit ,
* 从而得到 xmit 中的空闲空间。
* 否则返回 2048 。
*/
space = tty_write_room(tty);
if (!space) {
mutex_unlock(&ldata->output_lock);
return 0;
}
// 实际输出的字符数不能大于可用空间
if (nr > space)
nr = space;
// 依次处理 buf 中的每个字符
for (i = 0, cp = buf; i < nr; i++, cp++) {
unsigned char c = *cp;
switch (c) {
case '\n':
// 不输出回车 CR
if (O_ONLRET(tty))
ldata->column = 0;
// 将 NL 映射为 CR-NL
if (O_ONLCR(tty))
goto break_out;
// canon_column = 0
ldata->canon_column = ldata->column;
break;
case '\r':
// 不在 column 0 输出回车 CR ,并且 column = 0
if (O_ONOCR(tty) && ldata->column == 0)
goto break_out;
// 将 CR 映射为 NL
if (O_OCRNL(tty))
goto break_out;
// 设置 column 为 0 ,从行首输出
ldata->canon_column = ldata->column = 0;
break;
case '\t':
goto break_out;
case '\b':
// 退格
if (ldata->column > 0)
ldata->column--;
break;
default:
// 非控制字符
if (!iscntrl(c)) {
// 将小写字符映射为大写字符
if (O_OLCUC(tty))
goto break_out;
if (!is_continuation(c, tty))
ldata->column++;
}
break;
}
}
break_out:
/*
* uart_write :
* 从 tty->driver_data 获取 uart_state ,进而获取
* state->xmit ,将 buf 内的 i 字节数据拷贝到 xmit 中,
* 调用 __uart_start 发送 xmit 内的数据。
*/
i = tty->ops->write(tty, buf, i);
mutex_unlock(&ldata->output_lock);
return i;
}
1.1.2. do_output_char
内核中该函数的注释如下:
这个函数帮助处理单个输出字符,包括特殊字符: TAB , CR , LF 等,执行 OPOST 处理,将结果保存到 tty 驱动的 write buffer 。返回使用的缓冲区空间的字符数,无可用空间时返回 0 。
这个函数在调用 process_output_block
后调用,后者遇到特殊字符就会跳出循环,输出已经处理的输出字符,从而调用 do_output_char
处理特殊字符,完成后再次调用 process_output_block
,从而处理缓冲区中的所有数据。
static int do_output_char(unsigned char c, struct tty_struct *tty, int space)
{
struct n_tty_data *ldata = tty->disc_data;
int spaces;
if (!space)
return -1;
switch (c) {
case '\n':
if (O_ONLRET(tty))
ldata->column = 0;
// 映射 CR 为 CR-NL ,需要两个字符的空间
if (O_ONLCR(tty)) {
if (space < 2)
return -1;
ldata->canon_column = ldata->column = 0;
tty->ops->write(tty, "\r\n", 2);
return 2;
}
ldata->canon_column = ldata->column;
break;
case '\r':
if (O_OCRNL(tty)) {
c = '\n';
if (O_ONLRET(tty))
ldata->canon_column = ldata->column = 0;
break;
}
ldata->canon_column = ldata->column = 0;
break;
case '\t':
spaces = 8 - (ldata->column & 7);
if (O_TABDLY(tty) == XTABS) {
if (space < spaces)
return -1;
ldata->column += spaces;
tty->ops->write(tty, " ", spaces);
return spaces;
}
ldata->column += spaces;
break;
case '\b':
if (ldata->column > 0)
ldata->column--;
break;
default:
if (!iscntrl(c)) {
if (O_OLCUC(tty))
c = toupper(c);
if (!is_continuation(c, tty))
ldata->column++;
}
break;
}
/*
* 如果 tty->ops->put_char 定义,调用,即 uart_put_char;
* 否则调用 tty->ops->write 输出字符。
* uart_put_char 将字符插入到 xmit 中后即返回。
*/
tty_put_char(tty, c);
return 1;
}
1.2. n_tty_read
读操作对应的函数为 n_tty_read
,根据 TTY 的参数对收到的数据进行处理。
/*
* @tty : tty 设备
* @file : 文件对象
* @buf :用户空间缓冲区
* @nr : I/O 的大小
*/
static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,
unsigned char __user *buf, size_t nr)
{
struct n_tty_data *ldata = tty->disc_data;
unsigned char __user *b = buf;
DEFINE_WAIT_FUNC(wait, woken_wake_function);
int c;
int minimum, time;
ssize_t retval = 0;
long timeout;
int packet;
size_t tail;
c = job_control(tty, file);
if (c < 0)
return c;
/*
* Internal serialization of reads.
*/
if (file->f_flags & O_NONBLOCK) {
if (!mutex_trylock(&ldata->atomic_read_lock))
return -EAGAIN;
} else {
if (mutex_lock_interruptible(&ldata->atomic_read_lock))
return -ERESTARTSYS;
}
down_read(&tty->termios_rwsem);
minimum = time = 0;
timeout = MAX_SCHEDULE_TIMEOUT;
// noncanonical 模式
if (!ldata->icanon) {
minimum = MIN_CHAR(tty);
if (minimum) {
time = (HZ / 10) * TIME_CHAR(tty);
} else {
timeout = (HZ / 10) * TIME_CHAR(tty);
minimum = 1;
}
}
packet = tty->packet;
tail = ldata->read_tail;
// 添加 wait 到等待队列 tty->read_wait
add_wait_queue(&tty->read_wait, &wait);
while (nr) {
/* First test for status change. */
if (packet && tty->link->ctrl_status) {
unsigned char cs;
if (b != buf)
break;
spin_lock_irq(&tty->link->ctrl_lock);
cs = tty->link->ctrl_status;
tty->link->ctrl_status = 0;
spin_unlock_irq(&tty->link->ctrl_lock);
if (put_user(cs, b)) {
retval = -EFAULT;
break;
}
b++;
nr--;
break;
}
// read_buf 中无可用字节数
if (!input_available_p(tty, 0)) {
up_read(&tty->termios_rwsem);
// 等待 tty->port.work 执行完毕
tty_buffer_flush_work(tty->port);
down_read(&tty->termios_rwsem);
// tty->port.work 执行完毕后依然没有数据
if (!input_available_p(tty, 0)) {
// tty 另一端被关闭,返回 -EIO
if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {
retval = -EIO;
break;
}
// tty 被挂起
if (tty_hung_up_p(file))
break;
/*
* noncanonical 模式, MIN 为 0 , timeout 为 0 ,
* polling read 模式,没有数据可用直接返回
*/
if (!timeout)
break;
// 非阻塞模式,可以进行重试,返回 -EAGAIN
if (file->f_flags & O_NONBLOCK) {
retval = -EAGAIN;
break;
}
// 进程被挂起,返回 -ERESTARTSYS
if (signal_pending(current)) {
retval = -ERESTARTSYS;
break;
}
up_read(&tty->termios_rwsem);
// 以上条件都不满足,等待一段时间后再次尝试
timeout = wait_woken(&wait, TASK_INTERRUPTIBLE,
timeout);
down_read(&tty->termios_rwsem);
continue;
}
}
// canonical 模式,
if (ldata->icanon && !L_EXTPROC(tty)) {
retval = canon_copy_from_read_buf(tty, &b, &nr);
if (retval)
break;
} else {
int uncopied;
/* Deal with packet mode. */
if (packet && b == buf) {
if (put_user(TIOCPKT_DATA, b)) {
retval = -EFAULT;
break;
}
b++;
nr--;
}
uncopied = copy_from_read_buf(tty, &b, &nr);
uncopied += copy_from_read_buf(tty, &b, &nr);
if (uncopied) {
retval = -EFAULT;
break;
}
}
n_tty_check_unthrottle(tty);
if (b - buf >= minimum)
break;
if (time)
timeout = time;
}
if (tail != ldata->read_tail)
n_tty_kick_worker(tty);
up_read(&tty->termios_rwsem);
remove_wait_queue(&tty->read_wait, &wait);
mutex_unlock(&ldata->atomic_read_lock);
if (b - buf)
retval = b - buf;
return retval;
}
其中, input_available_p
定义如下:
static inline int input_available_p(struct tty_struct *tty, int poll)
{
struct n_tty_data *ldata = tty->disc_data;
// poll 模式, block read 操作, MIN 为等待的字节数
int amt = poll && !TIME_CHAR(tty) && MIN_CHAR(tty) ? MIN_CHAR(tty) : 1;
// canonical 模式下, canon_head == read_tail ,表明 buf 为空
if (ldata->icanon && !L_EXTPROC(tty))
return ldata->canon_head != ldata->read_tail;
// noncanonical 模式下, buf 中的字符少于 1 就为空
else
return ldata->commit_head - ldata->read_tail >= amt;
}
1.2.1. canon_copy_from_read_buf
canonical 模式下拷贝数据到用户空间缓冲区:
static int canon_copy_from_read_buf(struct tty_struct *tty,
unsigned char __user **b,
size_t *nr)
{
struct n_tty_data *ldata = tty->disc_data;
size_t n, size, more, c;
size_t eol;
size_t tail;
int ret, found = 0;
/* N.B. avoid overrun if nr == 0 */
if (!*nr)
return 0;
// nr+1 为请求的字节数,后者为 canonical 模式下可用的字节数
n = min(*nr + 1, smp_load_acquire(&ldata->canon_head) - ldata->read_tail);
tail = ldata->read_tail & (N_TTY_BUF_SIZE - 1);
// size 为 read_tail + n
size = min_t(size_t, tail + n, N_TTY_BUF_SIZE);
n_tty_trace("%s: nr:%zu tail:%zu n:%zu size:%zu\n",
__func__, *nr, tail, n, size);
eol = find_next_bit(ldata->read_flags, size, tail);
more = n - (size - tail);
if (eol == N_TTY_BUF_SIZE && more) {
/* scan wrapped without finding set bit */
eol = find_next_bit(ldata->read_flags, more, 0);
found = eol != more;
} else
found = eol != size;
n = eol - tail;
if (n > N_TTY_BUF_SIZE)
n += N_TTY_BUF_SIZE;
c = n + found;
if (!found || read_buf(ldata, eol) != __DISABLED_CHAR) {
c = min(*nr, c);
n = c;
}
n_tty_trace("%s: eol:%zu found:%d n:%zu c:%zu tail:%zu more:%zu\n",
__func__, eol, found, n, c, tail, more);
// 将数据拷贝到用户空间缓冲区
ret = tty_copy_to_user(tty, *b, tail, n);
if (ret)
return -EFAULT;
*b += n;
*nr -= n;
if (found)
clear_bit(eol, ldata->read_flags);
smp_store_release(&ldata->read_tail, ldata->read_tail + c);
if (found) {
if (!ldata->push)
ldata->line_start = ldata->read_tail;
else
ldata->push = 0;
tty_audit_push();
}
return 0;
}
1.2.2. n_tty_check_unthrottle
static void n_tty_check_unthrottle(struct tty_struct *tty)
{
if (tty->driver->type == TTY_DRIVER_TYPE_PTY) {
if (chars_in_buffer(tty) > TTY_THRESHOLD_UNTHROTTLE)
return;
n_tty_kick_worker(tty);
// pty 唤醒与之相关联的 tty 设备的 write_wait 队列
tty_wakeup(tty->link);
return;
}
/* If there is enough space in the read buffer now, let the
* low-level driver know. We use chars_in_buffer() to
* check the buffer, as it now knows about canonical mode.
* Otherwise, if the driver is throttled and the line is
* longer than TTY_THRESHOLD_UNTHROTTLE in canonical mode,
* we won't get any more characters.
*/
while (1) {
int unthrottled;
tty_set_flow_change(tty, TTY_UNTHROTTLE_SAFE);
if (chars_in_buffer(tty) > TTY_THRESHOLD_UNTHROTTLE)
break;
// 如果需要的话启动 input worker
n_tty_kick_worker(tty);
// uart_unthrottle -> serial8250_unthrottle , NULL
unthrottled = tty_unthrottle_safe(tty);
if (!unthrottled)
break;
}
__tty_set_flow_change(tty, 0);
}
作者:glob
出处:http://www.cnblogs.com/adera/
欢迎访问我的个人博客:https://blog.globs.site/
本文版权归作者和博客园共有,转载请注明出处。