您的位置:首页 > 运维架构 > Linux

Linux中tty框架与uart框架之间的调用关系剖析

2013-12-03 16:31 661 查看
在串口驱动移植中static struct platform_device sc16550_device结构体在配置好以后,使用了linux内核模型的platform总线机制中设备注册接口函数:platform_device_register(&sc16550_device);将 sc16550_device 设备挂载到了platform bus上。上文已经提到驱动所使用的正是 8250来进行驱动,所以在8250.c驱动init时,调用的platform_driver_register(&serial8250_isa_driver);函数正是加载该驱动到platform
bus上,

下面是关于sc16550_device的重要的结构体的配置具体情况:
static struct plat_serial8250_port sc16550_data[] = {
.mapbase  = sc16550_UART_BASE, //flags使用IOREMAP,8250驱动会自动映射mapbase
.irq = sc16550_UART_IRQ,
.uartclk  = sc16550_UART_BAUD * 16,
.iotype  = UPIO_MEM,
.flags  = UPF_BOOT_AUTOCONF |
UPF_SKIP_TEST | UPF_IOREMAP,
.regshift       = 0,
};
static struct platform_device sc16550_device = {
.name = "serial8250",
.id = PLAT8250_DEV_PLATFORM,
.dev = { .platform_data = sc16550_data, },
};


而serial8250_isa_driver结构体的定义为:
static struct platform_driver serial8250_isa_driver = {
.probe = serial8250_probe,
.remove = __devexit_p(serial8250_remove),
.suspend = serial8250_suspend,
.resume = serial8250_resume,
.driver = {
.name = "serial8250",
.owner = THIS_MODULE,
},
};


那么在sc16550_device设备和serial8250_isa_driver驱动具体是怎样加载到总线中,而两者之间又是如何匹配相认的呢?从而团结一心一致对外的呢?

今天我们就一起深究一下吧……

当设备挂接到总线上时,与总线上的所有驱动进行匹配(用bus_type.match进行匹配),如果匹配成功,则调用bus_type.probe或者driver.probe初始化该设备,挂接到总线上如果匹配失败,则只是将该设备挂接到总线上。

驱动挂接到总线上时,与总线上的所有设备进行匹配(用bus_type.match进行匹配), 如果匹配成功,则调用bus_type.probe或者driver.probe初始化该设备;挂接到总线上如果匹配失败,则只是将该驱动挂接到总线上。

那么现在我们无法判断到底我们的sc16550_device和serial8250_isa_driver是哪一个先挂载到总线上的。不过这也并不影响我们理解剖析整个linux设备模型。

因为实际上platform_bus_type总线先被kenrel注册,有必要对platform_bus_type 的定义作一番注释,其 定义如下:
struct bus_type platform_bus_type = {
.name         = "platform",       // bus 的名字,将会生成/sys/bus/platform  目录

/* 该属性文件将产生在所有 platform_bus_type 类型的设备目录下,文件名为"modalias” */
.dev_attrs    = platform_dev_attrs,
.match        = platform_match,   // 用于drive 与device 匹配的例程
.uevent       = platform_uevent,  //  用于输出环境变量,与属性文件“uevent”相关
.pm           = PLATFORM_PM_OPS_PTR, //  电源管理方面
};


代码中, 通过bus_register(&platform_bus_type)将platform_bus_type 注册到总线模块。

我们继续,系统初始化过程中调用platform_add_devices或者platform_device_register,将平台设备(platform devices)注册到平台总线中(platform_bus_type),平台驱动(platform driver)与平台设备(platform device)的关联是在platform device或者driver_register中实现,一般这个函数在驱动的初始化过程调用。通过这三步,就将平台总线,设备,驱动关联起来。

下面我们就具体的来看看这整个过程吧。

首先,我们先来看看platform_device_register……

我们知道在系统boot up的时候,系统初始化会调用platform_device_register(),而其又先后调用了 device_initialize()和platform_device_add()。下面解析device_initialize()和platform_device_add()两个例程,它们分别定义在drivers/ base/core.c 和drivers/base/platform.c 中。

device_initialize的代码如下:
void device_initialize(struct device *dev)
{
dev->kobj.kset = devices_kset;    // 设置其指向的kset 容器

kobject_init(&dev->kobj, &device_ktype); // 初始化 kobj,将 device_ktype 传递给它

klist_init(&dev->klist_children, klist_children_get,

klist_children_put);    // 初试化klist

INIT_LIST_HEAD(&dev->dma_pools);

init_MUTEX(&dev->sem);

spin_lock_init(&dev->devres_lock);

INIT_LIST_HEAD(&dev->devres_head);

device_init_wakeup(dev, 0);

device_pm_init(dev);       // 初试化电源管理

set_dev_node(dev, -1);
}


代码中:

devices_kset 是所有dev 的kset,也就是所有dev 都被链接在该kset 下,其在初试化例程 devices_init()中通过调kset_create_and_add("devices", &device_uevent_ops, NULL)来创建。由于参数parent=NULL ,所以生成/sys/devices 目录。这里说明下kobj,kset 结构体中包含有一个 kobj,一个kobj 生成一个目录,在这里就是”devices " 目录,通过调用kobject_add_internal()例程
生成。所以从dev->kobj.kset = devices_kset 可以看出,该dev.kobj 添加到了devices_kset 容器 中,所的kobj 都归属于一个特定的kset 。关于kset,kobj,ktype,kref 的关系可以参考书LDD3的第十四章,在第370 页有一张说明kobj 和kset 关系的图(英文版)。

kobject_init(&dev->kobj, &device_ktype)用于初始化 dev->kobj 中变量的参数,如ktype、 kref、entry 和state*等。初试化例程devices_init()还会调用kobject_create_and_add()例程生成/sys/ dev、/sys/dev/block 和/sys/dev/char 目录。

其他初始化。

下面分析platform_device_add:
int platform_device_add(struct platform_device *pdev)
  {
  int i, ret = 0;
  if (!pdev)
  return -EINVAL;
  if (!pdev->dev.parent)
  pdev->dev.parent = &platform_bus;
  //可以看出,platform设备的父设备一般都是platform_bus,所以注册后的platform设备都出现在/sys/devices/platform_bus下
  pdev->dev.bus = &platform_bus_type;
  //挂到platform总线上
  if (pdev->id != -1)
  dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
  else
  dev_set_name(&pdev->dev, "%s", pdev->name);
  //设置设备名字,这个名字与/sys/devices/platform_bus下的名字对应
  for (i = 0; i < pdev->num_resources; i++) { //下面操作设备所占用的系统资源
  struct resource *p, *r = &pdev->resource[i];
  if (r->name == NULL)
  r->name = dev_name(&pdev->dev);
  p = r->parent;
  if (!p) {
  if (resource_type(r) == IORESOURCE_MEM)
  p = &iomem_resource;
  else if (resource_type(r) == IORESOURCE_IO)
  p = &ioport_resource;
  }
  if (p && insert_resource(p, r)) {
  printk(KERN_ERR
  "%s: failed to claim resource %d\n",
  dev_name(&pdev->dev), i);
  ret = -EBUSY;
  goto failed;
  }
  }
  //上面主要是遍历设备所占用的资源,找到对应的父资源,如果没有定义,那么根据资源的类型,分别赋予iomem_resource和ioport_resource,然后调用insert_resource插入资源。
  //这样系统的资源就形成了一个树形的数据结构,便于系统的管理
  pr_debug("Registering platform device '%s'. Parent at %s\n",
  dev_name(&pdev->dev), dev_name(pdev->dev.parent));
  ret = device_add(&pdev->dev);
  //注册到设备模型中
  if (ret == 0)
  return ret;
  failed:
  while (--i >= 0) {
  struct resource *r = &pdev->resource[i];
  unsigned long type = resource_type(r);
  if (type == IORESOURCE_MEM || type == IORESOURCE_IO)
  release_resource(r);
  }
  return ret;
  }


以上就完成了device的到总线上的注册。接下来我就来看driver到总线上的挂载过程

该过程是一个非常复杂繁琐的过程,期间牵扯到了层层函数的调用,下面就给出了具体的过程:

platform_driver_register()->driver_register()->bus_add_driver()->driver_attach()->bus_for_each_dev()对每个挂在虚拟的platform bus的设备作__driver_attach()->driver_probe_device()->drv->bus->match()==platform_match()->比较strncmp(pdev->name, drv->name, BUS_ID_SIZE),如果相符就调用platform_drv_probe()->driver->probe(),如果probe成功则绑定该设备到该驱动.

这整个过程中有两个地方我们需要注意,相信大家心里已经很有数了,就是match()和probe(),一个负责匹配一个负责对成功绑定的设备进行port的赋值。

先来看match的过程吧:

前面也大致提到了所谓的match就是驱动成功注册到总线中后逐个与总线上已挂载的设配进行匹配,具体的实现就在driver_attach()里面了bus_for_each_dev()函数负责将驱动与设备们逐个匹配,这个函数中有一个参数函数最终调到了__driver_attach来实现具体的匹配过程,其中指针指向的match成员就是调用了paltform_match(), 当然这是要有根据的,大家不要忘记了platform_bus_type总线被kenrel注册的时候的那platform_bus_type结构体,里面的成员有一项为
.match=platform_match,对,paltform_match()函数就是定义在drivers/base/platform.c中。有兴趣的朋友可以check一下源码,很简单只有3行,值得一提的是有一个container_of的宏定义函数,在内核代码中此函数用的很多,可以着重了解一下,此函数可以返回传至函数内部参数所在的结构体的地址;初次之外就是一个简单的strcmp函数用来对比驱动与设备中所存的name是否一致。匹配成功后以后继续往下执行就会执行到probe。

为什么要执行probe呢,这是因为驱动好不容易找到了对的设备,就要把我们对该设备进行的一些初始化信息加入到驱动的标准处理过程中,当然这之后的行为就和我们的驱动模型没有多大的关系了。当然,继续关注我的朋友在以后应该会看到后续部分。那系统怎么知道probe函数到底调用的是哪一个驱动里的probe呢?就在下面……

probe具体调用的过程,大家先看下面的这个结构体:
static struct platform_driver serial8250_isa_driver = {
.probe  = serial8250_probe,
.remove  = __devexit_p(serial8250_remove),
.suspend  = serial8250_suspend,
.resume  = serial8250_resume,
.driver  = {
.name = "serial8250",
.owner  = THIS_MODULE,
},
};


.probe= serial8250_probe这一句就是重点了,从这一句我们可以很明显的看出我们在将驱动注册挂载到总线时的调用的platform_driver_register()所传的参数就是serial8250_isa_driver 结构体,所以当系统执行到driver->probe()时就会调用serial8250_probe()了。

到此为止,串口设备驱动加载过程中的系统驱动模型方面platform总线的行为就已经结束了,接下来的行为就是具体驱动的事情了。在后面我还会在后面的博客中写一些用户空间对驱动进行的读写等操作时,这些操作是如何层层调用,如何落实到驱动的底层操作。

之前本人在"从串口驱动的移植看linux2.6内核中的驱动模型
platform device & platform driver"一文中已经写到了移植的设备是如何通过platform总线来与对应的驱动挂载。

在这期间有一个问题困扰着我,那就是来自用户空间的针对uart设备的操作意图是如何通过tty框架逐层调用到uart层的core驱动,进而又是如何调用到真实对应于设备的设备驱动的,本文中的对应设备驱动就是8250驱动,最近我想将这方面的内容搞清楚。

在说明这一方面问题之前我们先要大致了解两个基本的框架结构,tty框架和uart框架。

首先看看tty框架:

在linux系统中,tty表示各种终端。终端通常都跟硬件相对应。比如对应于输入设备键盘鼠标,输出设备显示器的控制终端和串口终端。

下面这张图是一张很经典的图了,很清楚的展现了tty框架的层次结构,大家先看图,下面给大家解释。



最上面的用户空间会有很多对底层硬件(在本文中就是8250uart设备)的操作,像read,write等。用户空间主要是通过设备文件同tty_core交互,tty_core根据用空间操作的类型再选择跟line discipline和tty_driver也就是serial_core交互,例如设置硬件的ioctl指令就直接交给serial_core处理。Read和write操作就会交给line
discipline处理。Line discipline是线路规程的意思。正如它的名字一样,它表示的是这条终端”线程”的输入与输出规范设置,主要用来进行输入/输出数据的预处理。处理之后,就会将数据交给serial_core,最后serial_core会调用8250.c的操作。

下图是同一样一副经典的uart框架图,将uart重要的结构封装的很清楚,大家且看。



一个uart_driver通常会注册一段设备号.即在用户空间会看到uart_driver对应有多个设备节点。例如:

/dev/ttyS0 /dev/ttyS1 每个设备节点是对应一个具体硬件的,这样就可做到对多个硬件设备的统一管理,而每个设备文件应该对应一个uart_port,也就是说:uart_device要和多个uart_port关系起来。并且每个uart_port对应一个circ_buf(用来接收数据),所以uart_port必须要和这个缓存区关系起来。

1 自底向上

接下来我们就来看看对设备的操作是怎样进行起来的,不过在此之前我们有必要从底层的uart驱动注册时开始说起,这样到后面才能更清晰。

这里我们讨论的是8250驱动,在驱动起来的时候调用了uart_register_driver(&serial8250_reg);函数将参数serial8250_reg注册进了tty层。具体代码如下所示:

[cpp] view
plaincopy

int uart_register_driver(struct uart_driver *drv)

[cpp] view
plaincopy

{

struct tty_driver *normal = NULL;

int i, retval;

BUG_ON(drv->state);

/*

* Maybe we should be using a slab cache for this, especially if

* we have a large number of ports to handle.

*/

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

retval = -ENOMEM;

if (!drv->state)

goto out;

normal = alloc_tty_driver(drv->nr);

if (!normal)

goto out;

drv->tty_driver = normal;

normal->owner = drv->owner;

normal->driver_name = drv->driver_name;

normal->name = drv->dev_name;

normal->major = drv->major;

normal->minor_start = drv->minor;

normal->type = TTY_DRIVER_TYPE_SERIAL;

normal->subtype = SERIAL_TYPE_NORMAL;

normal->init_termios = tty_std_termios;

normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;

normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;

normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;

normal->driver_state = drv; // here is important for me, ref uart_open function in this file

tty_set_operations(normal, &uart_ops);

/*

* Initialise the UART state(s).

*/

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

out:

if (retval < 0) {

put_tty_driver(normal);

kfree(drv->state);

}

return retval;

}

从上面代码可以看出,uart_driver中很多数据结构其实就是tty_driver中的,将数据转换为tty_driver之后,注册tty_driver。然后初始化uart_driver->state的存储空间。

这里有两个地方我们需要特别关注:

第一个是

[cpp] view
plaincopy

normal->driver_state = drv;

为什么说重要呢,因为真实这一句将参数的ops关系都赋给了serial_core层。也就是说在后面serial_core会根据uart_ops关系找到我们的8250.c中所对应的操作,而我们参数中的ops是在哪被赋值的呢?这个一定是会在8250.c中不会错,所以我定位到了8250.c中的serial8250_ops结构体,初始化如下:

[cpp] view
plaincopy

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,

.startup = serial8250_startup,

.shutdown = serial8250_shutdown,

.set_termios = serial8250_set_termios,

.pm = serial8250_pm,

.type = serial8250_type,

.release_port = serial8250_release_port,

.request_port = serial8250_request_port,

.config_port = serial8250_config_port,

.verify_port = serial8250_verify_port,

#ifdef CONFIG_CONSOLE_POLL

.poll_get_char = serial8250_get_poll_char,

.poll_put_char = serial8250_put_poll_char,

#endif

};

这样一来只要将serial8250_ops结构体成员的值赋给我们uart_dirver就可以了,那么这个过程在哪呢?就是在uart_add_one_port()函数中,这个函数是从serial8250_init->serial8250_register_ports()->uart_add_one_port()逐步调用过来的,这一步就将port和uart_driver联系起来了。

第二个需要关注的地方:

[cpp] view
plaincopy

tty_set_operations(normal, &uart_ops);

此句之所以值得关注是因为.在这里将tty_driver的操作集统一设为了uart_ops.这样就使得从用户空间下来的操作可以找到正确的serial_core的操作函数,uart_ops是在serial_core.c中的:

[cpp] view
plaincopy

static const struct tty_operations uart_ops = {

.open = uart_open,

.close = uart_close,

.write = uart_write,

.put_char = uart_put_char,

.flush_chars = uart_flush_chars,

.write_room = uart_write_room,

.chars_in_buffer= uart_chars_in_buffer,

.flush_buffer = uart_flush_buffer,

.ioctl = uart_ioctl,

.throttle = uart_throttle,

.unthrottle = uart_unthrottle,

.send_xchar = uart_send_xchar,

.set_termios = uart_set_termios,

.set_ldisc = uart_set_ldisc,

.stop = uart_stop,

.start = uart_start,

.hangup = uart_hangup,

.break_ctl = uart_break_ctl,

.wait_until_sent= uart_wait_until_sent,

#ifdef CONFIG_PROC_FS

.read_proc = uart_read_proc,

#endif

.tiocmget = uart_tiocmget,

.tiocmset = uart_tiocmset,

#ifdef CONFIG_CONSOLE_POLL

.poll_init = uart_poll_init,

.poll_get_char = uart_poll_get_char,

.poll_put_char = uart_poll_put_char,

#endif

};

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

2 自顶向下

说完了从底层注册时所需要注意的地方,现在我们来看看正常的从上到下的调用关系。tty_core是所有tty类型的驱动的顶层构架,向用户应用层提供了统一的接口,应用层的read/write等调用首先会到达这里。此层由内核实现,代码主要分布在drivers/char目录下的n_tty.c,tty_io.c等文件中,下面的代码:

[cpp] view
plaincopy

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函数,代码如下:

[cpp] view
plaincopy

int tty_register_driver(struct tty_driver * driver)

{

...

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

...

}

我们可以看到,此句就已经将指针调用关系赋给了cdev,以用于完成调用。在前面我们已经说过了,Read和write操作就会交给line discipline处理,我们在下面的代码可以看出调用的就是线路规程的函数:

[cpp] view
plaincopy

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函数

else

i = -EIO;

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

if (!ld->ops->write)

ret = -EIO;

else

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 (;;) {

size_t size = count;

if (size > chunk)

size = chunk;

ret = -EFAULT;

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

break;

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

//调用到了ldisc层的write函数

if (ret <= 0)

break;

...

}

那我们就去看看线路规程调用的是又是谁,代码目录在drivers/char/n_tty.c文件中,下面的代码是线路规程中的write函数:

[cpp] view
plaincopy

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) {

set_current_state(TASK_INTERRUPTIBLE);

if (signal_pending(current)) {

retval = -ERESTARTSYS;

break;

}

//进入此处继续执行的原因可能是被信号打断,而不是条件得到了满足。

//只有条件得到了满足,我们才会继续,否则,直接返回!

if (tty_hung_up_p(file) || (tty->link && !tty->link->count)) {

retval = -EIO;

break;

}

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

while (nr > 0) {

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

if (num < 0) {

if (num == -EAGAIN)

break;

retval = num;

goto break_out;

}

b += num;

nr -= num;

if (nr == 0)

break;

c = *b;

if (process_output(c, tty) < 0)

break;

b++; nr--;

}

if (tty->ops->flush_chars)

tty->ops->flush_chars(tty);

} else {

while (nr > 0) {

c = tty->ops->write(tty, b, nr);

//调用到具体的驱动中的write函数

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;

}

/*

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

*/

schedule();//执行到这里,当前进程才会真正让出cpu!!!

}

break_out:

__set_current_state(TASK_RUNNING);

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

...

}

在上面我们可以看到此句:

[cpp] view
plaincopy

c = tty->ops->write(tty, b, nr);

此句很明显告诉我们这是调用了serial_core的write()函数,可是这些调用关系指针是在哪赋值的,刚开始我也是郁闷了一段时间,不过好在我最后还是找到了一些蛛丝马迹。其实就是在tty_core进行open的时候悄悄把tty->ops指针给赋值了。具体的代码就在driver/char/tty_io.c中,调用关系如下所示:

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

[cpp] view
plaincopy

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函数,在这里就是我们前面说到的8250驱动中的write函数没问题了。从这就可以看出其实在操作指针值得层层传递上open操作还是功不可没的,这么讲不仅仅是因为上面的赋值过程,还有下面这个,在open操作调用到serial_core层的时候有下面的代码:

[cpp] view
plaincopy

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

{

struct uart_driver *drv = (struct uart_driver *)tty->driver->driver_state; // here just tell me why uart_open can call 8250

struct uart_state *state;

int retval, line = tty->index;

……

uart_update_termios(state);

}

fail:

return retval;

}

在此函数的第一句我们就看到了似曾相识的东西了,没错就是我们在uart_register_driver()的时候所做的一些事情,那时我们是放进去,现在是拿出来而已。

这样一来,我们先从底层向上层分析上来后,又由顶层向底层分析下去,两头总算是接上头了,我很高兴,不是因为我花了近两个小时的时间终于写完了这篇博客,而是我是第一次通过这篇博客的写作过程弄清楚了这个有点小复杂的环节,当然有谬误的地方还是希望大家能慷慨指出。

分享知识,共同进步~

来源:http://blog.csdn.net/bonnshore/article/details/7996730


linux tty驱动架构分析

前一阵子移植一个串口驱动,发现linux的驱动构架中,面向对象的思想已经根深蒂固。就比如这串口驱动,代码中经常有一些貌似和串口无关的代码,比
如,tty_register_driver等。但我们却删它不得。因为正是这些代码实现了tty core和具体的tty driver(比如串口驱动)的联系和纽带。以前看ldd3,里边有术语tty core和tty driver,当是不清楚各指的是什么,但是后来看了代码,才知道,tty core指的是所有tty类型的驱动的顶层架构,它的代码由内核实现,我们无需修改,代码主要分布在drivers/char下的 n_tty.c,tty_io.c等文件中。而tty driver就指具体的设备驱动,比如串口驱动,console驱动等。以下总结只是对tty构架的总体分析,希望对大家有所启发。

tty的架构其实分为三层:

第一层:

tty_core

所有tty类型的驱动的顶层构架,向应用曾提供了统一的接口,应用层的read/write等调用首先会到达这里。此层由内核实现,代码主要分布在

drivers/char目录下的n_tty.c,tty_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,

};

每个tty类型的驱动注册时都调用tty_register_driver函数

int tty_register_driver(struct tty_driver * driver)

{

...

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

...

}

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函数

else

i = -EIO;

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

if (!ld->ops->write)

ret = -EIO;

else

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 (;;) {

size_t size = count;

if (size > chunk)

size = chunk;

ret = -EFAULT;

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

break;

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

//调用到了ldisc层的write函数

if (ret <= 0)

break;

...

}

第二层:线路规程

不同的tty类型的设备,具有不同的线路规程。这一层也由内核实现,主要代码在drivers/char.tty_ldisc.c文件中

从tty_read/tty_write函数可以看出,他们最后调用到了线路规程的read/write函数

struct tty_ldisc_ops 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 = 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

};

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) {

set_current_state(TASK_INTERRUPTIBLE);

if (signal_pending(current)) {

retval = -ERESTARTSYS;

break;

}

//进入此处继续执行的原因可能是被信号打断,而不是条件得到了满足。

//只有条件得到了满足,我们才会继续,否则,直接返回!

if (tty_hung_up_p(file) || (tty->link && !tty->link->count)) {

retval = -EIO;

break;

}

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

while (nr > 0) {

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

if (num < 0) {

if (num == -EAGAIN)

break;

retval = num;

goto break_out;

}

b += num;

nr -= num;

if (nr == 0)

break;

c = *b;

if (process_output(c, tty) < 0)

break;

b++; nr--;

}

if (tty->ops->flush_chars)

tty->ops->flush_chars(tty);

} else {

while (nr > 0) {

c = tty->ops->write(tty, b, nr);

//调用到具体的驱动中的write函数

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;

}

/*

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

*/

schedule();//执行到这里,当前进程才会真正让出cpu!!!

}

break_out:

__set_current_state(TASK_RUNNING);

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

...

}

关于此段代码的具体分析在
http://blog.chinaunix.net/u2/73067/showart. href="http://biancheng.dnbcw.info/php/" target=_blank>php?id=2241493

这 段代码中使用了wait等待队列,为什么要使用等待队列呢?大家想想看,我们在应用层打开一个设备文件的时候,有两种方式,阻塞和非阻塞,非阻塞很简单, 不管结果怎样直接返回。但阻塞则有点死皮赖脸的意思,会一直等待,直到操作完成。那write函数的“阻塞”版本在内核里边是怎么实现的呢?就是使用等待 队列,只要条件没有得到满足(驱动层调用write函数失败),那么就一直让出cpu,直到条件满足了才会继续执行,并将写操作的结果返回给上层。

通过以上分析,我们也可以得到如下结论:阻塞是在ldisc层也就是线路规程里边实现的。出于代价和操作性的考虑,我们不会再驱动里边实现阻塞类型的write/read函数

上述代码中有一句:

c = tty->ops->write(tty, b, nr);

这句代码调用到了tty_struct结构的ops->write函数。但是tty_struct结构的ops->write和具体的驱动里边定义的write函数有什么关系呢?

tty_open -> tty_init_dev -> initialize_tty_struct

driver/char/tty_io.c

void initialize_tty_struct(struct tty_struct *tty,

struct tty_driver *driver, int idx)

{

...

tty->ops = driver->ops;

...

}

可见,tty设备打开的时候,就将驱动的ops指针赋给了tty设备的结构体tty_struct的ops

这样,tty->ops->write()其实调用到了具体的驱动的write函数,比如,假如是个串口驱动,那么就会调用到串口驱动的write函数!

n_tty_read的操作比较复杂,暂时不讨论,但是它最终也会调用到具体的tty驱动的read函数

第三层:

具体的tty类型的驱动,由我们实现

比如,以下是摘自serial_core.c的一段代码,描述的是串口驱动:

static const struct tty_operations uart_ops = {

.open = uart_open,

.close = uart_close,

.write = uart_write,

.put_char = uart_put_char,

.flush_chars = uart_flush_chars,

.write_room = uart_write_room,

.chars_in_buffer= uart_chars_in_buffer,

.flush_buffer = uart_flush_buffer,

.ioctl = uart_ioctl,

.throttle = uart_throttle,

.unthrottle = uart_unthrottle,

.send_xchar = uart_send_xchar,

.set_termios = uart_set_termios,

.set_ldisc = uart_set_ldisc,

.stop = uart_stop,

.start = uart_start,

.hangup = uart_hangup,

.break_ctl = uart_break_ctl,

.wait_until_sent= uart_wait_until_sent,

#ifdef CONFIG_PROC_FS

.read_proc = uart_read_proc,

#endif

.tiocmget = uart_tiocmget,

.tiocmset = uart_tiocmset,

#ifdef CONFIG_CONSOLE_POLL

.poll_init = uart_poll_init,

.poll_get_char = uart_poll_get_char,

.poll_put_char = uart_poll_put_char,

#endif

};

int uart_register_driver(struct uart_driver *drv)

{

struct tty_driver *normal = NULL;

drv->tty_driver = normal;

normal->owner = drv->owner;

normal->driver_name = drv->driver_name;

normal->name = drv->dev_name;

normal->major = drv->major;

normal->minor_start = drv->minor;

normal->type = TTY_DRIVER_TYPE_SERIAL;

normal->subtype = SERIAL_TYPE_NORMAL;

normal->init_termios = tty_std_termios;

normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;

normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;

normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;

normal->driver_state = drv;

tty_set_operations(normal, &uart_ops);//##

...

}

我们主要实现这一层的功能,前两层是kernel中已经实现的,我们仅仅需要套用之。当我们按照tty driver的格式书写这一层驱动,并实现几个必要的函数,这个驱动就可以成功运转了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: