大家好,我是本期栏目编辑小友,现在为大家讲解控制台驱动是linux重要的设备驱动之一问题。
一:前言
我们在之前分析过投入子系统和电传打字机设备驱动架构。今天需要将两者结合起来。看看Linux操作系统操作系统中的控制台是怎么样实现的。
二:控制台驱动的初始化
之前在分析电传打字机驱动架构的时候曾分析到。主设备为4,次设备为0的设备节点,即/dev/tty0为当前的控制终端。
有tty_init()中,有以下代码段:
静态int __init tty_init(无效)
{
……
……
#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)
死机('无法注册/dev/tty0驱动程序/n’);
device_create(tty_class,NULL,MKDEV(TTY_MAJOR,0),' tty 0 ');
vty _ init();
#endif
返回0;
}
CONFIG_VT:是指配置虚拟终端。即我们所说的控制台。在此可以看到TTY _梅杰(4),0对应的设备节点操作集为控制台_fops .
继续跟进vty_init()
int __init vty_init(无效)
{
VCS _ init();
console _ driver=alloc _ tty _ driver(MAX _ NR _ CONSOLES);
if(!控制台_驱动程序)
死机("无法分配控制台驱动程序/n ");
控制台_驱动程序-所有者=THIS _ MODULE
console _ driver-name=' tty ';
console _ driver-name _ base=1;
控制台_驾驶员-少校=TTY _少校;
console _ driver-minor _ start=1;
控制台_驱动程序-类型=TTY _驱动程序_类型_控制台;
console _ driver-init _ term IOs=tty _ STD _ term IOs;
dent: 2em;"> 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)) 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;
}
}
标签:
版权声明:转载此文是出于传递更多信息之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系,我们将及时更正、删除,谢谢您的支持与理解。