console调linux设备,linux设备驱动之console控制台驱动-程序员宅基地

技术标签: console调linux设备  

我们在之前分析过input子系统和tty设备驱动架构.今天需要将两者结合起来.看看linux中的控制台是怎么样实现的.

二:控制台驱动的初始化

之前在分析tty驱动架构的时候曾分析到.主设备为4,次设备为0的设备节点,即/dev/tty0为当前的控制终端.

有tty_init()中,有以下代码段:

static int __init tty_init(void)

{

……

……

#ifdef CONFIG_VT

cdev_init(&vc0_cdev, &console_fops);

if (cdev_add(&vc0_cdev, MKDEV(TTY_MAJOR, 0), 1) ||

register_chrdev_region(MKDEV(TTY_MAJOR, 0), 1, "/dev/vc/0") < 0)

panic("Couldn't register /dev/tty0 driver\n");

device_create(tty_class, NULL, MKDEV(TTY_MAJOR, 0), "tty0");

vty_init();

#endif

return 0;

}

CONFIG_VT:是指配置虚拟终端.即我们所说的控制台.在此可以看到TTY_MAJOR(4),0对应的设备节点操作集为console_fops.

继续跟进vty_init()

int __init vty_init(void)

{

vcs_init();

console_driver = alloc_tty_driver(MAX_NR_CONSOLES);

if (!console_driver)

panic("Couldn't allocate console driver\n");

console_driver->owner = THIS_MODULE;

console_driver->name = "tty";

console_driver->name_base = 1;

console_driver->major = TTY_MAJOR;

console_driver->minor_start = 1;

console_driver->type = TTY_DRIVER_TYPE_CONSOLE;

console_driver->init_termios = tty_std_termios;

console_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_RESET_TERMIOS;

tty_set_operations(console_driver, &con_ops);

if (tty_register_driver(console_driver))

panic("Couldn't register console driver\n");

kbd_init();

console_map_init();

#ifdef CONFIG_PROM_CONSOLE

prom_con_init();

#endif

#ifdef CONFIG_MDA_CONSOLE

mda_console_init();

#endif

return 0;

}

经过我们之前的tty驱动架构分析,这段代码看起来就比较简单了,它就是注册了一个tty驱动.这个驱动对应的操作集是位于con_ops里面的.

仔细看.在之后还会调用kbd_init().顾名思义,这个是一个有关键盘的初始化.控制终端跟键盘有什么关系呢?在之前分析tty的时候,

曾提到过,.对于控制台而言,它的输入设备是键盘鼠标,它的输出设备是当前显示器.这两者是怎么关联起来的呢?不着急.请看下面的分析.

三:控制台的open操作

在前面分析了,对应console的操作集为con_ops.定义如下:

static const struct file_operations console_fops = {

.llseek                = no_llseek,

.read                   = tty_read,

.write                  = redirected_tty_write,

.poll           = tty_poll,

.ioctl          = tty_ioctl,

.compat_ioctl    = tty_compat_ioctl,

.open                  = tty_open,

.release    = tty_release,

.fasync               = tty_fasync,

};

里面的函数指针值我们都不陌生了,在之前分析的tty驱动中已经分析过了.

结合前面的tty驱动分析.我们知道在open的时候,会调用ldisc的open和tty_driver.open.

对于ldisc默认是tty_ldiscs[0].我们来看下它的具体赋值.

console_init():

void __init console_init(void)

{

initcall_t *call;

/* Setup the default TTY line discipline. */

(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);

/*

* set up the console device so that later boot sequences can

* inform about problems etc..

*/

call = __con_initcall_start;

while (call < __con_initcall_end) {

(*call)();

call++;

}

}

在这里,通过tty_register_ldisc.将tty_ldisc_N_TTY注册为了第N_TTY项.即第1项. tty_ldisc_N_TTY定义如下:

struct tty_ldisc tty_ldisc_N_TTY = {

.magic           = TTY_LDISC_MAGIC,

.name            = "n_tty",

.open            = n_tty_open,

.close           = n_tty_close,

.flush_buffer    = n_tty_flush_buffer,

.chars_in_buffer = n_tty_chars_in_buffer,

.read            = read_chan,

.write           = write_chan,

.ioctl           = n_tty_ioctl,

.set_termios     = n_tty_set_termios,

.poll            = normal_poll,

.receive_buf     = n_tty_receive_buf,

.write_wakeup    = n_tty_write_wakeup

}

对应的open操作为n_tty_open:

static int n_tty_open(struct tty_struct *tty)

{

if (!tty)

return -EINVAL;

/* This one is ugly. Currently a malloc failure here can panic */

if (!tty->read_buf) {

tty->read_buf = alloc_buf();

if (!tty->read_buf)

return -ENOMEM;

}

memset(tty->read_buf, 0, N_TTY_BUF_SIZE);

reset_._flags(tty);

tty->column = 0;

n_tty_set_termios(tty, NULL);

tty->minimum_to_wake = 1;

tty->closing = 0;

return 0;

}

它为tty->read_buf分配内存.这个buffer空间大小为N_TTY_BUF_SIZE.read_buf实际上就是从按键的缓存区.然后调用reset_flags()来初始化tty中的一些字段:

static void reset_buffer_flags(struct tty_struct *tty)

{

unsigned long flags;

spin_lock_irqsave(&tty->read_lock, flags);

tty->read_head = tty->read_tail = tty->read_cnt = 0;

spin_unlock_irqrestore(&tty->read_lock, flags);

tty->canon_head = tty->canon_data = tty->erasing = 0;

memset(&tty->read_flags, 0, sizeof tty->read_flags);

n_tty_set_room(tty);

check_unthrottle(tty);

}

这里比较简,不再详细分析.在这里要注意几个tty成员的含义:

Tty->read_head, tty->read_tail , tty->read_cnt分别代表read_buf中数据的写入位置,读取位置和数据总数.read_buf是一个环形缓存区.

n_tty_set_room()是设备read_buf中的可用缓存区

check_unthrottle():是用来判断是否需要打开”阀门”,允许输入数据流入

对于console tty_driver对应的open函数如下示:

static int con_open(struct tty_struct *tty, struct file *filp)

{

unsigned int currcons = tty->index;

int ret = 0;

acquire_console_sem();

if (tty->driver_data == NULL) {

ret = vc_allocate(currcons);

if (ret == 0) {

struct vc_data *vc = vc_cons[currcons].d;

tty->driver_data = vc;

vc->vc_tty = tty;

if (!tty->winsize.ws_row && !tty->winsize.ws_col) {

tty->winsize.ws_row = vc_cons[currcons].d->vc_rows;

tty->winsize.ws_col = vc_cons[currcons].d->vc_cols;

}

release_console_sem();

vcs_make_sysfs(tty);

return ret;

}

}

release_console_sem();

return ret;

}

tty->index表示的是tty_driver所对示的设备节点序号.在这里也就是控制台的序列.用alt+fn就可以切换控制终端.

在这里,它主要为vc_cons[ ]数组中的对应项赋值.并将tty和vc建立关联.

四:控制台的read操作

从tty驱动架构中分析可得到,最终的read操作会转入到ldsic->read中进行.

相应tty_ldisc_N_TTY的read操作如下.这个函数代码较长,分段分析如下:

static ssize_t read_chan(struct tty_struct *tty, struct file *file,

unsigned char __user *buf, size_t nr)

{

unsigned char __user *b = buf;

DECLARE_WAITQUEUE(wait, current);

int c;

int minimum, time;

ssize_t retval = 0;

ssize_t size;

long timeout;

unsigned long flags;

do_it_again:

if (!tty->read_buf) {

printk(KERN_ERR "n_tty_read_chan: read_buf == NULL?!?\n");

return -EIO;

}

c = job_control(tty, file);

if (c < 0)

return c;

minimum = time = 0;

timeout = MAX_SCHEDULE_TIMEOUT;

if (!tty->icanon) {

time = (HZ / 10) * TIME_CHAR(tty);

minimum = MIN_CHAR(tty);

if (minimum) {

if (time)

tty->minimum_to_wake = 1;

else if (!waitqueue_active(&tty->read_wait) ||

(tty->minimum_to_wake > minimum))

tty->minimum_to_wake = minimum;

} else {

timeout = 0;

if (time) {

timeout = time;

time = 0;

}

tty->minimum_to_wake = minimum = 1;

}

}

首先,检查read操作的合法性,read_buf是否已经建立.然后再根据操作的类型来设置tty->

minimum_to_wake.这个成员的含义即为:

如果读进程在因数据不足而睡眠的情况下,数据到达并超过了minimum_to_wake.就将这个读进程唤醒.具体的唤醒过程我们在遇到的时候再进行分

析.

/*

*      Internal serialization of reads.

*/

//不允许阻塞

if (file->f_flags & O_NONBLOCK) {

if (!mutex_trylock(&tty->atomic_read_lock))

return -EAGAIN;

} else {

if (mutex_lock_interruptible(&tty->atomic_read_lock))

return -ERESTARTSYS;

}

add_wait_queue(&tty->read_wait, &wait);

在不允许睡眠的情况下,调用mutex_trylock()去获得锁.如果锁被占用,马上返回.否则用可中断的方式去获取锁,如果取锁错误,返回失败.如果取锁成功,将进程加至等待队列.在没有数据可读的情况下,直接睡眠.如果有数据可读,将其移出等待队列即可.

while (nr) {

/* First test for status change. */

if (tty->packet && tty->link->ctrl_status) {

unsigned char cs;

if (b != buf)

break;

cs = tty->link->ctrl_status;

tty->link->ctrl_status = 0;

if (tty_put_user(tty, cs, b++)) {

retval = -EFAULT;

b--;

break;

}

nr--;

break;

}

接下来就是一个漫长的while循环,用来读取数据,一直到数据取满为止.如果tty->packet被置为1.即为信包模式,通常用在

伪终端设备.如果tty->link->ctrl_status有数据.则说明如果链路状态发生改变,需要提交此信息.在这种情况下,将其直

接copy到用户空间即可.

/* This statement must be first before checking for input

so that any interrupt will set the state back to

TASK_RUNNING. */

set_current_state(TASK_INTERRUPTIBLE);

if (((minimum - (b - buf)) < tty->minimum_to_wake) &&

((minimum - (b - buf)) >= 1))

tty->minimum_to_wake = (minimum - (b - buf));

if (!input_available_p(tty, 0)) {

if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {

retval = -EIO;

break;

}

if (tty_hung_up_p(file))

break;

if (!timeout)

break;

if (file->f_flags & O_NONBLOCK) {

retval = -EAGAIN;

break;

}

if (signal_pending(current)) {

retval = -ERESTARTSYS;

break;

}

n_tty_set_room(tty);

timeout = schedule_timeout(timeout);

continue;

}

__set_current_state(TASK_RUNNING);

先将进程设为TASK_INTERRUPTIBLE状态.再调用input_available_p()来判断可数据供读取.如果没有.则进程睡眠.如果

有数据,则将进程状态设为TASK_RUNNING.在终端接收数据的处理过程中,有两种方式,一种是规范模式.一种是原始模式.在规范模式下,终端需要

对数据里面的一些特殊字符做处理.在原始模式下.终端不会对接收到的数据做任何的处理.在这里input_available_p()在判断是否有数据可

读也分两种情况进行,对于规范模式,看是否有已经转换好的数据,对于原始模式,判断接收的信息总数

/* Deal with packet mode. */

//packet模式`忽略

if (tty->packet && b == buf) {

if (tty_put_user(tty, TIOCPKT_DATA, b++)) {

retval = -EFAULT;

b--;

break;

}

nr--;

}

if (tty->icanon) {

/* N.B. avoid overrun if nr == 0 */

while (nr && tty->read_cnt) {

int eol;

eol = test_and_clear_bit(tty->read_tail,

tty->read_flags);

c = tty->read_buf[tty->read_tail];

spin_lock_irqsave(&tty->read_lock, flags);

tty->read_tail = ((tty->read_tail+1) &

(N_TTY_BUF_SIZE-1));

tty->read_cnt--;

if (eol) {

/* this test should be redundant:

* we shouldn't be reading data if

* canon_data is 0

*/

if (--tty->canon_data < 0)

tty->canon_data = 0;

}

spin_unlock_irqrestore(&tty->read_lock, flags);

//如果没有到结束字符,将字符copy到数据空间

//__DISABLED_CHAR是不需要copy到用户空间的

if (!eol || (c != __DISABLED_CHAR)) {

if (tty_put_user(tty, c, b++)) {

retval = -EFAULT;

b--;

break;

}

nr--;

}

if (eol) {

//如果遇到行结束符.就可以退出了

tty_audit_push(tty);

break;

}

}

if (retval)

break;

} else {

//非加工模式,直接copy

int uncopied;

//环形缓存,copy两次

uncopied = copy_from_read_buf(tty, &b, &nr);

uncopied += copy_from_read_buf(tty, &b, &nr);

if (uncopied) {

retval = -EFAULT;

break;

}

}

对于规范模式,要读满一行才会返回用户空间.例如我们在shell上输入指令的时候,要按下enter键指令才会进行处理.在

tty->read_flags数组中定义了一些满行的标志,如果read_buf中对应的数据在tty->read_flags中被置位.

就会认为这次读入已经到结尾了.在这里还要注意的是,不要将__DISABLED_CHAR即’/0’拷贝到用户空间.

对于原始模式,只需要将read_buf中的数据读入到用户空间就可以返回了.在这里需要注意read_buf是一个环形缓存,需要copy两次.例如tail在head之前的情况.

/* If there is enough space in the read buffer now, let the

* low-level driver know. We use n_tty_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.

*/

if (n_tty_chars_in_buffer(tty) <= TTY_THRESHOLD_UNTHROTTLE) {

n_tty_set_room(tty);

check_unthrottle(tty);

}

OK.到这里,read_buf中或多或少已经有数据被取出了.如果当前的数据量少于TTY_THRESHOLD_UNTHROTTLE.就可以调用check_unthrottle()将其它的写进程唤醒了

if (b - buf >= minimum)

break;

if (time)

timeout = time;

}

mutex_unlock(&tty->atomic_read_lock);

remove_wait_queue(&tty->read_wait, &wait);

if (!waitqueue_active(&tty->read_wait))

tty->minimum_to_wake = minimum;

__set_current_state(TASK_RUNNING);

已经读完了数据,是该到清理的时候了.将进程移出等待队列,并当进程状态设为TASK_RUNNING

size = b - buf;

if (size) {

retval = size;

if (nr)

clear_bit(TTY_PUSH, &tty->flags);

} else if (test_and_clear_bit(TTY_PUSH, &tty->flags))

goto do_it_again;

//更新剩余空间数

n_tty_set_room(tty);

return retval;

}

TTY_PUSH:是由底层驱动程序在读到一个EOF字符并将其放入缓存区造成的,表示用户要尽快将缓存区数据取走.

如果本次操作没有读取任何数据,且被设置了TTY_PUSH,则跳转到do_it_again,继续执行.如果本次操作读取了数据,可以等到下一次read的时候再来取.

最后,更新read_buf的剩余空间数.

五:控制终端数据的来源

从这个函数里面我们可以看到,数据是从read_buf中取出来的,但是谁将数据放入到read_buf中的呢?为了探究出它的根源.我们还得要从vty_init()说起.

在之前分析过. vty_init()会调用一个表面字义看起来与键盘相关的一个子函数: kbd_init().跟踪这个函数:

int __init kbd_init(void)

{

int i;

int error;

for (i = 0; i < MAX_NR_CONSOLES; i++) {

kbd_table[i].ledflagstate = KBD_DEFLEDS;

kbd_table[i].default_ledflagstate = KBD_DEFLEDS;

kbd_table[i].ledmode = LED_SHOW_FLAGS;

kbd_table[i].lockstate = KBD_DEFLOCK;

kbd_table[i].slockstate = 0;

kbd_table[i].modeflags = KBD_DEFMODE;

kbd_table[i].kbdmode = default_utf8 ? VC_UNICODE : VC_XLATE;

}

error = input_register_handler(&kbd_handler);

if (error)

return error;

tasklet_enable(&keyboard_tasklet);

tasklet_schedule(&keyboard_tasklet);

return 0;

}

暂时用不到的部份我们先不与分析。 在这里注册了一个input handler。结合前面我们分析的input子系统,在handler里会处理input device上报的事件。跟进这个handler看一下:

kbd_handler定义如下:

static struct input_handler kbd_handler = {

.event                 = kbd_event,

.connect   = kbd_connect,

.disconnect       = kbd_disconnect,

.start                   = kbd_start,

.name                = "kbd",

.id_table   = kbd_ids,

};

Id_table是用来匹配input device的。跟进去看一下,看哪些device的事件,才会交给它处理:

static const struct input_device_id kbd_ids[] = {

{

.flags = INPUT_DEVICE_ID_MATCH_EVBIT,

.evbit = { BIT_MASK(EV_KEY) },

},

{

.flags = INPUT_DEVICE_ID_MATCH_EVBIT,

.evbit = { BIT_MASK(EV_SND) },

},

{ },    /* Terminating entry */

};

从这个id_table中看来,只要是能支持EV_KEY或者是EV_SND的设备都会被这个hnadler匹配到。相应的。也就能够处理input device上报的事件了.

根据之前的input子系统分析,在input device和handler 进行匹配的时候会调用handler->connect.即kbd_connect().代码如下:

static int kbd_connect(struct input_handler *handler, struct input_dev *dev,

const struct input_device_id *id)

{

struct input_handle *handle;

int error;

int i;

for (i = KEY_RESERVED; i < BTN_MISC; i++)

if (test_bit(i, dev->keybit))

break;

if (i == BTN_MISC && !test_bit(EV_SND, dev->evbit))

return -ENODEV;

handle = kzalloc(sizeof(struct input_handle), GFP_KERNEL);

if (!handle)

return -ENOMEM;

handle->dev = dev;

handle->handler = handler;

handle->name = "kbd";

error = input_register_handle(handle);

if (error)

goto err_free_handle;

error = input_open_device(handle);

if (error)

goto err_unregister_handle;

return 0;

err_unregister_handle:

input_unregister_handle(handle);

err_free_handle:

kfree(handle);

return error;

}

在这段代码里,它申请分初始化了一个hande结构,并将其注册。Open。这些都是我们之前分析过的东东。在注册handle的时候。又会调用到hande->start.函数如下:

static void kbd_start(struct input_handle *handle)

{

unsigned char leds = ledstate;

tasklet_disable(&keyboard_tasklet);

if (leds != 0xff) {

input_inject_event(handle, EV_LED, LED_SCROLLL, !!(leds & 0x01));

input_inject_event(handle, EV_LED, LED_NUML,    !!(leds & 0x02));

input_inject_event(handle, EV_LED, LED_CAPSL,   !!(leds & 0x04));

input_inject_event(handle, EV_SYN, SYN_REPORT, 0);

}

tasklet_enable(&keyboard_tasklet);

}

这里就是对键盘上的LED进行操作。启用了tasklent。这些都不是我们所关心的重点。

来看下它的事件处理过程:

static void kbd_event(struct input_handle *handle, unsigned int event_type,

unsigned int event_code, int value)

{

if (event_type == EV_MSC && event_code == MSC_RAW && HW_RAW(handle->dev))

kbd_rawcode(value);

if (event_type == EV_KEY)

kbd_keycode(event_code, value, HW_RAW(handle->dev));

tasklet_schedule(&keyboard_tasklet);

do_poke_blanked_console = 1;

schedule_console_callback();

}

不管对应键盘的那一种模式。后面的数据流程都会转入到input_queue()进等处理。

实际上。控制终端由vc_cons[ ]数组表示。数组中的每一个项都表示一个控制终端。由全局变量fg_console来指示当前所用的cosole/另外。对于键盘等输出设备也对应一个数组。即kbd_table[ ].用来表示当前终端的控制信息.

其余的都不是我们想关心的。来跟踪一下这个函数的实现:

static void put_queue(struct vc_data *vc, int ch)

{

struct tty_struct *tty = vc->vc_tty;

if (tty) {

tty_insert_flip_char(tty, ch, 0);

con_schedule_flip(tty);

}

}

这里的参数vc就是指的在vc_cons[ ]中的当前项。回忆在console open的时候。初始化了这一项。并建立了VC和tty的关联。就这样。在vc中可以寻着关联关系找到tty了.

Tty_insert_filp_char( )将数据ch存入tty的一个缓存中,具体代码如下示:

static inline int tty_insert_flip_char(struct tty_struct *tty,

unsigned char ch, char flag)

{

struct tty_buffer *tb = tty->buf.tail;

if (tb && tb->used < tb->size) {

tb->flag_buf_ptr[tb->used] = flag;

tb->char_buf_ptr[tb->used++] = ch;

return 1;

}

return tty_insert_flip_string_flags(tty, &ch, &flag, 1);

}

在这里,将数存先存进了tty->buf中。后面的tty_insert_flip_string_flags是在当前buf不够的情况下,扩张buf使用的。代码比较简单,请自行分析。

将数据暂存之后,会调用con_schedule_flip(tty)去唤醒一个软中断的工作队列.代码如下:

static inline void con_schedule_flip(struct tty_struct *t)

{

unsigned long flags;

spin_lock_irqsave(&t->buf.lock, flags);

if (t->buf.tail != NULL)

t->buf.tail->commit = t->buf.tail->used;

spin_unlock_irqrestore(&t->buf.lock, flags);

schedule_delayed_work(&t->buf.work, 0);

}

对应的工作队列为t->buf.work.这个工作队列是怎么定义的呢?这就要回到我们之前分析的tty驱动的tty_struct的初始化.

代码片段如下所示:

static void initialize_tty_struct(struct tty_struct *tty)

{

。。。。。。

。。。。。。。

INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc);

。。。。。。

}

这就是这个工作队列的定义了.

在这里,特别提醒一下。在上面的put_queue()处理是处于一个中断环境。回想一想整个事件的流程。是键盘中断àinput device上报事件àhandler处理这个事件àput_queue()

在中断中,将工作队列唤醒。将比较繁重的工作交由这个工作队列处理。虽然工作队列也是工作在中断状态。但它是开中断执行的.这也就是软中断存在的目的.

跟进flush_to_ldisc():

static void flush_to_ldisc(struct work_struct *work)

{

struct tty_struct *tty =

container_of(work, struct tty_struct, buf.work.work);

unsigned long          flags;

struct tty_ldisc *disc;

struct tty_buffer *tbuf, *head;

char *char_buf;

unsigned char *flag_buf;

disc = tty_ldisc_ref(tty);

if (disc == NULL)       /*  !TTY_LDISC */

return;

工作队列所调用的参数是它本身所表示的work_queue.而它本身又是封装在tty_strcut里面的。调用container_of()宏就可以获取到封装它的tty_struct.然后增加tty->ldisc的引用计数

spin_lock_irqsave(&tty->buf.lock, flags);

/* So we know a flush is running */

set_bit(TTY_FLUSHING, &tty->flags);

head = tty->buf.head;

if (head != NULL) {

tty->buf.head = NULL;

for (;;) {

int count = head->commit - head->read;

if (!count) {

if (head->next == NULL)

break;

tbuf = head;

head = head->next;

tty_buffer_free(tty, tbuf);

continue;

}

/* Ldisc or user is trying to flush the buffers

we are feeding to the ldisc, stop feeding the

line discipline as we want to empty the queue */

if (test_bit(TTY_FLUSHPENDING, &tty->flags))

break;

if (!tty->receive_room) {

schedule_delayed_work(&tty->buf.work, 1);

break;

}

if (count > tty->receive_room)

count = tty->receive_room;

char_buf = head->char_buf_ptr + head->read;

flag_buf = head->flag_buf_ptr + head->read;

head->read += count;

spin_unlock_irqrestore(&tty->buf.lock, flags);

disc->receive_buf(tty, char_buf, flag_buf, count);

spin_lock_irqsave(&tty->buf.lock, flags);

}

/* Restore the queue head */

tty->buf.head = head;

}

对于tty->buf中的每个缓存区,如果缓存区中没有数据,则将其释放,这个释放是有优化的。如果数据少于512就将其放到

tty->buf->free中。下次要放分存放空间的时候可以直接到这里面取。如果设置了TTY_

FLUSHPENDING就会跳出循环。

如果tty的接收缓存区不够,则跳出循环,定时器到达过后再来调用这个工作队列.

最后调用tty->receive_buf()来处理这个数据了.

/* We may have a deferred request to flush the input buffer,

if so pull the chain under the lock and empty the queue */

if (test_bit(TTY_FLUSHPENDING, &tty->flags)) {

__tty_buffer_flush(tty);

clear_bit(TTY_FLUSHPENDING, &tty->flags);

wake_up(&tty->read_wait);

}

clear_bit(TTY_FLUSHING, &tty->flags);

spin_unlock_irqrestore(&tty->buf.lock, flags);

tty_ldisc_deref(disc);

}

数据最终会通过tty-> receive_buf()将数据放入read_buf.

在这段代码中,有几个很有意思的处理。在进入工作队列的时候,首先会置TTY_FLUSHING标志.如果有进程在读read_buf的时候,

如果此标志被置位,就会设置TTY_FLUSHPENDING标志,并进行睡眠。在数据处理完成之后,判断是否有TTY_FLUSHPENDING标志。

如果有,则将读进程唤醒.并清除TTY_FLUSHPENDING和TTY_FLUSHING

想一想。为什么会这么处理呢?为什么这里需要两个缓存区,一个buf.一个read_buf。为什么要这样麻烦呢?

首先,对于缓存区的数目问题:我们在后面会看到。对接收数据还有一系列的预处理过程,这些过程是比较费时的。不宜在中断中进行费时的操作。所以

需要选用软中断机制。这就需要将数据先放置一个buf.再由软中断进行预处理之后,再将它放入到read_buf.这就是两个缓存区的原因.

另外:在存数据到read_buf的时候。会有进程从read_buf中读数据。这样就会造成一个竞争。注意到在软中断情况下是不可睡眠的。我

们只能选用自旋锁一类的机制。而这种机制是禁止中断和抢占的。这又违背了软中断机制的初衷。怎么办呢?这就是这样标志的作用了。在设计中,我们必须首先得

要保证软中断处理机制的快速完成。所以一进入软中断,就置了一个标志。如果有进程来读数据了,也就是说竞争条件发生了,先将读进程置睡眠。不管怎样,先让

软中断处理完之后再说。软中断的工作over这后,再唤醒读进程。

我们之前讲的一系统加锁机制是在两者同样平等的情况。而原子置位与判断置位一般是为了保证一方的工作先完成。

好了,到这一步,我们终于看到跟踪read_buf中数据来源问题的一丝曙光了。数据经过tty->receive_buf之后,这个过程就清晰明朗了。

对于tty_ldisc_N_TTY. receive_buf接口如下所示:

static void n_tty_receive_buf(struct tty_struct *tty, const unsigned char *cp,

char *fp, int count)

{

const unsigned char *p;

char *f, flags = TTY_NORMAL;

int     i;

char buf[64];

unsigned long cpuflags;

if (!tty->read_buf)

return;

if (tty->real_raw) {

spin_lock_irqsave(&tty->read_lock, cpuflags);

i = min(N_TTY_BUF_SIZE - tty->read_cnt,

N_TTY_BUF_SIZE - tty->read_head);

i = min(count, i);

memcpy(tty->read_buf + tty->read_head, cp, i);

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

cp += i;

count -= i;

i = min(N_TTY_BUF_SIZE - tty->read_cnt,

N_TTY_BUF_SIZE - tty->read_head);

i = min(count, i);

memcpy(tty->read_buf + tty->read_head, cp, i);

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

spin_unlock_irqrestore(&tty->read_lock, cpuflags);

} else {

for (i = count, p = cp, f = fp; i; i--, p++) {

if (f)

flags = *f++;

switch (flags) {

case TTY_NORMAL:

n_tty_receive_char(tty, *p);

break;

case TTY_BREAK:

n_tty_receive_break(tty);

break;

case TTY_PARITY:

case TTY_FRAME:

n_tty_receive_parity_error(tty, *p);

break;

case TTY_OVERRUN:

n_tty_receive_overrun(tty);

break;

default:

printk(KERN_ERR "%s: unknown flag %d\n",

tty_name(tty, buf), flags);

break;

}

}

if (tty->driver->flush_chars)

tty->driver->flush_chars(tty);

}

对于原始模式。直接将数据copy到read_buf中。对于加工模式,将数据预处理之后,再加入到read_buf中。这个预处理过程比较繁杂,这里先忽略.

n_tty_set_room(tty);

if (!tty->icanon && (tty->read_cnt >= tty->minimum_to_wake)) {

kill_fasync(&tty->fasync, SIGIO, POLL_IN);

if (waitqueue_active(&tty->read_wait))

wake_up_interruptible(&tty->read_wait);

}

/*

* Check the remaining room for the input canonicalization

* mode.  We don't want to throttle the driver if we're in

* canonical mode and don't have a newline yet!

*/

if (tty->receive_room < TTY_THRESHOLD_THROTTLE) {

/* check TTY_THROTTLED first so it indicates our state */

if (!test_and_set_bit(TTY_THROTTLED, &tty->flags) &&

tty->driver->throttle)

tty->driver->throttle(tty);

}

}

重新计数read_buf的剩余空间量。如果可读数据大于tty->minimum_to_wake.就将它的读进程唤醒。

如果当前read_buf剩余空间不足TTY_THRESHOLD_THROTTLE.就调用tty->driver->throttle(tty)将数程流入进程先阻塞.

六:控制终端的write操作

在输入shell指令的时候,屏幕上会出现我们键入的字符。在输入密码的时候,屏幕上一般不会显示我们当前按入了什么键。就就是终端的两种模式,回显和非回显(ECHO)。当设置为回显模式的时候,会将键入的值在屏幕上面显示出来。这个显示的过程就是通过tty driver->write来实现的。

屏幕上的显示操作跟显示驱动有很重要的联系。一般就是调用显卡驱动的显示接口来实现。在切换终端的时候。设置显示区域。由于这部份跟显卡驱动关联较深,而功能又比较单一。在这里不做详细分析。

七:总结

在这一节里,将之前分析过的input子系统,tty驱动架构联系在了一起。我们渐渐体会到,Linux中大量的使用分层架构。层与层之前的联系很紧密而维护也很简单。深入体会其中的架构思想。对于我们平时做开发是很有裨益的.

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_33139275/article/details/116813153

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签