tty驱动详解

来源:互联网 发布:多媒体网络触控一体机 编辑:程序博客网 时间:2024/05/16 10:37

/dev/tty代表当前tty设备,在当前的终端中输入 echo hello” > /dev/tty ,都会直接显示在当前的终端中。/dev/ttyS*是串行终端设备。

/dev/pty*即伪终端,所谓伪终端是逻辑上的终端设备,多用于模拟终端程序。例如,我们在X Window下打开的终端,以及我们在Windows使用telnet  ssh等方式登录Linux主机,此时均在使用pty设备(准确的说应该pty从设备)

/dev/tty0代表当前虚拟控制台,而/dev/tty1等代表第一个虚拟控制台,例如当使用ALT+F2进行切换时,系统的虚拟控制台为/dev/tty2 ,当前的控制台则指向/dev/tty2

使用tty命令可以确定当前的终端或者控制台。

2.1.71之前,/dev/console根据不同系统的设定可以链接到/dev/tty0或者其他tty*上,在2.1.71版本之后则完全由内核控制。目前,只有在单用户模式下可以登录/dev/console(可以在单用户模式下输入tty命令进行确认)。

只有24102.6才叫ttySAC09200等的还是叫ttyS0 




在驱动起来的时候调用了uart_register_driver(&serial8250_reg);函数将参数serial8250_reg注册进了tty层。

int uart_register_driver(struct uart_driver *drv)  

{  

    struct tty_driver *normal = NULL;  

    int i, retval;  

  

    drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL); 

    normal  = alloc_tty_driver(drv->nr);  

    drv->tty_driver = normal;  

normal->driver_state    = drv;  

tty_set_operations(normal, &uart_ops);  

  

    for (i = 0; i < drv->nr; i++) {  

        struct uart_state *state = drv->state + i;  

        state->close_delay     = 500;    /* .5 seconds */  

        state->closing_wait    = 30000;  /* 30 seconds */  

        mutex_init(&state->mutex);  

  

        tty_port_init(&state->info.port);  

        init_waitqueue_head(&state->info.delta_msr_wait);  

        tasklet_init(&state->info.tlet, uart_tasklet_action,  

                 (unsigned long)state);  

    }  

    retval = tty_register_driver(normal);  

}  

uart_driver中很多数据结构其实就是tty_driver中的,将数据转换为tty_driver之后,注册tty_driver。然后初始化uart_driver->state的存储空间。

normal->driver_state    = drv; 这一句将参数的ops关系都赋给了serial_core层。也就是说在后面serial_core会根据uart_ops关系找到我们的8250.c中所对应的操作,而我们参数中的ops是在哪被赋值的呢?定位到了8250.c中的serial8250_ops结构体,初始化如下:

static struct uart_ops serial8250_pops = {  

    .tx_empty   = serial8250_tx_empty,  

    .set_mctrl  = serial8250_set_mctrl,  

    .get_mctrl  = serial8250_get_mctrl,  

    .stop_tx    = serial8250_stop_tx,  

    .start_tx   = serial8250_start_tx,  

    .stop_rx    = serial8250_stop_rx,  

    .enable_ms  = serial8250_enable_ms,  

    .break_ctl  = serial8250_break_ctl,  

    .set_termios    = serial8250_set_termios,  

    .pm     = serial8250_pm,  

    .request_port   = serial8250_request_port,  

    .config_port    = serial8250_config_port,  

};  
uart_add_one_port()函数中,从serial8250_init->serial8250_register_ports()->uart_add_

one_port()逐步调用过来的,这一步就将portuart_driver联系起来了。

tty_set_operations(normal, &uart_ops);  tty_driver的操作集统一设为了uart_ops.这样就使得从用户空间下来的操作可以找到正确的serial_core的操作函数,uart_ops是在serial_core.c中的:

static const struct tty_operations uart_ops = {  

    .open       = uart_open,   

    .write      = uart_write,  

    .put_char   = uart_put_char,  

    .flush_chars    = uart_flush_chars,  

    .write_room = uart_write_room,  

    .flush_buffer   = uart_flush_buffer,  

    .ioctl      = uart_ioctl,  

    .send_xchar = uart_send_xchar,  

    .set_termios    = uart_set_termios,  

    .set_ldisc  = uart_set_ldisc,  

    .start      = uart_start,  

    .break_ctl  = uart_break_ctl,  

    .wait_until_sent= uart_wait_until_sent,  

    .tiocmget   = uart_tiocmget,  

    .tiocmset   = uart_tiocmset,  

};  

这样就保证了调用关系的通畅。

自顶向下

tty_core是所有tty类型的驱动的顶层构架,向用户应用层提供了统一的接口,应用层的read/write等调用首先会到达这里。代码主要分布在drivers/char目录下的n_tty.ctty_io.c等文件中

static const struct file_operations tty_fops = {  

    .llseek        = no_llseek,  

    .read        = tty_read,  

    .write        = tty_write,  

    .poll        = tty_poll,  

    .unlocked_ioctl    = tty_ioctl,  

    .compat_ioctl    = tty_compat_ioctl,  

    .open        = tty_open,  

    .release    = tty_release,  

    .fasync        = tty_fasync,  

};  

此层调用函数的结构体,在uart_register_driver()函数中我们调用了每个tty类型的驱动注册时都会调用的tty_register_driver函数:

int tty_register_driver(struct tty_driver * driver)  

{  

    cdev_init(&driver->cdev, &tty_fops);  //将指针调用关系赋给了cdev

}  

Readwrite操作就会交给line discipline处理

static ssize_t tty_read(struct file *file, char __user *buf, size_t count,  

            loff_t *ppos)  

{  

    ld = tty_ldisc_ref_wait(tty);  

    if (ld->ops->read)  

        i = (ld->ops->read)(tty, file, buf, count);  //调用到了ldisc层(线路规程)的read函数   

    tty_ldisc_deref(ld);  

}  

static ssize_t tty_write(struct file *file, const char __user *buf,  

                        size_t count, loff_t *ppos)  

{  

    ld = tty_ldisc_ref_wait(tty); 

    ret = do_tty_write(ld->ops->write, tty, file, buf, count);  

    tty_ldisc_deref(ld);  

    return ret;  

}  

static inline ssize_t do_tty_write(ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t), struct tty_struct *tty, struct file *file, const char __user *buf, size_t count)  

{  

    for (;;) {  

        copy_from_user(tty->write_buf, buf, size)

        ret = write(tty, file, tty->write_buf, size);//调用到了ldisc层的write函数   

}

}  

drivers/char/n_tty.c文件中,下面的代码是线路规程中的write函数:

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

               const unsigned char *buf, size_t nr)  

{  

    add_wait_queue(&tty->write_wait, &wait);//将当前进程放到等待队列中   

    while (1) {    

        if (O_OPOST(tty) && !(test_bit(TTY_HW_COOK_OUT, &tty->flags))) {  

            while (nr > 0) {  

                ssize_t num = process_output_block(tty, b, nr);  

                b += num;  

                nr -= num;  

                c = *b;  

            if (tty->ops->flush_chars)  

                tty->ops->flush_chars(tty);  

        } else {  

            while (nr > 0) {  

                c = tty->ops->write(tty, b, nr);  //调用到具体的驱动中的write函数   

                b += c;  

                nr -= c;  

            }  

        }  

        /*  假如是以非阻塞的方式打开的,那么也直接返回。否则,让出cpu,等条件满足以后再继续执行。*/          

        schedule();

    }  

}  

c = tty->ops->write(tty, b, nr)是调用了serial_corewrite()函数,在tty_core进行open的时候悄悄把tty->ops指针给赋值了。具体的代码就在driver/char/tty_io.c中,调用关系如下所示:

tty_open -> tty_init_dev -> initialize_tty_structinitialize_tty_struct()函数的代码在下面:

void initialize_tty_struct(struct tty_struct *tty,  

        struct tty_driver *driver, int idx)  

{  

    tty->ops = driver->ops;  

}  

这里就将serial_core层的操作调用关系指针值付给了tty_core层,这样tty->ops->write()其实调用到了具体的驱动的write函数

open操作调用到serial_core层的时候有下面的代码:

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

{  

    struct uart_driver *drv = (struct uart_driver *)tty->driver->driver_state; //uart_register_driver()的时候所做的一些事情,那时我们是放进去,现在是拿出来而已。

    struct uart_state *state;  

    int retval, line = tty->index;  

    ……  

        uart_update_termios(state);  

    }  

}  





tty_driver也就是serial_core交互


tty设备文件的操作中。Open操作会进行一系统初始化。然后调用ldsic->open tty_driver->open。在writeread调用中只tty_core只会用到 ldisc->wirte/ldisc->readioctl操作,以及它封装的几个 termios。这些ioctl类的操作会直接和tty_driver相关联.

tty console这些概念主要是一些虚设备的概念,而串口更多的是指一个真正的设备驱动Tty实际是一类终端I/O设备的抽象,它实际上更多的是一个管理的概念,它和tty_ldisc(行规程)和tty_driver(真实设备驱动)组合在一起每个描述tty设备的 tty_struct在初始化时必然挂了某个具体芯片的字符设备驱动,可以是很多,包括显卡或串口chip,这些驱动实际上都有而console是一个缓冲的概念,它的目的有一点类似于tty实际上 console不仅和tty连在一起,还和framebuffer连在一起Tty的一个子集需要使用 console(典型的如主设备号4,次设备号164),但是要注意的是没有consoletty是存在的而串口则指的是tty_driver 

printk不一定是将信息往控制台上输出,设置kernel的启动参数可能可以打到将信息送到显示器的效果。


Linux内核中 tty的层次结构图所示,包含tty核心、tty线路规程和tty驱动。tty核心是对整个tty设备的抽象,对用户提供统一的接口,tty线路规程是对传输数据的格式化,tty驱动则是面向tty设备的驱动,这是对设备的驱动,应该要我们来实现的

tty设备发送数据的流程为:tty核心从一个用户获取将要发送给一个 tty设备的数据,tty核心将数据传递给tty线路规程驱动,接着数据被传递到tty驱动,tty驱动将数据转换为可以发送给硬件的格式。

接收数据的流程为: 从tty硬件接收到的数据向上交给tty驱动,进入tty线路规程驱动,再进入 tty 核心,在这里它被一个用户获取。尽管大多数时候tty核心和tty之间的数据传输会经历tty线路规程的转换,但是tty驱动与tty核心之间也可以直接传输数据。

三、tty设备驱动结构

tty_io.c定义了tty 设备通用的file_operations结构体并实现了接口函数tty_io.c也提供了tty_register_ldisc()接口函数用于注册线路规程,n_tty.c文件则实现了tty_disc结构体中的成员。特定tty设备驱动的主体工作是填充tty_driver结构体中的成员,实现其中的成员函数。

四、串口驱动核心层

tty设备驱动到串口驱动中间经过了一层serial_core ,从tty设备驱动中需要填充的是tty_driver结构,经过串口核心层后就转变成了实现xxx_uart.c 。到现在linux系统已经封装了终端设备(tty)的驱动,而我们只需要实现串口驱动就能实现整个串口终端驱动。

五、主要结构与关系

对于tty驱动层主要有几个重要的结构

serial_core实现了UART设备的通用TTY驱动层(称为串口核心层),这样UART驱动的主要任务演变成了实现serial_core中定义的一组uart_xxx接口而非tty_xxx接口

----------------设备方法-----------------------------设备注册------------------------设备信息

 ------------tty_operations------------------------tty_driver---------------------------tty-struct          // tty核心层定义,serial_core(串口核心层实现的结构体)

--------------uart_ops-----------------------------uart_driver---------------------------uart_port     // 串口核心层定义,需要我们实现

----------s3c24xx_serial-ops-----------------s3c24xx_uart_drv-----------------s3c24xx_uart_port

s3c24xx_uart_port主要包含一下几个结构

uart_port

s3c24xx_uart_info        ——>主要是UART中的FIFO信息,需要修改

s3c34xx_uart_clksrc     ——>UART时钟、频率 ,需要修改

另外在加上一个结构体 s3c2410_uartcfg ——>硬件端口、寄存器 ,这个需要修改



用户空间主要是通过设备文件同tty_core交互.tty_core根据用空间操作的类型再选择跟line disciplinetty_driver交互.设置硬件的ioctl指令就直接交给tty_driver处理。Readwrite操作就会交给 line discipline处理.

Line discipline是线路规程的意思,它表示的是这条终端”线程”的输入与输出规范设置.主要用来进行输入/输出数据的预处理。处理之后就会将数据交给tty_driver.

没有为tty_driver提供read操作。也就是说tty_core line discipline都没有办法从tty_driver里直接读终端信息。在line discipline中有一个输入缓存区。并提供了一个名叫receive_buf()的接口函数。只要调用line discipinereceiver_buf函数,将数据写入到输入缓存区就可以了。

三:tty驱动接口分析

tty driver的所有操作都包含在tty_driver中。alloc_tty_driver()来分配这个tty_driver

tty_register_driver() 将其注册.

alloc_tty_driver()接口代码如下所示:

struct tty_driver *alloc_tty_driver(int lines) 

      struct tty_driver *driver; 

      driver = kzalloc(sizeof(struct tty_driver), GFP_KERNEL); 

      if (driver) { 

               driver->magic = TTY_DRIVER_MAGIC; 

               driver->num = lines; 

      } 

      return driver; 

line的个数即次设备号的个数。注意每个设备文件都会对应一个line.

tty_register_driver()用来注册一个tty_driver。代码如下:

int tty_register_driver(struct tty_driver *driver) 

      dev_t dev; 

      void **p =  kzalloc(driver->num * 3 * sizeof(void *), GFP_KERNEL); 

      //注册字符设备号 

    dev = MKDEV(driver->major, driver->minor_start); 

    error = register_chrdev_region(dev, driver->num, driver->name); 

    if (p) { 

               driver->ttys = (struct tty_struct **)p; 

               driver->termios = (struct ktermios **)(p + driver->num); 

               driver->termios_locked = (struct ktermios **) 

                                                                (p + driver->num * 2); 

      } 

      //注册字符设备 

      cdev_init(&driver->cdev, &tty_fops); 

      driver->cdev.owner = driver->owner; 

      error = cdev_add(&driver->cdev, dev, driver->num); 

      //指定默认的put_char 

      if (!driver->put_char) 

               driver->put_char = tty_default_put_char; 

      list_add(&driver->tty_drivers, &tty_drivers); //tty_driver 挂载到tty_drivers链表中.以设备号为关键字找到对应的driver.

      //如果没有指定TTY_DRIVER_DYNAMIC_DEV.即动态设备管理 

      proc_tty_register_driver(driver); 

      return 0; 

}  

四:设备文件的操作

设备文件的操作是本节分析的重点。它的主要操作是将各项操作对应到ldsic或者是tty_driver.

41:打开tty设备的操作

从注册的过程可以看到,所有的操作都会对应到tty_fops中。Open操作对应的操作接口是tty_open()。代码如下:

static int tty_open(struct inode *inode, struct file *filp) 

      struct tty_struct *tty; 

      int noctty, retval; 

      struct tty_driver *driver; 

      int index; 

      dev_t device = inode->i_rdev; 

      unsigned short saved_flags = filp->f_flags; 

      nonseekable_open(inode, filp); 

retry_open: 

      //O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端 

      //noctty:需不需要更改当前进程的控制终端 

      noctty = filp->f_flags & O_NOCTTY; 

      //设备号(5,0) /dev/tty.表示当前进程的控制终端 

      if (device == MKDEV(TTYAUX_MAJOR, 0)) { 

               tty = get_current_tty(); 

               //取得当前进程的tty_driver 

               driver = tty->driver; 

               index = tty->index; 

               filp->f_flags |= O_NONBLOCK; /* Don't let /dev/tty block */ 

               goto got_driver; 

      } 

#ifdef CONFIG_VT 

      //设备号(4,0)./dev/tty0:表示当前的控制台 

      if (device == MKDEV(TTY_MAJOR, 0)) { 

               extern struct tty_driver *console_driver; 

               driver = console_driver; 

               //fg_console: 表示当前的控制台 

               index = fg_console; 

               noctty = 1; 

               goto got_driver; 

      } 

#endif 

      //设备号(5,1)./dev/console.表示外接的控制台通过regesit_console() 

      if (device == MKDEV(TTYAUX_MAJOR, 1)) { 

               driver = console_device(&index); 

               if (driver) {

                        /* Don't let /dev/console block */ 

                        filp->f_flags |= O_NONBLOCK; 

                        noctty = 1; 

                        goto got_driver; 

               } 

      } 

      //以文件的设备号为关键字,tty_drivers中搜索所注册的driver 

      driver = get_tty_driver(device, &index); 

got_driver: 

      //index表示它的次设备号 

      retval = init_dev(driver, index, &tty); 

      filp->private_data = tty; 

      file_move(filp, &tty->tty_files); 

      check_tty_count(tty, "tty_open"); 

      if (tty->driver->type == TTY_DRIVER_TYPE_PTY && 

         tty->driver->subtype == PTY_TYPE_MASTER) 

               noctty = 1; 

      if (!retval) { 

               if (tty->driver->open) 

                        retval = tty->driver->open(tty, filp); 

      } 

      filp->f_flags = saved_flags; 

}  

注意在这里有个容易忽略的操作:init_dev()。

Init_dev() ----> initialize_tty_struct() -----> tty_ldisc_assign(tty, tty_ldisc_get(N_TTY));

看一下tty_ldisc_assign(tty, tty_ldisc_get(N_TTY))的操作:

Tty_ldisc_get()tty_ldiscs[ ]找到对应项.这个数组中的成员是调用tty_register_ldisc()将其设置进去的.

tty_ldisc_assign操作如下:

static void tty_ldisc_assign(struct tty_struct *tty, struct tty_ldisc *ld) 

      tty->ldisc = *ld; 

      tty->ldisc.refcount = 0; 

}

即将取出来的idisc作为tty->ldisc字段.

在这段代码中涉及到了tty_driver,tty_struct, struct tty_ldisc.



tty_struct的ldisc是默认指定为tty_ldiscs[N_TTY].该ldisc对应的是控制终端的线路规范。可以在用空间用带TIOCSETD的ioctl调用进行更改.

4.2:设备文件的write操作

设备文件的write操作对应tty_fops->writetty_write().代码如下:

static ssize_t tty_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) 

      struct tty_struct *tty; 

      struct inode *inode = file->f_path.dentry->d_inode; 

      ssize_t ret; 

      struct tty_ldisc *ld; 

      tty = (struct tty_struct *)file->private_data; 

      ld = tty_ldisc_ref_wait(tty); 

      ret = do_tty_write(ld->write, tty, file, buf, count); 

      tty_ldisc_deref(ld); 

open的过程中,将tty_struct存放在file的私有区。在write,file的私有区中就可以取到要操作的tty_struct.如果一切正常,递增ldsic的引用计数。用do_tty_wirte()再行写操作。写完之后,再递减ldsic的引用计数.

Do_tty_write代码分段分析如下:

static inline ssize_t do_tty_write( ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t), 

struct tty_struct *tty, struct file *file, const char __user *buf, size_t count) 

      ssize_t ret, written = 0; 

      unsigned int chunk; 

      ret = tty_write_lock(tty, file->f_flags & O_NDELAY); 

      chunk = 2048; 

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

               chunk = 65536; 

      if (count)

               chunk = count; 

  tty->write_cnt = chunk; 

      tty->write_buf = buf; 

}  

默认一次写数据的大小为2K.如果设置了TTY_NO_WRITE_SPLIT.则将一次写的数据量扩大为65536.Tty->write_buf是写操作的临时缓存区。即将用户空的数据暂时存放到这里

Tty->write_cnt是临时缓存区的大小。必须要根据一次写的数据量对这个临时缓存区做调整

      /* Do the write .. */ 

      for (;;) { 

               size_t size = count; 

               if (size > chunk) 

                        size = chunk; 

               copy_from_user(tty->write_buf, buf, size);

               ret = write(tty, file, tty->write_buf, size); 

               written += ret; 

               buf += ret; 

               count -= ret; 

               if (signal_pending(current)) 

                        break; 

               cond_resched(); 

      } 

      if (written) { 

               struct inode *inode = file->f_path.dentry->d_inode; 

               inode->i_mtime = current_fs_time(inode->i_sb); 

               ret = written; 

      } 

}  

将用户空间的数据copy到临时缓存区,然后再调用ldisc->write()完成这次写操作.最后再更新设备结点的时间戳ldisc中对写入的数据做预处理过后,还是会调用tty_driver->write()将其写入硬件.

4.3:设备文件的read操作

static ssize_t tty_read(struct file *file, char __user *buf, size_t count, 

                        loff_t *ppos) 

      int i; 

      struct tty_struct *tty; 

      struct inode *inode; 

      struct tty_ldisc *ld; 

      tty = (struct tty_struct *)file->private_data; 

      inode = file->f_path.dentry->d_inode; 

      i = (ld->read)(tty, file, buf, count); 

}  

直接调用ldsic->read()完成工作