码迷,mamicode.com
首页 > 其他好文 > 详细

[tty与uart]2.tty驱动分析

时间:2016-06-25 13:33:55      阅读:385      评论:0      收藏:0      [点我收藏+]

标签:

转自:http://www.wowotech.net/linux_kenrel/183.html

目录:

1 首先分析设备驱动的注册

1.1 uart_register_driver分析

1.2 tty_register_driver分析

1.3 serial8250_register_ports()函数分析

1.4 serial8250_probe()函数分析

2 然后,我们来看设备的打开过程

3 TTY设备的读

3.1 read_chan()

4 TTY设备的写

5 总结 

首先分析设备驱动的注册

对于8250.c来说,主要涉及:

  • serial8250_init()--->uart_register_driver(&serial8250_reg)
  • serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev)
  • serial8250_probe(struct platform_device *dev)

struct uart_driver serial8250_reg的定义如下:

 1 static static struct uart_driver serial8250_reg = {
 2 
 3 .owner = THIS_MODULE,
 4 .driver_name = "serial",
 5 .dev_name = "ttyS",
 6 .major = TTY_MAJOR,
 7 .minor = 64,
 8 .nr = UART_NR,
 9 .cons = SERIAL8250_CONSOLE,
10 }; 

 

1.1 uart_register_driver分析

主要完成了一下功能:

  • 分配数个uart_state结构体内存:      (在uart_add_one_port()里会用到它来关联uart_port)
  • 分配tty_driver。normal  = alloc_tty_driver(drv->nr)
  • 关联struct uart_driver和tty_driver:
          uart_driver-> tty_driver= tty_driver;     tty_driver ->driver_state = uart_driver;
  • 设置tty_driver的操作函数为uart_ops(tty_operations类型)中的操作函数:
  • 调用tty_register_driver():根据tty_driver里的数据来注册字符设备(来自于uart_driver);并添加到tty_drivers链表;调用tty_register_device()产生设备文件。 
技术分享
 1 int uart_register_driver(struct uart_driver *drv)
 2 {
 3 struct tty_driver *normal = NULL;
 4 int i, retval; BUG_ON(drv->state); /*
 5 * Maybe we should be using a slab cache for this, especially if
 6 * we have a large number of ports to handle.
 7 */
 8 drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);
 9 retval = -ENOMEM;
10 if (!drv->state)
11 goto out; normal = alloc_tty_driver(drv->nr);
12 if (!normal)
13 goto out; drv->tty_driver = normal; normal->owner = drv->owner;
14 normal->driver_name = drv->driver_name;
15 normal->name = drv->dev_name;
16 normal->major = drv->major;
17 normal->minor_start = drv->minor;
18 normal->type = TTY_DRIVER_TYPE_SERIAL;
19 normal->subtype = SERIAL_TYPE_NORMAL;
20 normal->init_termios = tty_std_termios;
21 normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
22 normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;
23 normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;
24 normal->driver_state = drv;
25 tty_set_operations(normal, &uart_ops); /*
26 * Initialise the UART state(s).
27 */
28 for (i = 0; i < drv->nr; i++) {
29 struct uart_state *state = drv->state + i; state->close_delay = 500; /* .5 seconds */
30 state->closing_wait = 30000; /* 30 seconds */ mutex_init(&state->mutex);
31 } retval = tty_register_driver(normal);
32 out:
33 if (retval < 0) {
34 put_tty_driver(normal);
35 kfree(drv->state);
36 }
37 return retval;
38 }
tty_register_device

 

1.2 tty_register_driver分析

与传统的字符设备驱动程序完全一致,主要做了一下工作:

  • 创建字符设备
  • 注册字符设备
  • 设置udev,创建/dev节点,名称为"%s%d", driver->name, index + driver->name_base,
                       normal->name = uart_driver->dev_name; //来自于uart_driver= "ttyS", //见struct uart_driver serial8250_reg的定义。
                       driver->name_base =0;
                       driver->num=(0--- driver->num);  // driver->num = uart_driver->nr = UART_NR = 8 
                                   因此创建的节点名为:/dev/ttySx  x=(0…7)
  • Proc文件系统操作; 
技术分享
 1 int tty_register_driver(struct tty_driver *driver)
 2 {
 3 int error;
 4 int i;
 5 dev_t dev;
 6 void **p = NULL; if (driver->flags & TTY_DRIVER_INSTALLED)
 7 return 0; if (!(driver->flags & TTY_DRIVER_DEVPTS_MEM) && driver->num) {
 8 p = kzalloc(driver->num * 3 * sizeof(void *), GFP_KERNEL);
 9 if (!p)
10 return -ENOMEM;
11 } if (!driver->major) {
12 error = alloc_chrdev_region(&dev, driver->minor_start, driver->num,
13 driver->name);
14 if (!error) {
15 driver->major = MAJOR(dev);
16 driver->minor_start = MINOR(dev);
17 }
18 } else {
19 dev = MKDEV(driver->major, driver->minor_start);
20 error = register_chrdev_region(dev, driver->num, driver->name);
21 }
22 if (error < 0) {
23 kfree(p);
24 return error;
25 } if (p) {
26 driver->ttys = (struct tty_struct **)p;
27 driver->termios = (struct ktermios **)(p + driver->num);
28 driver->termios_locked = (struct ktermios **)(p + driver->num * 2);
29 } else {
30 driver->ttys = NULL;
31 driver->termios = NULL;
32 driver->termios_locked = NULL;
33 } cdev_init(&driver->cdev, &tty_fops);
34 driver->cdev.owner = driver->owner;
35 error = cdev_add(&driver->cdev, dev, driver->num); if (error) {
36 unregister_chrdev_region(dev, driver->num);
37 driver->ttys = NULL;
38 driver->termios = driver->termios_locked = NULL;
39 kfree(p);
40 return error;
41 } if (!driver->put_char)
42 driver->put_char = tty_default_put_char; mutex_lock(&tty_mutex);
43 list_add(&driver->tty_drivers, &tty_drivers);
44 mutex_unlock(&tty_mutex); if ( !(driver->flags & TTY_DRIVER_DYNAMIC_DEV) ) {
45 for(i = 0; i < driver->num; i++)
46 tty_register_device(driver, i, NULL);
47 }
48 proc_tty_register_driver(driver);
49 return 0;
50 }
tty_register_driver

此时,内核已经注册了tty_drivers到全局链表tty_drivers。

 

1.3 serial8250_register_ports()函数分析

主要完成以下任务:

  • 为端口号line赋值 
  • 初始化定时器
  • 为uart_8250_port->uart_port.ops赋值= &serial8250_pops
  • 为uart_8250_port[].uart_port->device赋值
  • 将uart_8250_port[].uart_port挂入uart_driver->state[]->port 
技术分享
1 static void __init serial8250_register_ports(struct uart_driver *drv, struct device *dev)
2 {
3 int i; serial8250_isa_init_ports(); for (i = 0; i < nr_uarts; i++) {
4 struct uart_8250_port *up = &serial8250_ports[i]; up->port.dev = dev;
5 uart_add_one_port(drv, &up->port);
6 }
serial8250_register_ports 

 

1.4 serial8250_probe()函数分析

通过struct plat_serial8250_port *p = dev->dev.platform_data获取platform_device的设备私有数据(里面一般包括mapbase、irq、iotype等),将这些数据赋给uart_port,然后调用:

serial8250_register_port()--->uart_add_one_port(&serial8250_reg, &uart->port)

将uart_port注册到uart_driver->state[]->port里面。

技术分享
 1 static int __devinit serial8250_probe(struct platform_device *dev)
 2 {
 3         struct plat_serial8250_port *p = dev->dev.platform_data;
 4         struct uart_port port;
 5         int ret, i;
 6  
 7         memset(&port, 0, sizeof(struct uart_port));
 8  
 9         for (i = 0; p && p->flags != 0; p++, i++) {
10                 port.iobase     = p->iobase;
11                 port.membase    = p->membase;
12                 port.irq        = p->irq;
13                 port.uartclk    = p->uartclk;
14                 port.regshift   = p->regshift;
15                 port.iotype     = p->iotype;
16                 port.flags      = p->flags;
17                 port.mapbase    = p->mapbase;
18                 port.hub6       = p->hub6;
19                 port.dev        = &dev->dev;
20                 if (share_irqs)
21                         port.flags |= UPF_SHARE_IRQ;
22                 ret = serial8250_register_port(&port);
23                 if (ret < 0) {
24                         dev_err(&dev->dev, "unable to register port at index %d "
25                                 "(IO%lx MEM%llx IRQ%d): %d\n", i,
26                                 p->iobase, (unsigned long long)p->mapbase,
27                                 p->irq, ret);
28                 }
29         }
30         return 0;
31 }
serial8250_probe

 

然后,我们来看设备的打开过程

以/dev/ttyS0为例。

根据系统在前面在此字符设备注册的fops,在open()后,系统应该是进入tty_fops的tty_open()函数。

可以明确:

tty_struct结构是在tty_open()时构建;

tty_struct保存在file->private_data; 
        以后的操作通过filp就可以找到tty_struct

然后通过tty_struct->tty_driver->open(tty_struct*, filp)调用的是tty_operations uart_ops.open =uart_open(serile_core.c);通过 uart_register_driver()->tty_set_operations(normal, &uart_ops)注册。
        tty_operations里的函数都是以(tty_struct, file* filp) 为参数。

而在uart_open(tty_struct*, filp)里,进行一些初始化后,调用了uart_startup(state, 0),此函数主要做了两件事:
        1)分配并初始化transmit 和 temporary缓冲区circ_buf
        2)调用port->ops->startup(port
                port=state.port |state = uart_driver->state[] |uart_driver=tty_struct->tty_driver->driver_state 

技术分享
 1 static int tty_open(int input, int output, int primary, void *d,
 2                     char **dev_out)
 3 {
 4         struct tty_chan *data = d;
 5         int fd, err;        fd = os_open_file(data->dev, of_set_rw(OPENFLAGS(), input, output), 0);
 6         if(fd < 0)
 7                 return fd;        if(data->raw){
 8                 CATCH_EINTR(err = tcgetattr(fd, &data->tt));
 9                 if(err)
10                         return err;                err = raw(fd);
11                 if(err)
12                         return err;
13         }        *dev_out = data->dev;
14         return fd;
15 }
tty_open

  

总结

tty_open()后,创建了tty_struct,并保存在filp中;再调用uart层的tty_operations->uart_ops.open(),在里面创建了发送的circ_buf;然后调用了uart_port->uart_ops->open(tty, filp)。

tty_struct对应一个已经打开的具体tty设备。 

 

3 TTY设备的读

TTY设备的读分为两部分:首先是进程读取tty_struct对应的缓冲区并阻塞当前进程;然后设备中断里,接收数据,唤醒进程的读操作。

程序首先进入tty_read():

  • 首先通过file->private_data获取tty_struct,然后再获取tty_ldisc;
  • 最后调用tty_ldisc->read。对于N_TTY即tty_ldisc_N_TTY.read()=read_chan()  
技术分享
 1 static ssize_t tty_read(struct file * file, char __user * buf, size_t count,
 2                         loff_t *ppos)
 3 {
 4         int i;
 5         struct tty_struct * tty;
 6         struct inode *inode;
 7         struct tty_ldisc *ld;
 8         tty = (struct tty_struct *)file->private_data;
 9         inode = file->f_path.dentry->d_inode;
10         if (tty_paranoia_check(tty, inode, "tty_read"))
11                 return -EIO;
12         if (!tty || (test_bit(TTY_IO_ERROR, &tty->flags)))
13                 return -EIO;
14         /* We want to wait for the line discipline to sort out in this
15            situation */
16         ld = tty_ldisc_ref_wait(tty);
17         lock_kernel();
18         if (ld->read)
19                 i = (ld->read)(tty,file,buf,count);
20         else
21                 i = -EIO;
22         tty_ldisc_deref(ld);
23         unlock_kernel();
24         if (i > 0)
25                 inode->i_atime = current_fs_time(inode->i_sb);
26         return i;
tty_read 

 

3.1 read_chan()

  • 初始化延迟工作队列:init_dev()==>initialize_tty_struct()==>INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc)
  • tty->read_wait只被n_tty_receive_buf()函数(或里面的分支)调用;
  • n_tty_receive_buf()只被flush_to_ldisc()调用
  • 而tty_flip_buffer_push()有两种方式来调用flush_to_ldisc(): 
         1)tty->low_latency===> flush_to_ldisc(&tty->buf.work.work); 
         2)schedule_delayed_work(&tty->buf.work, 1); 
    两者都是调用flush_to_ldisc(),不同点在于后者是延迟执行flush_to_ldisc()。延迟工作队列是在initialize_tty_struct()===>INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc);中进行初始化的。

对于驱动层,调用轨迹如下:

在open()操作里申请中断;在中断里唤醒进程。

tty_open()==>………==>serial8250_startup()==>serial_link_irq_chain()==>request_irq()--------申请中断

serial8250_interrupt()--------------------------------------------------------------------------------------------处理中断

    ->serial8250_handle_port()

        ->receive_chars()

    -> uart_insert_char()               //接收字符,存入tty_buffer,tty_struct包含tty_bufhead

           ->tty_insert_flip_char()  //而tty_bufhead包含三个tty_buffer成员:head、tail、free
    ->tty_flip_buffer_push()

    -> flush_to_ldisc()

           -> n_tty_receive_buf()

                -> memcpy(tty->read_buf + tty->read_head, cp, i);  //拷贝数据至tty->read_buf

                     ->tty->read_cnt += i                   //指示接收buff的字符数。

                                                                      //与read_chan()-->input_available_p()
                                                                 里对tty->read_cnt的判断对应

                            ->wake_up(&tty->read_wait)          //唤醒进程

 

大致是下图的流程:

调用tty_insert_flip_char或者tty_insert_flip_string将数据放入tty的缓存tty->tty_buffer;然后调用tty_flip_buffer_push(),将数据从tty缓存拷贝至ldisc缓存。

技术分享

 

 技术分享  

技术分享
  1 static ssize_t read_chan(struct tty_struct *tty, struct file *file,
  2                          unsigned char __user *buf, size_t nr)
  3 {
  4         unsigned char __user *b = buf;
  5         DECLARE_WAITQUEUE(wait, current);
  6         int c;
  7         int minimum, time;
  8         ssize_t retval = 0;
  9         ssize_t size;
 10         long timeout;
 11         unsigned long flags;
 12  
 13 do_it_again:
 14  
 15         if (!tty->read_buf) {
 16                 printk("n_tty_read_chan: called with read_buf == NULL?!?\n");
 17                 return -EIO;
 18         }
 19  
 20         c = job_control(tty, file);
 21         if(c < 0)
 22                 return c;
 23  
 24         minimum = time = 0;
 25         timeout = MAX_SCHEDULE_TIMEOUT;
 26         if (!tty->icanon) {
 27                 time = (HZ / 10) * TIME_CHAR(tty);
 28                 minimum = MIN_CHAR(tty);
 29                 if (minimum) {
 30                         if (time)
 31                                 tty->minimum_to_wake = 1;
 32                         else if (!waitqueue_active(&tty->read_wait) ||
 33                                  (tty->minimum_to_wake > minimum))
 34                                 tty->minimum_to_wake = minimum;
 35                 } else {
 36                         timeout = 0;
 37                         if (time) {
 38                                 timeout = time;
 39                                 time = 0;
 40                         }
 41                         tty->minimum_to_wake = minimum = 1;
 42                 }
 43         }
 44  
 45         /*
 46          *      Internal serialization of reads.
 47          */
 48         if (file->f_flags & O_NONBLOCK) {
 49                 if (!mutex_trylock(&tty->atomic_read_lock))
 50                         return -EAGAIN;
 51         }
 52         else {
 53                 if (mutex_lock_interruptible(&tty->atomic_read_lock))
 54                         return -ERESTARTSYS;
 55         }
 56  
 57         add_wait_queue(&tty->read_wait, &wait);
 58         while (nr) {
 59                 /* First test for status change. */
 60                 if (tty->packet && tty->link->ctrl_status) {
 61                         unsigned char cs;
 62                         if (b != buf)
 63                                 break;
 64                         cs = tty->link->ctrl_status;
 65                         tty->link->ctrl_status = 0;
 66                         if (tty_put_user(tty, cs, b++)) {
 67                                 retval = -EFAULT;
 68                                 b--;
 69                                 break;
 70                         }
 71                         nr--;
 72                         break;
 73                 }
 74                 /* This statement must be first before checking for input
 75                    so that any interrupt will set the state back to
 76                    TASK_RUNNING. */
 77                 set_current_state(TASK_INTERRUPTIBLE);
 78  
 79                 if (((minimum - (b - buf)) < tty->minimum_to_wake) &&
 80                     ((minimum - (b - buf)) >= 1))
 81                         tty->minimum_to_wake = (minimum - (b - buf));
 82  
 83                 if (!input_available_p(tty, 0)) {
 84                         if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {
 85                                 retval = -EIO;
 86                                 break;
 87                         }
 88                         if (tty_hung_up_p(file))
 89                                 break;
 90                         if (!timeout)
 91                                 break;
 92                         if (file->f_flags & O_NONBLOCK) {
 93                                 retval = -EAGAIN;
 94                                 break;
 95                         }
 96                         if (signal_pending(current)) {
 97                                 retval = -ERESTARTSYS;
 98                                 break;
 99                         }
100                         n_tty_set_room(tty);
101                         timeout = schedule_timeout(timeout);
102                         continue;
103                 }
104                 __set_current_state(TASK_RUNNING);
105  
106                 /* Deal with packet mode. */
107                 if (tty->packet && b == buf) {
108                         if (tty_put_user(tty, TIOCPKT_DATA, b++)) {
109                                 retval = -EFAULT;
110                                 b--;
111                                 break;
112                         }
113                         nr--;
114                 }
115  
116                 if (tty->icanon) {
117                         /* N.B. avoid overrun if nr == 0 */
118                         while (nr && tty->read_cnt) {
119                                 int eol;
120  
121                                 eol = test_and_clear_bit(tty->read_tail,
122                                                 tty->read_flags);
123                                 c = tty->read_buf[tty->read_tail];
124                                 spin_lock_irqsave(&tty->read_lock, flags);
125                                 tty->read_tail = ((tty->read_tail+1) &
126                                                   (N_TTY_BUF_SIZE-1));
127                                 tty->read_cnt--;
128                                 if (eol) {
129                                         /* this test should be redundant:
130                                          * we shouldn‘t be reading data if
131                                          * canon_data is 0
132                                          */
133                                         if (--tty->canon_data < 0)
134                                                 tty->canon_data = 0;
135                                 }
136                                 spin_unlock_irqrestore(&tty->read_lock, flags);
137  
138                                 if (!eol || (c != __DISABLED_CHAR)) {
139                                         if (tty_put_user(tty, c, b++)) {
140                                                 retval = -EFAULT;
141                                                 b--;
142                                                 break;
143                                         }
144                                         nr--;
145                                 }
146                                 if (eol) {
147                                         tty_audit_push(tty);
148                                         break;
149                                 }
150                         }
151                         if (retval)
152                                 break;
153                 } else {
154                         int uncopied;
155                         uncopied = copy_from_read_buf(tty, &b, &nr);
156                         uncopied += copy_from_read_buf(tty, &b, &nr);
157                         if (uncopied) {
158                                 retval = -EFAULT;
159                                 break;
160                         }
161                 }
162  
163                 /* If there is enough space in the read buffer now, let the
164                  * low-level driver know. We use n_tty_chars_in_buffer() to
165                  * check the buffer, as it now knows about canonical mode.
166                  * Otherwise, if the driver is throttled and the line is
167                  * longer than TTY_THRESHOLD_UNTHROTTLE in canonical mode,
168                  * we won‘t get any more characters.
169                  */
170                 if (n_tty_chars_in_buffer(tty) <= TTY_THRESHOLD_UNTHROTTLE) {
171                         n_tty_set_room(tty);
172                         check_unthrottle(tty);
173                 }
174  
175                 if (b - buf >= minimum)
176                         break;
177                 if (time)
178                         timeout = time;
179         }
180         mutex_unlock(&tty->atomic_read_lock);
181         remove_wait_queue(&tty->read_wait, &wait);
182  
183         if (!waitqueue_active(&tty->read_wait))
184                 tty->minimum_to_wake = minimum;
185  
186         __set_current_state(TASK_RUNNING);
187         size = b - buf;
188         if (size) {
189                 retval = size;
190                 if (nr)
191                         clear_bit(TTY_PUSH, &tty->flags);
192         } else if (test_and_clear_bit(TTY_PUSH, &tty->flags))
193                  goto do_it_again;
194  
195         n_tty_set_room(tty);
196  
197         return retval;
198 }
read_chan

 

 

 4 TTY设备的写

首先调用tty_write. 

技术分享
 1 static ssize_t tty_write(struct file * file, const char __user * buf, size_t count,
 2                          loff_t *ppos)
 3 {
 4         struct tty_struct * tty;
 5         struct inode *inode = file->f_path.dentry->d_inode;
 6         ssize_t ret;
 7         struct tty_ldisc *ld;
 8  
 9         tty = (struct tty_struct *)file->private_data;
10         if (tty_paranoia_check(tty, inode, "tty_write"))
11                 return -EIO;
12         if (!tty || !tty->driver->write || (test_bit(TTY_IO_ERROR, &tty->flags)))
13                 return -EIO;
14  
15         ld = tty_ldisc_ref_wait(tty);
16         if (!ld->write)
17                 ret = -EIO;
18         else
19                 ret = do_tty_write(ld->write, tty, file, buf, count);
20         tty_ldisc_deref(ld);
21         return ret;
22 }
tty_write

  

TTY设备写涉及写进程、中断ISR、即tasklet_action三部分的配合: 

 技术分享

 

应用APP函数调用

tty_write()---------------------------------------------------------------------------------------------------tty_io.c

     do_tty_write()

           copy_from_user()             //将用户空间数据copy至tty->write_buf

           write ()                            //对于N_TTY,即tty_ldisc_N_TTY.write()=write_chan()

                add_wait_queue()-------------------------------------------------------------------------n_tty.c

                                                                 //添加等待队列

                tty->driver->flush_chars(tty);      //struct tty_operations uart_ops. flush_chars=uart_flush_chars()

                      uart_start()---------------------------------------------------------------------------serial_core.c

                             __uart_start()

                                     uart_port->ops->start_tx()   //uart_ops serial8250_pops.start_tx = serial8250_start_tx

                                           transmit_chars()               //启动真正发送------------------------------8250.c

                                                   serial_out(up, UART_TX, xmit->buf[xmit->tail])      //copy至芯片发送buffer

                                           uart_write_wakeup(struct uart_port *port)           //调度tasklet_schedule()

                                                   tasklet_schedule(&info->tlet);

                        schedule();                 //进程睡眠

中断函数调用:

serial8250_interrupt()-----------------------------------------------------------------------------------处理中断

         serial8250_handle_port()------------------------------------------------------------------------8250.c

                   if(Transmit-hold-register empty)

                            transmit_chars()

                                     serial_out(up, UART_TX, xmit->buf[xmit->tail])      //copy至芯片发送缓冲

                                     uart_write_wakeup()---------------------------------------------------serial_core.c

                                               tasklet_schedule()                                    //调度tasklet_schedule()

tasklet在tty_open()-->……-->uart_open()-->uart_get()
        -->tasklet_init(&state->info->tlet, uart_tasklet_action,state)中进行初始化。
       【tty_open()-->tty_struct.tty_driver.open()=uart_open()-->uart_get()-->tasklet_init()】

tasklet_action()调用:

经过tasklet_schedule ()后执行uart_tasklet_action。

uart_tasklet_action ()

         tty_wakeup()

                   ld->write_wakeup(tty)//即n_tty_write_wakeup():发送信号SIGIO给fasync_struct所描述的PID

                   wake_up_interruptible(&tty->write_wait);    //唤醒写进程

 

总结

在理清了数据走向和函数调用关系后,我们可以清晰的知道开发TTY驱动,需要我们做什么:

  • 定义uart_driver数据结构;
  • 定义uart_port数据结构;
  • 完成uart_ops操作函数集合。

最后放一张从进程、vfs、tty_core、serial_core到uart驱动各个数据结构之间的相互关系图: 

技术分享

 

[tty与uart]2.tty驱动分析

标签:

原文地址:http://www.cnblogs.com/aaronLinux/p/5616153.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!