天天看點

Linux裝置模型之tty驅動架構分析

------------------------------------------

本文系本站原創,歡迎轉載!

轉載請注明出處:http://ericxiao.cublog.cn/

一:前言

Tty這個名稱源于電傳打位元組的簡稱。在linux表示各種終端。終端通常都跟硬體相對應。比如對應于輸入裝置鍵盤滑鼠。輸出裝置顯示器的控制終端和序列槽終端.也有對應于不存在裝置的pty驅動。在如此衆多的終端模型之中,linux是怎麼将它們統一模組化的呢?這就是我們今天要讨論的問題.

二:tty驅動概貌

Tty架構如下所示:

如上圖所示,使用者空間主要是通過裝置檔案同tty_core互動.tty_core根據用空間操作的類型再選擇跟line discipline和tty_driver互動.例如設定硬體的ioctl指令就直接交給tty_driver處理。Read和write操作就會交給line discipline處理.

Line discipline是線路規程的意思。正如它的名字一樣,它表示的是這條終端”線程”的輸入與輸出規範設定.主要用來進行輸入/輸出資料的預處理。處理之後。就會将資料交給tty_driver

Tty_driver就是終端對應的驅動了。它将字元轉換成終端可以了解的字串.将其傳給終端裝置。

值得注意的是,這個架構沒有為tty_drivero提供read操作。也就是說tty_core 和line discipline都沒有辦法從tty_driver裡直接讀終端資訊。這是因為tty_driver對就的hardware并不一定是輸入資料和輸出資料的共同負載者。例如控制終端,輸出裝置是顯示器。輸入裝置是鍵盤。基于這樣的原理。在line discipline中有一個輸入緩存區。并提供了一個名叫receive_buf()的接口函數。對應的終端裝置隻要調用line discipine的receiver_buf函數,将資料寫入到輸入緩存區就可以了。

如果一個裝置同時是輸入裝置又是輸出裝置。那在裝置的中斷進行中調用receive_buf()将資料寫入即可.

三:tty驅動接口分析

具體的tty驅動設計可以參考LDD3。這裡隻對它的接口實作做一個分析.tty driver的所有操作都包含在tty_driver中。核心即供了一個名叫alloc_tty_driver()來配置設定這個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;

                   /* later we'll move allocation of tables here */

         }

         return driver;

}

這個函數隻有一個參數。這個參數的含義為line的個數。也即次裝置号的個數。注意每個裝置檔案都會對應一個line.

在這個接口裡為tty_driver配置設定記憶體,然後将driver->mage.driver->num初始化之後就傳回了.

tty_register_driver()用來注冊一個tty_driver。代碼如下:

int tty_register_driver(struct tty_driver *driver)

         int error;

         int i;

         dev_t dev;

         void **p = NULL;

         //TTY_DRIVER_INSTALLED:已安裝的

         if (driver->flags & TTY_DRIVER_INSTALLED)

                   return 0;

         //TTY_DRIVER_DEVPTS_MEM:使用devpts進行動态記憶體映射

         if (!(driver->flags & TTY_DRIVER_DEVPTS_MEM) && driver->num) {

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

                   if (!p)

                            return -ENOMEM;

         //注冊字元裝置号

         //如果沒有指定driver->major

         if (!driver->major) {

                   error = alloc_chrdev_region(&dev, driver->minor_start,

                                                        driver->num, driver->name);

                   if (!error) {

                            driver->major = MAJOR(dev);

                            driver->minor_start = MINOR(dev);

                   }

         } else {

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

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

         if (error

                   kfree(p);

                  return error;

         if (p) {

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

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

                   driver->termios_locked = (struct ktermios **)

                                                                 (p + driver->num * 2);

                   driver->ttys = NULL;

                   driver->termios = NULL;

                   driver->termios_locked = NULL;

         //注冊字元裝置

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

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

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

         if (error) {

                   unregister_chrdev_region(dev, driver->num);

                   driver->termios = driver->termios_locked = NULL;

                   return error;

         //指定預設的put_char

         if (!driver->put_char)

                   driver->put_char = tty_default_put_char;

         mutex_lock(&tty_mutex);

         list_add(&driver->tty_drivers, &tty_drivers);

         mutex_unlock(&tty_mutex);

         //如果沒有指定TTY_DRIVER_DYNAMIC_DEV.即動态裝置管理

         if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {

                   for (i = 0; i num; i++)

                       tty_register_device(driver, i, NULL);

         proc_tty_register_driver(driver);

         return 0;

這個函數操作比較簡單。就是為tty_driver建立字元裝置。然後将字元裝置的操作集指定為tty_fops.并且将tty_driver挂載到tty_drivers連結清單中.其實這個連結清單的作用跟我們之前分析的input子系統中的input_dev[ ]數組類似。都是以裝置号為關鍵字找到對應的driver.

特别的。如果沒有定義TTY_DRIVER_DYNAMIC_DEV.還會在sysfs中建立一個類裝置.這樣主要是為了udev管理裝置.

以流程圖的方式将上述操作表示如下:

四:裝置檔案的操作

裝置檔案的操作是本節分析的重點。它的主要操作是将各項操作對應到ldsic或者是tty_driver.

4.1:打開tty裝置的操作

從注冊的過程可以看到,所有的操作都會對應到tty_fops中。Open操作對應的操作接口是tty_open()。代碼如下:

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

         struct tty_struct *tty;

         int noctty, retval;

         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;

         index  = -1;

         retval = 0;

         //裝置号(5,0) 即/dev/tty.表示目前程序的控制終端

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

                   tty = get_current_tty();

                   //如果目前程序的控制終端不存在,退出

                   if (!tty) {

                            mutex_unlock(&tty_mutex);

                            return -ENXIO;

                   //取得目前程序的tty_driver

                   driver = tty->driver;

                   index = tty->index;

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

                   /* noctty = 1; */

                   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;

#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;

                   mutex_unlock(&tty_mutex);

                   return -ENODEV;

         //以檔案的裝置号為關鍵字,到tty_drivers中搜尋所注冊的driver

         driver = get_tty_driver(device, &index);

         if (!driver) {

got_driver:

         //index表示它的次裝置号

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

         if (retval)

                   return retval;

         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)

#ifdef TTY_DEBUG_HANGUP

         printk(KERN_DEBUG "opening %s...", tty->name);

         if (!retval) {

                   if (tty->driver->open)

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

                   else

                            retval = -ENODEV;

         filp->f_flags = saved_flags;

         if (!retval && test_bit(TTY_EXCLUSIVE, &tty->flags) &&

                                                        !capable(CAP_SYS_ADMIN))

                   retval = -EBUSY;

         if (retval) {

                   printk(KERN_DEBUG "error %d in opening %s...", retval,

                          tty->name);

                   release_dev(filp);

                   if (retval != -ERESTARTSYS)

                            return retval;

                   if (signal_pending(current))

                   schedule();

                   /*

                    * Need to reset f_op in case a hangup happened.

                    */

                   if (filp->f_op == &hung_up_tty_fops)

                            filp->f_op = &tty_fops;

                   goto retry_open;

         spin_lock_irq(&current->sighand->siglock);

         //設定目前程序的終端

         if (!noctty &&

             current->signal->leader &&

             !current->signal->tty &&

             tty->session == NULL)

                   __proc_set_tty(current, tty);

         spin_unlock_irq(&current->sighand->siglock);

         tty_audit_opening();

注意在這裡有個容易忽略的操作: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():

struct tty_ldisc *tty_ldisc_get(int disc)

         unsigned long flags;

         struct tty_ldisc *ld;

         if (disc = NR_LDISCS)

                   return NULL;

         spin_lock_irqsave(&tty_ldisc_lock, flags);

         ld = &tty_ldiscs[disc];

         /* Check the entry is defined */

         if (ld->flags & LDISC_FLAG_DEFINED) {

                   /* If the module is being unloaded we can't use it */

                   if (!try_module_get(ld->owner))

                            ld = NULL;

                   else /* lock it */

                            ld->refcount++;

         } else

                   ld = NULL;

         spin_unlock_irqrestore(&tty_ldisc_lock, flags);

         return ld;

這個函數的操作為到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調用進行更改.

将上述open用流程圖的方式表示如下:

4.2:裝置檔案的write操作

裝置檔案的write操作對應tty_fops->write即tty_write().代碼如下:

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

                                                        size_t count, loff_t *ppos)

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

         ssize_t ret;

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

         if (tty_paranoia_check(tty, inode, "tty_write"))

                   return -EIO;

         if (!tty || !tty->driver->write ||

                   (test_bit(TTY_IO_ERROR, &tty->flags)))

                            return -EIO;

         ld = tty_ldisc_ref_wait(tty);

         if (!ld->write)

                   ret = -EIO;

         else

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

         tty_ldisc_deref(ld);

         return ret;

在open的過程中,将tty_struct存放在file的私有區。在write中,從file的私有區中就可以取到要操作的tty_struct.

如果tty_driver中沒有write.如果tty有錯誤都會有效性判斷失敗傳回。如果一切正常,遞增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);

         if (ret

                   return ret;

         /*

          * We chunk up writes into a temporary buffer. This

          * simplifies low-level drivers immensely, since they

          * don't have locking issues and user mode accesses.

          *

          * But if TTY_NO_WRITE_SPLIT is set, we should use a

          * big chunk-size..

          * The default chunk-size is 2kB, because the NTTY

          * layer has problems with bigger chunks. It will

          * claim to be able to handle more characters than

          * it actually does.

          * FIXME: This can probably go away now except that 64K chunks

          * are too likely to fail unless switched to vmalloc...

          */

         chunk = 2048;

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

                   chunk = 65536;

         if (count

                   chunk = count;

         /* write_buf/write_cnt is protected by the atomic_write_lock mutex */

         if (tty->write_cnt

                   unsigned char *buf;

                   if (chunk

                            chunk = 1024;

                   buf = kmalloc(chunk, GFP_KERNEL);

                   if (!buf) {

                            ret = -ENOMEM;

                            goto out;

                   kfree(tty->write_buf);

                   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;

                   ret = -EFAULT;

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

                            break;

                   lock_kernel();

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

                   unlock_kernel();

                   if (ret

                   written += ret;

                   buf += ret;

                   count -= ret;

                   if (!count)

                   ret = -ERESTARTSYS;

                   cond_resched();

         if (written) {

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

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

                   ret = written;

out:

         tty_write_unlock(tty);

後面的操作就比較簡單了。先将使用者空間的資料copy到臨時緩存區,然後再調用ldisc->write()完成這次寫操作.最後再更新裝置結點的時間戳.

Write操作的流程圖如下示:

在這裡,我們隻看到将資料寫放到了ldisc->write().沒有看到與tty_driver相關的部份。實際上在ldisc中對寫入的資料做預處理過後,還是會調用tty_driver->write()将其寫入硬體.

4.3:裝置檔案的read操作

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

                            loff_t *ppos)

         struct inode *inode;

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

         if (tty_paranoia_check(tty, inode, "tty_read"))

         if (!tty || (test_bit(TTY_IO_ERROR, &tty->flags)))

         /* We want to wait for the line discipline to sort out in this

            situation */

         lock_kernel();

         if (ld->read)

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

                   i = -EIO;

         unlock_kernel();

         if (i > 0)

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

         return i;

這個read操作就更簡單。直接調用ldsic->read()完成工作

流程圖如下:

五:小結

在tty裝置檔案的操作中。Open操作會進行一系統初始化。然後調用ldsic->open tty_driver->open。在write和read調用中隻tty_core隻會用到ldisc->wirte/ldisc->read.除了上面分析的幾個操作之外,還有一個ioctl操作,以及它封裝的幾個termios。這些ioctl類的操作會直接和tty_driver相關聯.

在這一節裡,隻對tty的構造做一個分析,具體ldisc的操作我們之後以控制終端為例進行分析.

閱讀(78) | 評論(0) | 轉發(0) |

給主人留下些什麼吧!~~

評論熱議

請登入後評論。

繼續閱讀