Chinaunix首页 | 论坛 | 博客
  • 博客访问: 408525
  • 博文数量: 62
  • 博客积分: 1483
  • 博客等级: 上尉
  • 技术积分: 779
  • 用 户 组: 普通用户
  • 注册时间: 2009-02-24 12:25
文章分类

全部博文(62)

文章存档

2012年(2)

2011年(6)

2010年(6)

2009年(48)

我的朋友

分类: LINUX

2009-10-23 21:20:56

linux下的tty,console,串口控制台,伪终端,终端设备驱动的概念很模糊,彼此的界限让人很郁闷。
这里主要是弄清楚什么是控制台以及控制台的作用。

1.有那些控制台?
2.linux对tty字符设备的设备号的使用规矩
3.怎么查看tty驱动和线路规程层?
4.查看你的标准输入输出错误输出用的是那个tty设备文件(驱动)?
5.关闭控制台echo的过程?
6.tty驱动的初始化过程?
7.启动信息是怎么给打印出来的呢?
为什么从命令行传递过去的参数中的所有console中都能看到打印信息?
为什么系统启动之后,open(/dev/console,O_RDWR)打开的是命令行最后的那个设备?

1.有那些控制台?
有两种控制台:串口控制台,虚拟控制台(vga+键盘),至于什么并口控制台本文当他们透明。
也就是说每种设备 (串口控制台 或者 虚拟控制台(vga+键盘))只能作为一种控制台,而且linux系统在启动之后
只支持其中的一种,要么是串口console,要么是虚拟控制台(vga+键盘)。

2.linux对tty字符设备的设备号的使用规矩
设备号
  主:次        对应的设备文件        表征的意义
(4,0)        /dev/vc/0        当前虚拟控制台,假如是pc的话,就是alt+fn切换过去的那组键盘+vga组合,arm下呢?
(5,0)        /dev/tty        本进程寄主,就是本进程的控制终端,也叫本进程的控制台(串口,vga+键盘二选一)
(5,1)        /dev/console    你使用的控制台(串口,vga+键盘二选一)
(5,2)        /dev/ptmx        伪终端

主次设备号是根本,设备文件的名字变化了没有关系
                        
[root@FriendlyARM /dev]# cd vc                                                 
[root@FriendlyARM vc]# ls                                                      
0   12  16  2   23  27  30  34  38  41  45  49  52  56  6   63                 
1   13  17  20  24  28  31  35  39  42  46  5   53  57  60  7                  
10  14  18  21  25  29  32  36  4   43  47  50  54  58  61  8                  
11  15  19  22  26  3   33  37  40  44  48  51  55  59  62  9                  
[root@FriendlyARM vc]# ls -l                                                   
crw-------    1 root     root       4,   0 Jan  1 00:06 0
...                   
crw-------    1 root     root       4,  63 Jan  1 00:00 63                        
[root@FriendlyARM vc]#

3.怎么查看tty驱动和线路规程层?
[root@FriendlyARM /]#  cat /proc/tty/drivers                                   
/dev/tty             /dev/tty        5       0 system:/dev/tty                 
/dev/console         /dev/console    5       1 system:console                  
/dev/ptmx            /dev/ptmx       5       2 system                          
/dev/vc/0            /dev/vc/0       4       0 system:vtmaster                 
ttySAC               /dev/s3c2410_serial 204 64-66 serial                      
pty_slave            /dev/pts      136 0-1048575 pty:slave                     
pty_master           /dev/ptm      128 0-1048575 pty:master                    
unknown              /dev/tty        4 1-63 console   

[root@FriendlyARM /]# cat proc/tty/ldiscs                                      
n_tty       0                                                                  
input       2
系统最多有16种线路规程,这里列出了第0个和2个以及他们的名字.n_tty就是console_init()中注册的那个.

4.查看你的标准输入输出错误输出用的是那个tty设备文件(驱动)?
在 apue 中描述ttyname函数为:在该描述符上打开的终端设备的路径名
编译下面这小段程序
main(argc, argv)
{

    char *tty[3];

    tty[0] = ttyname(0);
    tty[1] = ttyname(1);
    tty[2] = ttyname(2);
   
    int i;
    for(i=0;i<3;i++)
    printf("fd%d:%s\n",i,*tty[i]?tty[i]:"null");
}

在我的arm板子上:
[root@FriendlyARM /]# ./a.out                                                  
fd0:/dev/console                                                               
fd1:/dev/console                                                               
fd2:/dev/console

在xterm上:
lzd@lzd-laptop:~/c_test$ ./a.out
fd0:/dev/pts/0
fd1:/dev/pts/0
fd2:/dev/pts/0

在tty1上:
lzd@lzd-laptop:~/c_test$ ./a.out                                                 
fd0:/dev/tty1                                                              
fd1:/dev/tty1                                                               
fd2:/dev/tty1

5.关闭控制台echo的过程?
apue的一个练习:
stty -echo
reset
意思是先使终端处于非回显模式,然后复位为原来的默认模式。
那么他是怎么作的呢?
lzd@lzd-laptop:~/c_test$ strace stty -echo
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig icanon -echo ...}) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon -echo ...}) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon -echo ...}) = 0
close(1)                                = 0
close(2)                                = 0
可以发现,它是在对自己的标准输入设备的操作(这里应该都是/dev/pts/0)。ioctl的都是0哦。其实是0,1,2
都是无所谓的,都将设置到tty驱动中(这里是串口控制台)

6.tty驱动的初始化过程?
所有tty字符驱动都在tty_init()中初始化:
/*
 * Ok, now we can initialize the rest of the tty devices and can count
 * on memory allocations, interrupts etc..
 */
static int __init tty_init(void)
{
    cdev_init(&tty_cdev, &tty_fops);
    if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
        register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0)
        panic("Couldn't register /dev/tty driver\n");
    devfs_mk_cdev(MKDEV(TTYAUX_MAJOR, 0), S_IFCHR|S_IRUGO|S_IWUGO, "tty");
    class_device_create(tty_class, MKDEV(TTYAUX_MAJOR, 0), NULL, "tty");

    cdev_init(&console_cdev, &console_fops);
    if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
        register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0)
        panic("Couldn't register /dev/console driver\n");
    devfs_mk_cdev(MKDEV(TTYAUX_MAJOR, 1), S_IFCHR|S_IRUSR|S_IWUSR, "console");
    class_device_create(tty_class, MKDEV(TTYAUX_MAJOR, 1), NULL, "console");

#ifdef CONFIG_UNIX98_PTYS
    cdev_init(&ptmx_cdev, &ptmx_fops);
    if (cdev_add(&ptmx_cdev, MKDEV(TTYAUX_MAJOR, 2), 1) ||
        register_chrdev_region(MKDEV(TTYAUX_MAJOR, 2), 1, "/dev/ptmx") < 0)
        panic("Couldn't register /dev/ptmx driver\n");
    devfs_mk_cdev(MKDEV(TTYAUX_MAJOR, 2), S_IFCHR|S_IRUGO|S_IWUGO, "ptmx");
    class_device_create(tty_class, MKDEV(TTYAUX_MAJOR, 2), NULL, "ptmx");
#endif

#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)
        panic("Couldn't register /dev/tty0 driver\n");
    devfs_mk_cdev(MKDEV(TTY_MAJOR, 0), S_IFCHR|S_IRUSR|S_IWUSR, "vc/0");
    class_device_create(tty_class, MKDEV(TTY_MAJOR, 0), NULL, "tty0");

    vty_init();
#endif
    return 0;
}
module_init(tty_init);

static struct file_operations tty_fops = {
    .llseek        = no_llseek,
    .read        = tty_read,
    .write        = tty_write,
    .poll        = tty_poll,
    .ioctl        = tty_ioctl,
    .open        = tty_open,
    .release    = tty_release,
    .fasync        = tty_fasync,
};

#ifdef CONFIG_UNIX98_PTYS
static struct file_operations ptmx_fops = {
    .llseek        = no_llseek,
    .read        = tty_read,
    .write        = tty_write,
    .poll        = tty_poll,
    .ioctl        = tty_ioctl,
    .open        = ptmx_open,
    .release    = tty_release,
    .fasync        = tty_fasync,
};
#endif

static struct file_operations console_fops = {
    .llseek        = no_llseek,
    .read        = tty_read,
    .write        = redirected_tty_write,
    .poll        = tty_poll,
    .ioctl        = tty_ioctl,
    .open        = tty_open,
    .release    = tty_release,
    .fasync        = tty_fasync,
};

可见,他们的open一般都是tty_open()(除了伪终端是:ptmx_open),这样查看

显示出的前四行就是上面初始化的四个tty驱动,其实只有三类哦!看file_operations,vc/0与console用的
可都是控制台驱动,他们的不同在于.write成员,这个函数可能会被重定向。
比如:
echo hello > /dev/s3c2410_serial0
echo hello > /dev/tty
echo hello > /dev/console
echo hello > /dev/vc/0
上面的这些命令分别在作什么呢?
对于后面三个echo,他们的file_operations分别是tty_fops,console_fops,console_fops。
所以他们的打开函数最终都是tty_open(还没有学伪终端呢),而对于第一个echo,他的打开函数也是
tty_open(),这么找到的呢?
s3c24xx_serial_modinit->uart_register_driver->tty_register_driver()
最后的tty_register_driver有下面的语句
cdev_init(&driver->cdev, &tty_fops);
可见,凡是tty_register_driver()注册的tty设备,他们的file_operations都是tty_fops。

uart核心曾会根据需要,再次注册串口控制台,这里没有
uart_add_one_port

echo hello > /dev/console的操作过程
tty_open()->console_device()
console_device()在printk()中定义,他顺着console_drivers这个控制台驱动链表找到
第一个能够返回具体tty驱动的那个控制台,这里是串口控制台。

7.启动信息是怎么给打印出来的呢?
下面是kernel-2.6.13的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);

    call = __con_initcall_start;
    while (call < __con_initcall_end) {
        (*call)();
        call++;
    }
}

它首先注册了第0个(逻辑上1)线路规程,然后依次调用__con_initcall_start ~ __con_initcall_end之间的
函数指针(找到这两个函数指针的方法为还可以通过init.h头文件中console_initcall的定义):

c001dc30 <__con_initcall_start>:
c001dc30:    c001730c     andgt    r7, r1, ip, lsl #6

c001dc34 <__initcall_s3c24xx_serial_initconsole>:
c001dc34:    c01597d0     ldrgtsb    r9, [r5], -r0

c001dc38 <__con_initcall_end>:

可以找到分别是:
1。con_init()
2。s3c24xx_serial_initconsole()


1。con_init()
con_init()在driver/char/vt.c中,这个函数的描述是
This routine initializes console interrupts, and does nothing else
他只是初始化控制台的中断,其他的什么都不做。虽然他会调用一次
register_console(&vt_console_driver);
但是会因为命令行上的console选项而死掉,也就是说他没有被注册到控制台链表中。

2。s3c24xx_serial_initconsole()
这个函数首先获取正确的
s3c24xx_uart_info 结构,然后调用s3c24xx_serial_init_ports()初始化端口,
初始化端口的主要工作是:完善串口核心层struct uart_port结构,完善s3c24xx_uart_port结构,
(s3c24xx_uart_port结构封装了struct uart_port),使能时钟,复位fifo.
然后注册自己到控制台单向链表.

这个控制台单向链表 是 以console_drivers为首的一系列console结构体单向链表,但是在我的板子上,他链表的
意义不大,这要分析下register_console()函数了

register_console()很有趣,我们知道了先后有两次注册控制台的调用,1.注册虚拟控制台 2.注册实际的串口控制台.
有这么几种情景:
(1)如果在命令行上没有console这样的选项,那么selected_console会一直是-1,con_init()就会将自己注册为
系统默认的控制台,启动信息会打印到/dev/tty(4,0),他相当于系统当前虚拟控制台的 “符号连接”。
(2)如果命令行上有 console=xxx这样的选项,那么处理参数的时候,会将selected_console设置为相应的数值(不再<0)。
这样con_init()就流产了,从而被使用的默认控制台变成了 s3c24xx_serial_initconsole()串口控制台。
    /*
     *    See if this console matches one we selected on
     *    the command line.
     */
通过了上面的这个扫描后,串口控制台就被注册到了console_drivers链表中,这个就是可以在串口上看到启动信息的原因了
在/document/serial-console.txt中,有这样的描述,当然是针对x86的。

console=ttyS1,9600 console=tty0

defines that opening /dev/console will get you the current foreground
virtual console, and kernel messages will appear on both the VGA
console and the 2nd serial port (ttyS1 or COM2) at 9600 baud.

就是说最后传递过去的console=xxx是open()的默认控制台,这个应该是在命令行解析的时候处理的,我没看。
通过console_init和register_console()可以看出来,你只能使用串口或者虚拟终端(键盘+vga),而且
二者在系统启动的时候都可以显示出启动信息(arm下的虚拟控制台被定义为dummy),命令行中最后的console=xxx
会将register_console()使用的 selected_console确定为自己的索引,所以注册控制台的时候,最后的那个
xxx就会被register_console()确定为带有CON_CONSDEV标志,这个标志表征着次控制台应该在控制台链表中
处于首领的位置,就是所谓的默认控制台了,默认控制台意味着open打开时使用本控制台的tty驱动。代码如下:
console->flags |= CON_CONSDEV;
默认控制台意味着open打开时使用本控制台的tty驱动:具体的例子如
sz程序,他打开的就是/dev/console,并且通过这个设备将数据发送到我的pc上,那么可以想见,/dev/console
这个设备使用的肯定是串口的tty驱动,而不是将数据写到vga上。

    .name        = S3C24XX_SERIAL_NAME,
    .setup        = s3c24xx_serial_console_setup
#define S3C24XX_SERIAL_NAME    "ttySAC"

这个名字是我们从命令行中传递过去的ttySAC,不能变的,因为变了,就不能通过名字比对的那个扫描了(register_console()中)
又因为它存在setup()成员函数,所以串口的setup是由
s3c24xx_serial_console_setup()来进行的,这个函数设置了串口的波特率(115200是tag_list给出的,如果没有己使用
默认的9600),8n1,设置termios,等等
然后运行到
release_console_sem();
这个函数就是把__log_buf环形缓冲区的内容放到串口的操作的最上层调用.
具体的写到串口的路径是:
release_console_sem()->call_console_drivers()->_call_console_drivers()->__call_console_drivers()->
console->write()->vt_console_print()->vc->vc_sw->con_putcs()函数

而con_init()->visual_init()中
vc->vc_sw = conswitchp;
把这个虚拟控制台的vc_sw设置为conswitchp.
conswitchp是在setup.c中被设置的.如下
conswitchp = &dummy_con;
这样,由于dummy_con中的函数指针中的con_putcs()函数为空操作,所以虚拟控制台并不能把__log_buf中的内容放到串口上.
但是经过他的写入操作,此时的con_start变成了
con_start = log_end;        /* Flush */
也就是说,没有内容可写了.那么在s3c24xx_serial_initconsole()中,这个真正控制台的注册过程中,怎么打印呢?

这是因为在s3c24xx_serial_console这个控制台结构中的.flags成员被设置为 CON_PRINTBUFFER.
static struct console s3c24xx_serial_console =
{
    .name        = S3C24XX_SERIAL_NAME,
    .device        = uart_console_device,
    .flags        = CON_PRINTBUFFER,  //这里
    .index        = -1,
    .write        = s3c24xx_serial_console_write,
    .setup        = s3c24xx_serial_console_setup
};

而在register_console()中:

    if (console->flags & CON_PRINTBUFFER) {
        /*
         * release_console_sem() will print out the buffered messages
         * for us.
         */
        spin_lock_irqsave(&logbuf_lock, flags);
        con_start = log_start;
        spin_unlock_irqrestore(&logbuf_lock, flags);
    }
    release_console_sem();

在release_console_sem()之前,先要判断这个控制台结构中的flags成员有没有CON_PRINTBUFFER标志,
如果有的话,把con_start这个指示控制台打印起始位置的指针
static unsigned long con_start;    /* Index into log_buf: next char to be sent to consoles */
从新改成log_start,这样,从系统开始的第一个prink()打印出的内容就被放到了串口上了.

关于__log_buf
CONFIG_LOG_BUF_SHIFT=14
上面的是配置文件中设置的,通过改变这个数值,可以改变__log_buf的大小,这里是16k.
#define __LOG_BUF_LEN    (1 << CONFIG_LOG_BUF_SHIFT)
static char __log_buf[__LOG_BUF_LEN];
static char *log_buf = __log_buf;
static int log_buf_len = __LOG_BUF_LEN;
static unsigned long log_start;    /* Index into log_buf: next char to be read by syslog() */
static unsigned long con_start;    /* Index into log_buf: next char to be sent to consoles */
static unsigned long log_end;    /* Index into log_buf: most-recently-written-char + 1 */

int main(int argc, char **argv)
{
//    close(0);
    close(1);
//    close(2);
    printf("ok?");
}
关闭标准输出,ok是不会被打印出来的

lzd@lzd-laptop:~$ ps -ef|grep tty
root      4518     1  0 09:54 tty4     00:00:00 /sbin/getty 38400 tty4
root      4519     1  0 09:54 tty5     00:00:00 /sbin/getty 38400 tty5
root      4521     1  0 09:54 tty2     00:00:00 /sbin/getty 38400 tty2
root      4522     1  0 09:54 tty3     00:00:00 /sbin/getty 38400 tty3
root      4523     1  0 09:54 tty6     00:00:00 /sbin/getty 38400 tty6
root      5367  5363  3 09:54 tty7     00:06:30 /usr/bin/X :0 -br -audit 0 -auth /var/lib/gdm/:0.Xauth -nolisten tcp vt7
root     14105     1  0 12:34 tty1     00:00:00 /sbin/getty 38400 tty1
lzd      14725  6063  0 12:46 pts/0    00:00:00 grep tty





阅读(7822) | 评论(0) | 转发(1) |
0

上一篇:按键驱动学习

下一篇:从init到shell

给主人留下些什么吧!~~