Linux 0.11 字符设备的使用

Linux 0.11字符设备的使用一、概述

本文自顶向下一步步探索字符设备的读写是怎么完成的。通常我们在Linux应用程序中用open、read、write对各种类型的文件进行操作。我们可以从键盘输入,然后命令行窗口会显示你的输入,有输出的话则命令行窗口会显示输出。为什么所有的设备在Linux中都被看成是一个个文件,可以通过统一的read、write直接进行读写?文件句柄与终端设备有什么关联?为什么Linux允许多个控制终端登录?tty又是什么东西?读写时将发生哪些硬件中断,驱动程序是怎么回事?微型计算机原理与接口技术中的串口在Linux是怎么用的?对于这些疑问,本文将通过Linux 0.11版本的源码找到解答!

二、上层接口2.1、sys_open、sys_read、sys_write源码及分析

在fs/open.c(p310,第138行)中,给出了sys_open这个系统调用的具体实现

int sys_open(const char *filename,int flag,int mode){    struct m_inode * inode;    struct file * f;    int i,fd;    mode &= 0777 &~current->umask;    for(fd=0 ; fd<NR_OPEN ; fd++)        if(!current->filp[fd])            break;    if (fd>=NR_OPEN)        return -EINVAL;    current->close_on_exec&= ~(1<<fd);    f=0+file_table;    for (i=0 ; i<NR_FILE ; i++,f++)        if (!f->f_count)break;    if (i>=NR_FILE)        return -EINVAL;    (current->filp[fd]=f)->f_count++;    if((i=open_namei(filename,flag,mode,&inode))<0)    {        current->filp[fd]=NULL;        f->f_count=0;        return i;    }    /* ttys are somewhatspecial (ttyxx major==4, tty major==5) */    if(S_ISCHR(inode->i_mode))    {        if(MAJOR(inode->i_zone[0])==4)        {            if(current->leader && current->tty<0)            {                current->tty= MINOR(inode->i_zone[0]);                tty_table[current->tty].pgrp= current->pgrp;            }        }        else if(MAJOR(inode->i_zone[0])==5)            if(current->tty<0)            {                iput(inode);                current->filp[fd]=NULL;                f->f_count=0;                return -EPERM;            }    }    /* Likewise withblock-devices: check for floppy_change */    if(S_ISBLK(inode->i_mode))        check_disk_change(inode->i_zone[0]);    f->f_mode =inode->i_mode;    f->f_flags = flag;    f->f_count = 1;    f->f_inode = inode;    f->f_pos = 0;    return (fd);}

sys_open首先查看当前进程的文件指针数组(NR_OPEN=20,include/linux/fs.h,p395,第43行),看是否有未使用的文件句柄fd,然后将句柄设置为在加载新的程序文件时不关闭,即可以在两个进程共享。

接着遍历全局文件结构表file_table(NR_FILE=64,include/linux/fs.h,p395,第45行),检查占用次数是否为零(因为file_table已经在内核的数据区中,不用再申请空间,这里检查的是count是否为零,而不是是否为NULL,task_struct是指针数组,检查该项是否为NULL确定是否被占用),找到后用当前句柄对其进行关联,引用计数加一(最后是一)。然后使用open_namei找到该打开文件的inode。对于字符设备文件,如果是串口设备且是在没有控制终端的会话领导进程打开,则设置当前进程的控制终端为串口次设备号(主串口或者辅串口),串口的前台进程组为当前的进程的进程组号。如果要打开控制台(键盘和显示屏),但当前进程没有控制终端(current->tty=-1),则不能打开控制终端设备文件。另外该函数还能处理块设备文件。最后关联文件指针和inode节点,返回文件句柄(其实就是当前进程文件指针数组的下标)。

从这里可以看出,一个进程最多可以打开20个文件,而一个系统最多可以打开64个文件,每个进程的每一个文件指针都要消耗全局进程表的一项。一个设备文件节点的核心之处在于inode->i_zone[0],也就是字符设备号。内核通过设备号定位具体的设备,对该设备进行读写。

在fs/read_write.c中(p304,第55行)实现了sys_read和sys_write两个系统调用:

int sys_read(unsigned int fd,char * buf,int count){    struct file * file;    struct m_inode * inode;    if (fd>=NR_OPEN ||count<0 || !(file=current->filp[fd]))        return -EINVAL;    if (!count)        return 0;    verify_area(buf,count);    inode = file->f_inode;    if (inode->i_pipe)        return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;    if(S_ISCHR(inode->i_mode))        return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);    if(S_ISBLK(inode->i_mode))        return block_read(inode->i_zone[0],&file->f_pos,buf,count);    if (S_ISDIR(inode->i_mode)|| S_ISREG(inode->i_mode))    {        if (count+file->f_pos> inode->i_size)            count =inode->i_size - file->f_pos;        if (count<=0)            return 0;        return file_read(inode,file,buf,count);    }    printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);    return -EINVAL;}int sys_write(unsigned int fd,char * buf,int count){    struct file * file;    struct m_inode * inode;    if (fd>=NR_OPEN ||count <0 || !(file=current->filp[fd]))        return -EINVAL;    if (!count)        return 0;    inode=file->f_inode;    if (inode->i_pipe)        return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;    if(S_ISCHR(inode->i_mode))        return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);    if(S_ISBLK(inode->i_mode))        return block_write(inode->i_zone[0],&file->f_pos,buf,count);    if(S_ISREG(inode->i_mode))        return file_write(inode,file,buf,count);    printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);    return -EINVAL;}

首先利用fd获得当前进程的file指针,然后获得对应的inode。文件类型有字符设备文件、块设备文件、目录文件、普通文件和匿名管道,这里根据inode->i_mode进行确定,然后调用具体的文件操作函数。所以辨别文件类型是通过inode->i_mode,而却像一个大文件一样读写(拥有文件读取位置)。这也就是为什么所有的文件都可以用read和write来读写,且只需传递fd即可。将字符设备号(保存在字符设备节点inode->i_zone[0])传递给rw_char函数,而一个文件指针的作用仅是保存文件的当前位置(file->f_pos)。值得注意的是文件的当前位置对字符设备来说没有作用。

2.2、rw_char源码及分析

rw_char位于fs/char_dev.c(p303,第95行)中:

int rw_char(int rw,int dev,char * buf, int count, off_t * pos){    crw_ptr call_addr;    if (MAJOR(dev)>=NRDEVS)        return -ENODEV;    if(!(call_addr=crw_table[MAJOR(dev)]))        return -ENODEV;    return call_addr(rw,MINOR(dev),buf,count,pos);}

而crw_ptr是一个函数指针数组:

typedef int (*crw_ptr)(intrw,unsigned minor,char * buf,int count,off_t * pos);static crw_ptr crw_table[]={    NULL, /* nodev */    rw_memory, /* /dev/mem etc */    NULL, /* /dev/fd */    NULL, /* /dev/hd */    rw_ttyx, /* /dev/ttyx */    rw_tty, /* /dev/tty */    NULL, /* /dev/lp */    NULL}; /* unnamed pipes */

上述函数以主设备号为数组下标,将次设备号作为参数,调用对应的设备函数。注意一种设备只有一个主设备号,而同一种设备数量可以有多个,对应的便是多个次设备号。上述串口主设备号是4,调用的函数是rw_ttyx。控制终端的主设备号是5,调用的函数是rw_tty。

这两个函数在也在文件fs/char_dev.c(p301,第21行)中:

static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos){    return ((rw==READ)?tty_read(minor,buf,count) : tty_write(minor,buf,count));}static int rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos){    if (current->tty<0)        return -EPERM;    return rw_ttyx(rw,current->tty,buf,count,pos);}

从上面可以看出不管是串口还是控制台终端,实际调用的函数是tty_read和tty_write,传递的都是次设备号,且文件位置pos不起作用。只不过控制台终端要求进程必须有控制终端,传进来的minor次设备号被忽略,使用当前进程的控制终端代替(current->tty)。注意rw_char操作的是设备号,而不是inode。

2.3 上层接口结构图

三、操作tty设备3.1、tty_read和tty_write源码及分析

这两个函数位于linux/kernel/chr_drv/tty_io.c(p216,第230行)中:

int tty_read(unsigned channel,char * buf, int nr){    struct tty_struct * tty;    char c, * b=buf;    int minimum,time,flag=0;    long oldalarm;    if (channel>2 || nr<0)return -1;    tty = &tty_table[channel];    oldalarm = current->alarm;    time =10L*tty->termios.c_cc[VTIME];    minimum =tty->termios.c_cc[VMIN];    if (time && !minimum)    {        minimum=1;        if ((flag=(!oldalarm ||time+jiffies<oldalarm)))            current->alarm =time+jiffies;    }    if (minimum>nr)        minimum=nr;    while (nr>0)    {        if (flag &&(current->signal & ALRMMASK))        {            current->signal &=~ALRMMASK;            break;        }        if (current->signal)            break;        if (EMPTY(tty->secondary)|| (L_CANON(tty) &&                                     !tty->secondary.data &&LEFT(tty->secondary)>20))        {            sleep_if_empty(&tty->secondary);            continue;        }        do        {            GETCH(tty->secondary,c);            if (c==EOF_CHAR(tty) ||c==10)                tty->secondary.data--;            if (c==EOF_CHAR(tty) &&L_CANON(tty))                return (b-buf);            else            {                put_fs_byte(c,b++);                if (!--nr)                    break;            }        }while (nr>0 &&!EMPTY(tty->secondary));        if (time &&!L_CANON(tty))        {            if ((flag=(!oldalarm ||time+jiffies<oldalarm)))                current->alarm =time+jiffies;            else                current->alarm =oldalarm;        }        if (L_CANON(tty))        {            if(b-buf)                break;        }        else if (b-buf >= minimum)            break;    }    current->alarm = oldalarm;    if (current->signal &&!(b-buf))        return -EINTR;    return (b-buf);}int tty_write(unsigned channel, char * buf, int nr){    static int cr_flag=0;    struct tty_struct * tty;    char c, *b=buf;    if (channel>2 || nr<0)return -1;    tty = channel + tty_table;    while (nr>0)    {        sleep_if_full(&tty->write_q);        if (current->signal)            break;        while (nr>0 &&!FULL(tty->write_q))        {            c=get_fs_byte(b);            if (O_POST(tty))            {                if (c=='\r' &&O_CRNL(tty))                    c='\n';                else if (c=='\n' &&O_NLRET(tty))                    c='\r';                if (c=='\n' &&!cr_flag && O_NLCR(tty))                {                    cr_flag = 1;                    PUTCH(13,tty->write_q);                    continue;                }                if (O_LCUC(tty))                    c=toupper(c);            }            b++;            nr--;            cr_flag = 0;            PUTCH(c,tty->write_q);        }        tty->write(tty);        if (nr>0)            schedule();    }    return (b-buf);}

从上面可知,传递过来的次设备号被用来索引tty_table这个数组,进而获得对应的tty设备的内核数据结构。对于tty_read,从tty->secondary获取数据,写到用户态的buf中,当tty->secondary队列为空,或者没有EOF和换行符且字符太少时,当前进程都会进入可中断的休眠状态;对于tty_write,从用户态的buf写数据到tty->write_q,并调用tty->write(tty),表示将数据立即显示或者提醒串口输出数据。

tty_table这个数组已经占用了内核的数据段内存,内核中有很多已经定义好的固定长度的数组,如request数组,inode数组等。tty_table定义在kernel/chr_drv/tty_io.c(p217,第51行)中:

struct tty_struct tty_table[]={    {        {            ICRNL, /* change incomingCR to NL */            OPOST|ONLCR, /* changeoutgoing NL to CRNL */            0,            ISIG |ICANON | ECHO| ECHOCTL | ECHOKE,            0, /* console termio */            INIT_C_CC        },        0, /* initial pgrp */        0, /* initial stopped */        con_write,        {0,0,0,0,""}, /*console read-queue */        {0,0,0,0,""}, /*console write-queue */        {0,0,0,0,""} /*console secondary queue */    },{        {            0, /* no translation */            0, /* no translation */            B2400 | CS8,            0,            0,            INIT_C_CC        },        0,        0,        rs_write,        {0x3f8,0,0,0,""}, /*rs 1 */        {0x3f8,0,0,0,""},        {0,0,0,0,""}    },{        {            0, /* no translation */            0, /* no translation */            B2400 | CS8,            0,            0,            INIT_C_CC        },        0,        0,        rs_write,        {0x2f8,0,0,0,""}, /*rs 2 */        {0x2f8,0,0,0,""},        {0,0,0,0,""}    }};

每个tty设备占用一项tty_struct,上面第一项是控制台(键盘和显示屏),第二项是主串口(com1),第三项是辅串口(com2)。

tty_struct定义在include/linux/tty.h(p409,第45行):

struct tty_struct{    struct termios termios;    int pgrp;    int stopped;    void (*write)(structtty_struct * tty);    struct tty_queue read_q;    struct tty_queue write_q;    struct tty_queue secondary;};

其中termios位于include/termios.h(p374,第53行)

#define NCCS 17struct termios{    unsigned long c_iflag; /*input mode flags */    unsigned long c_oflag; /*output mode flags */    unsigned long c_cflag; /*control mode flags */    unsigned long c_lflag; /*local mode flags */    unsigned char c_line; /*line discipline */    unsigned char c_cc[NCCS]; /*control characters */};

这里主要存放字符设备的标志,且每个标志占用一个比特,这些标志将影响对读入数据的解释。尤其要注意的是本地模式标志,设置ICANON可以启用规范模式。

pgrp是一个前台进程组号,而write是一个函数指针。tty_write函数每次将用户态的数据写往write_q,并调用tty->write(tty)。对于控制台,这个函数是con_write,取走write_q中的数据到显存里,在显示屏显示。对于串口,这个函数是rs_write,提醒串口有数据可以写了,等待写到数据口发送出去。这里有点类似面向对象中的多态。

tty_queue(在p409,第14行)是个存放数据的循环队列。

#define TTY_BUF_SIZE 1024struct tty_queue{    unsigned long data;    unsigned long head;    unsigned long tail;    struct task_struct *proc_list;    char buf[TTY_BUF_SIZE];};

read_q是由中断程序操作的。串口或者键盘有数据到达时,就会有产生中断,然后保存到read_q中。read_q中的数据是原始数据,中断时还会调用copy_to_cooked,将其做进一步的处理,并将处理过的数据保存在secondary辅助队列中。从上面tty_read中可以看到tty_read读取的实际是secondary队列中的数据,也就是经过处理的数据。另外,从上面tty_table数组的初始化可以看出,串口read_q和write_q的data都是数据口的地址,而secondary的data是secondary中数据的行数。

尤其注意proc_list。对于读进程,当secondary没有数据时,将当前进程设置为可中断休眠,当数据到达时(由copy_to_cooked唤醒)会将进程设置为可运行状态。对于写进程,当write_q满时,将当前进程设置为可中断休眠,当write_q全部写完时(由串口中write_char子例程唤醒)会将进程设置为可运行状态。

3.2、con_write和rs_write源码及分析

con_write位于kernel/chr_dev/console.c(p201,第445行)中,这个函数可以说是显卡的驱动程序:

void con_write(struct tty_struct * tty){    int nr;    char c;    nr = CHARS(tty->write_q);    while (nr--)    {        GETCH(tty->write_q,c);        switch(state)        {        case 0:            if (c>31 &&c<127)            {                if(x>=video_num_columns)                {                    x-= video_num_columns;                    pos-= video_size_row;                    lf();                }                __asm__("movb attr,%%ah\n\t"                        "movw %%ax,%1\n\t"                        ::"a"(c),"m" (*(short *)pos)                       );                pos+= 2;                x++;            }            else if (c==27)                state=1;            else if (c==10 || c==11 ||c==12)                lf();            else if (c==13)                cr();            else if(c==ERASE_CHAR(tty))                del();            else if (c==8)            {                if (x)                {                    x--;                    pos -= 2;                }            }            else if (c==9)            {                c=8-(x&7);                x += c;                pos += c<<1;                if (x>video_num_columns)                {                    x -= video_num_columns;                    pos -= video_size_row;                    lf();                }                c=9;            }            else if (c==7)                sysbeep();            break;        case 1:            state=0;            if (c=='[')                state=2;            else if (c=='E')                gotoxy(0,y+1);            else if (c=='M')                ri();            else if (c=='D')                lf();            else if (c=='Z')                respond(tty);            else if (x=='7')                save_cur();            else if (x=='8')                restore_cur();            break;        case 2:            for(npar=0; npar<NPAR; npar++)                par[npar]=0;            npar=0;            state=3;            if ((ques=(c=='?')))                break;        case 3:            if (c==';' &&npar<NPAR-1)            {                npar++;                break;            }            else if (c>='0' &&c<='9')            {                par[npar]=10*par[npar]+c-'0';                break;            }            else state=4;        case 4:            state=0;            switch(c)            {            case 'G':            case '`':                if (par[0]) par[0]--;                gotoxy(par[0],y);                break;            case 'A':                if (!par[0]) par[0]++;                gotoxy(x,y-par[0]);                break;            case 'B':            case 'e':                if (!par[0]) par[0]++;                gotoxy(x,y+par[0]);                break;            case 'C':            case 'a':                if (!par[0]) par[0]++;                gotoxy(x+par[0],y);                break;            case 'D':                if (!par[0]) par[0]++;                gotoxy(x-par[0],y);                break;            case 'E':                if (!par[0]) par[0]++;                gotoxy(0,y+par[0]);                break;            case 'F':                if (!par[0]) par[0]++;                gotoxy(0,y-par[0]);                break;            case 'd':                if (par[0]) par[0]--;                gotoxy(x,par[0]);                break;            case 'H':            case 'f':                if (par[0]) par[0]--;                if (par[1]) par[1]--;                gotoxy(par[1],par[0]);                break;            case 'J':                csi_J(par[0]);                break;            case 'K':                csi_K(par[0]);                break;            case 'L':                csi_L(par[0]);                break;            case 'M':                csi_M(par[0]);                break;            case 'P':                csi_P(par[0]);                break;            case '@':                csi_at(par[0]);                break;            case 'm':                csi_m();                break;            case 'r':                if (par[0]) par[0]--;                if (!par[1]) par[1] =video_num_lines;                if (par[0] < par[1]&&                        par[1] <=video_num_lines)                {                    top=par[0];                    bottom=par[1];                }                break;            case 's':                save_cur();                break;            case 'u':                restore_cur();                break;            }        }    }    set_cursor();}

con_write这个函数从write_q中获取一个字符,如果ASCII位于32–126之间,也就是可以显示的字符,直接显示字符即可(可能要换行,因为屏幕一般是25行,80列,同时还要注意设置字符的属性,也就是前景和背景的颜色等)。对于ASCII码0– 31, 127其实是控制字符,必须进行特殊处理。如\n= 10表示换行调到下一行的相同位置,\r = 13表示回车回到当前行的开头,BEL= 7表示扬声器发声,8表示退格符删除左边一个字符,\t= 9向下个8的整数倍的位置移动光标。控制序列(CSI)以ESC(ASCII=27)开头,如ESC[7m]是将字符显示为白底黑字(反显)。代码中的case1-case4都是在处理以ESC开头的控制序列。例子:write(fd,“hello\tworld!\n”, 20)。

上面的gotoxy在(kernel/chr_drv/console.c,p193,第88行)中:

/* NOTE! gotoxy thinksx==video_num_columns is ok */static inline void gotoxy(unsigned int new_x,unsigned int new_y){    if (new_x >video_num_columns || new_y >= video_num_lines)        return;    x=new_x;    y=new_y;    pos=origin + y*video_size_row+ (x<<1);}

其中video_num_columns= 80, video_num_lines = 25,表示一个屏幕的大小25行x80列,而且是以字符为单位的。这里的字符要占用两个字节,低字节用于设置字符的ASCII,高字节用于设置字符属性,最多可以显示2000个字符。而video_size_row表示一行占用的字节数,video_size_row= 160。

这里的origin是显示屏显示区域的起始地址,而且是个虚拟地址。而x,y是坐标,0<=x<=80,0<=y<25。pos是当前光标的虚拟地址,不过它是针对0xB8000而言的。对于一个屏幕,有两个地址需要设置。一个是显示屏起始地址origin,但寄存器是个16位的(分为两个8位寄存器,下同),所以填的是origin – 0xB8000。另一个地址是当前光标的位置pos,寄存器也是16位的,所以填的是pos – 0xB8000。

注意:显示屏的坐标与通常的坐标不一样,这里的坐标原点在左上角,与Java Swing中的界面的坐标语义类似。如下图:

上述的原点是其实就是origin。我们可以通过改变origin,也就是改变起始地址来改变显示的内存区域,实现滚屏的效果。事实上,显存是非常大的,通常是0xB8000–0xBFFFF,而显示屏显示的只是显存的冰山一角,这里把显存单独作为一个tty设备了。其实可以把显存划分为几块,只有一个键盘输入,对应设置多个tty,这样也就有了多个互不干扰的控制终端。通过按键Ctrl+Alt + F1-F7,分别进入不同的tty设备,设置该设备对应的显示屏地址和光标当前位置,实现多用户登录的功能。把内容写到当前光标位置pos(已经是指针),若落在当前[origin,src_end)里面就可以在屏幕看到该字符。src_end= origin + 4000。

另外,set_origin位于第97行:

static inline void set_origin(void){    cli();    outb_p(12, video_port_reg);    outb_p(0xff&((origin-video_mem_start)>>9),video_port_val);    outb_p(13, video_port_reg);    outb_p(0xff&((origin-video_mem_start)>>1),video_port_val);    sti();}

set_cursor位于第313行:

static inline void set_cursor(void){    cli();    outb_p(14, video_port_reg);    outb_p(0xff&((pos-video_mem_start)>>9),video_port_val);    outb_p(15, video_port_reg);    outb_p(0xff&((pos-video_mem_start)>>1),video_port_val);    sti();}

上面video_mem_start= 0xB8000,video_port_reg= 0x3B4,video_port_val= 0x3B5。

0xB8000是显存的起始地址,0x3B4是显存的索引寄存器,由于显卡端口众多,要访问各个数据寄存器,首先应该向端口0x3B4写入索引,表示接下来的数据由该索引对应的寄存器来接收。可以填写0-17,也就是最多可以索引17个寄存器。选择相应的索引后,通过0x3B5向该索引对应的寄存器写入数据,是8位寄存器。

12和13分别用于索引显示屏起始地址的高8位和低8位。14和15分别用于索引显示屏光标地址的高8位和低8位。注意这里都是以字符为单位,需要除以2。

rs_write位于kernel/chr_drv/serial.c(p211,第53行)中:

/** This routine gets calledwhen tty_write has put something into* the write_queue. It mustcheck wheter the queue is empty, and* set the interrupt registeraccordingly** void _rs_write(structtty_struct * tty);*/void rs_write(struct tty_struct * tty){    cli();    if (!EMPTY(tty->write_q))        outb(inb_p(tty->write_q.data+1)|0x02,tty->write_q.data+1);    sti();}

这个函数主要是在write_q有数据的情况下,将四个中断允许位中的写中断允许位(位1)置位。这个中断允许寄存器是0x3F9(主串口)或者0x2F9(辅串口)。这样的话,以后串口准备好时,就会自动把数据写到数据口中(0x3F8或者0x2F8)。

四、驱动程序4.1、键盘输入的驱动程序

通过前面的讨论,我们已经知道了将数据写到显存中,就可以在显示屏显示数据,但依旧不知道这些数据是怎么获取到的,或者说键盘的输入是怎么处理的,如何读取串口中的数据。聪明的读者不难发现,tty_read读取的数据其实是保存在secondary辅助队列中的,那么secondary这个队列中的数据是怎么来的呢?是通过中断例程自动获取的。每次有数据到达,就会产生中断,如键盘中断IRQ1(33号中断)。串口1(主串口)的中断IRQ4是36号中断,串口2(辅串口)的中断IRQ3是35号中断。

键盘的中断入口在con_init(kernel/chr_drv/console.c,p207,第683行)这个函数中设置:

set_trap_gate(0x21,&keyboard_interrupt);outb_p(inb_p(0x21)&0xfd,0x21);

这里设置的是一个陷阱门,键盘中断时其他中断会被自动关闭。也就是在执行键盘中断例程时不允许其他中断的执行。

串口中断的入口绑定在rs_init(kernel/chr_drv/serial.c,p211,第37行)这个函数中设置:

void rs_init(void){    set_intr_gate(0x24,rs1_interrupt);    set_intr_gate(0x23,rs2_interrupt);    init(tty_table[1].read_q.data);    init(tty_table[2].read_q.data);    outb(inb_p(0x21)&0xE7,0x21);}

这两个函数最后都向8259A发送中断允许控制字。

先来看看keyboard_interrupt(kernel/chr_drv/keyboard.S,p178)这个汇编函数:

/*

* con_int is the realinterrupt routine that reads the

* keyboard scan-code andconverts it into the appropriate

* ascii character(s).

*/

keyboard_interrupt:

pushl %eax

pushl %ebx

pushl %ecx

pushl %edx

push %ds

push %es

movl $0x10,%eax

mov %ax,%ds

mov %ax,%es

xor %al,%al /* %eax is scancode */

inb $0x60,%al

cmpb $0xe0,%al

je set_e0

cmpb $0xe1,%al

je set_e1

call key_table(,%eax,4)

movb $0,e0

e0_e1: inb $0x61,%al

jmp 1f

1: jmp 1f

1: orb $0x80,%al

jmp 1f

1: jmp 1f

1: outb %al,$0x61

jmp 1f

1: jmp 1f

1: andb $0x7F,%al

outb %al,$0x61

movb $0x20,%al

outb %al,$0x20

pushl $0

call do_tty_interrupt

addl $4,%esp

pop %es

pop %ds

popl %edx

popl %ecx

popl %ebx

popl %eax

iret

set_e0: movb $1,e0

jmp e0_e1

set_e1: movb $2,e0

jmp e0_e1

键盘某个键按下时会产生make扫描码,松开时会产生break扫描码。对于同一个按键,这两个码是有关系的,就是make码的最高位置1则是break码,这样刚好有256个扫描码。大部分按键产生的扫描码只有一个字节,但少数几个按键有两个字节,如RCtrl键make扫描码有两个字节,第一个是0xE0,而Pause键make有6个字节,且第一个是0xE1。通常我们只在乎make码,也就是按下的码。

从上面的函数可以看出,键盘的数据口是0x60。先从数据口读取数据,然后调用以扫描码为下标的key_table数组中的函数。调用完成后则会操作0x61端口先禁止键盘,再允许键盘,以对收到扫描码做出应答。最后会调用do_tty_interrupt(0),对数据进行处理,并填到secondary队列中。

其中key_table位于同一个文件的第502行,调用的函数大都是do_self。我们也可以看到索引128以上大部分是调用none,也就是忽略。其他处理函数则是对mode的比特位进行相应设置,如左shift键按下,则mode的最低位置一,松开则置零。

我们可以看一下do_self(第453行),传递的寄存器参数是EAX扫描码:

/*

* do_self handles "normal"keys, ie keys that don’t change meaning

* and which have just onecharacter returns.

*/

do_self:

lea alt_map,%ebx

testb $0x20,mode /* alt-gr*/

jne 1f

lea shift_map,%ebx

testb $0x03,mode

jne 1f

lea key_map,%ebx

1: movb (%ebx,%eax),%al

orb %al,%al

je none

testb $0x4c,mode /* ctrl or caps */

je 2f

cmpb $’a,%al

jb 2f

cmpb $’},%al

ja 2f

subb $32,%al

2: testb $0x0c,mode /* ctrl */

je 3f

cmpb $64,%al

jb 3f

cmpb $64+32,%al

jae 3f

subb $64,%al

3: testb $0x10,mode /* left alt */

je 4f

orb $0x80,%al

4: andl $0xff,%eax

xorl %ebx,%ebx

call put_queue

none: ret

do_self主要是通过看mode这个字节的比特位,看是否有Alt或者Shift键按下(按下不放),进而选择对应的映射表(alt_map或shift_map),否则就选择普通的key_map数组。这三个数组已经在内核代码中,且已经初始化,表示的是该键产生的扫描码对应的ASCII码,但是有的键是没有ASCII码的,用零表示,直接返回。

上面所有标出颜色的部分都等价于一个if语句,共有3个连续的if语句,满足条件则执行。第一个加粗部分获得对应的ASCII码,并存放在AL中。如果该ASCII码位于[97,125]且Caps键或Ctrl键按下,则减去32转化为大写字母。这里假设前面用的是key_map这个数组,而且都是小写字母的ASCII值,才能减去32。

接着,如果Ctrl键按下且ASCII码位于[64,96)(这个区间的大部分字符是大写字符,与上面是Ctrl键置位时是对应的,也就是Ctrl按下不放时,减去96,Ctrl+ a→1,Ctrl + b→2,…,Ctrl + z → 26),则再减去64,即转化为[0,31](控制字符范围)的ASCII码,还是存放在AL中。如果左边的Alt按下,则AL的最高位置1。

将EAX= AL(高位补零,ASCII码)和EBX= 0这两个参数传递给put_queue这个函数处理。

put_queue这个函数在第88行:

/*

* This routine fills thebuffer with max 8 bytes, taken from

* %ebx:%eax. (%edx is high).The bytes are written in the

* order%al,%ah,%eal,%eah,%bl,%bh … until %eax is zero.

*/

put_queue:

pushl %ecx

pushl %edx

movl table_list,%edx #read-queue for console

movl head(%edx),%ecx

1: movb %al,buf(%edx,%ecx)

incl %ecx

andl $size-1,%ecx

cmpl tail(%edx),%ecx #buffer full – discard everything

je 3f

shrdl $8,%ebx,%eax# EBX = 0,直接跳到2处执行

je 2f

shrl $8,%ebx

jmp 1b

2: movl %ecx,head(%edx)

movl proc_list(%edx),%ecx

testl %ecx,%ecx

je 3f

movl $0,(%ecx)

3: popl %edx

popl %ecx

ret

上面用到了table_list这个数组,它位于kernel/chr_drv/tty_io.c(p218)第99行:

/** these are the tables usedby the machine code handlers.* you can implementpseudo-tty's or something by changing* them. Currently not done.*/struct tty_queue *table_list[]={    &tty_table[0].read_q,&tty_table[0].write_q,    &tty_table[1].read_q,&tty_table[1].write_q,    &tty_table[2].read_q,&tty_table[2].write_q};

上面加粗的部分使得EDX获得控制台终端读队列(tty_table[0].read_q)的地址,进而将一个字符AL写入到队头中,并将队头往前移位。需要注意的是队头一开始指向的位置为空,可以填充数据。而且这里使用的是循环队列,tail== head表示空,head+ 1 == tail表示队列已经满了。这里是在head这个位置先填数据了,再判断是否满了。

队列中缓冲区的数据存储如下:

key_table对应的函数处理完之后,键盘中断例程还要执行do_tty_interrupt(0)。这个函数位于kernel/chr_drv/tty_io.c(p224, 342行):

/** Jeh, sometimes I reallylike the 386.* This routine is called froman interrupt,* and there should beabsolutely no problem* with sleeping even in aninterrupt (I hope).* Of course, if somebodyproves me wrong, I'll* hate intel for all time:-). We'll have to* be careful and see toreinstating the interrupt* chips before calling this,though.** I don't think we sleep hereunder normal circumstances* anyway, which is good, asthe task sleeping might be* totally innocent.*/void do_tty_interrupt(int tty){    copy_to_cooked(tty_table+tty);}

而copy_to_cooked也在这个文件的第145行:

void copy_to_cooked(struct tty_struct * tty){    signed char c;    while (!EMPTY(tty->read_q)&& !FULL(tty->secondary))    {        GETCH(tty->read_q,c);        if (c==13)            if (I_CRNL(tty))                c=10;            else if (I_NOCR(tty))                continue;            else ;        else if (c==10 &&I_NLCR(tty))            c=13;        if (I_UCLC(tty))            c=tolower(c);        if (L_CANON(tty))        {            if (c==KILL_CHAR(tty))            {                /* deal with killing theinput line */                while(!(EMPTY(tty->secondary)||                        (c=LAST(tty->secondary))==10 ||                        c==EOF_CHAR(tty)))                {                    if (L_ECHO(tty))                    {                        if (c<32)                            PUTCH(127,tty->write_q);                        PUTCH(127,tty->write_q);                        tty->write(tty);                    }                    DEC(tty->secondary.head);                }                continue;            }            if (c==ERASE_CHAR(tty))            {                if (EMPTY(tty->secondary)||                        (c=LAST(tty->secondary))==10 ||                        c==EOF_CHAR(tty))                    continue;                if (L_ECHO(tty))                {                    if (c<32)                        PUTCH(127,tty->write_q);                    PUTCH(127,tty->write_q);                    tty->write(tty);                }                DEC(tty->secondary.head);                continue;            }            if (c==STOP_CHAR(tty))            {                tty->stopped=1;                continue;            }            if (c==START_CHAR(tty))            {                tty->stopped=0;                continue;            }        }        if (L_ISIG(tty))        {            if (c==INTR_CHAR(tty))            {                tty_intr(tty,INTMASK);                continue;            }            if (c==QUIT_CHAR(tty))            {                tty_intr(tty,QUITMASK);                continue;            }        }        if (c==10 ||c==EOF_CHAR(tty))            tty->secondary.data++;        if (L_ECHO(tty))        {            if (c==10)            {                PUTCH(10,tty->write_q);                PUTCH(13,tty->write_q);            }            else if (c<32)            {                if (L_ECHOCTL(tty))                {                    PUTCH('^',tty->write_q);                    PUTCH(c+64,tty->write_q);                }            }            else                PUTCH(c,tty->write_q);            tty->write(tty);        }        PUTCH(c,tty->secondary);    }    wake_up(&tty->secondary.proc_list);}

这个函数是实现行规则的关键,主要是对read_q进行遍历,如果是普通字符,则直接复制到tty->secondary中就可以了。如果设置了ICANON标志且当前字符是特殊字符,则对secondary进行处理。如果允许处理信号,则根据控制字符给相关的前台进程组发送对应的信号。同时根据标志,还能回显和控制回显等。

首先来了解EOF_CHAR(tty)的具体含义。在include/linux/tty.h(p410)中定义了:

#define INC(a) ((a) = ((a)+1)& (TTY_BUF_SIZE-1))#define DEC(a) ((a) = ((a)-1)& (TTY_BUF_SIZE-1))#define EMPTY(a) ((a).head ==(a).tail)#define LEFT(a)(((a).tail-(a).head-1)&(TTY_BUF_SIZE-1))#define LAST(a)((a).buf[(TTY_BUF_SIZE-1)&((a).head-1)])#define FULL(a) (!LEFT(a))#define CHARS(a)(((a).head-(a).tail)&(TTY_BUF_SIZE-1))#define GETCH(queue,c) \(void)({c=(queue).buf[(queue).tail];INC((queue).tail);})#define PUTCH(c,queue) \(void)({(queue).buf[(queue).head]=(c);INC((queue).head);})#define INTR_CHAR(tty)((tty)->termios.c_cc[VINTR])#define QUIT_CHAR(tty)((tty)->termios.c_cc[VQUIT])#define ERASE_CHAR(tty)((tty)->termios.c_cc[VERASE])#define KILL_CHAR(tty)((tty)->termios.c_cc[VKILL])#define EOF_CHAR(tty) ((tty)->termios.c_cc[VEOF])#define START_CHAR(tty)((tty)->termios.c_cc[VSTART])#define STOP_CHAR(tty)((tty)->termios.c_cc[VSTOP])#define SUSPEND_CHAR(tty)((tty)->termios.c_cc[VSUSP])/* intr=^C quit=^\ erase=del kill=^Ueof=^D vtime=\0 vmin=\1 sxtc=\0start=^Q stp=^S susp=^Z eol=\0reprint=^R discard=^U werase=^W lnext=^Veol2=\0*/#define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0"

在上面定义的tty_table中,使用INIT_C_CC这个数组初始化tty的termios结构的控制字符数组,这个控制字符数组保存的是ASCII码。上面VEOF即对应这个默认数组的下标。对于EOF这个字符每个tty都可以自己定义对应的ASCII码,也就是对应的是哪个按键。我们可以通过修改控制字符数组(termios)来更新对应的按键。

在include/termios.h(p374)中,定义了17个宏:

/* c_cc characters */#define VINTR 0#define VQUIT 1#define VERASE 2#define VKILL 3#define VEOF 4#define VTIME 5#define VMIN 6#define VSWTC 7#define VSTART 8#define VSTOP 9#define VSUSP 10#define VEOL 11#define VREPRINT 12#define VDISCARD 13#define VWERASE 14#define VLNEXT 15#define VEOL2 16

所以每个控制字符都有一个ASCII码,如Ctrl+ D 的ASCII= 4,输入结束。Ctrl+ C对应的是3,加64则是C。所以如果设置了回显的话会有^C出现。

在tty设置规范模式(ICANON)的时候,copy_to_cooked会处理四个特殊字符。删除一行是Ctrl+U,从当前secondary队列的head开始往后删除,直到碰到换行或者文件结束符或者secondary队列空为止。删除一个字符是Ctrl+ H,往后移动secondary的head指针。如果tty有设置回显标志,则用一个DEL(ASCII=127)表示删除一个字符,如果该字符的ASCII<32则再显示一个DEL,输出到write_q中。由于该队列对应的是显示屏,所以显示屏还会对其做进一步的处理,如将光标处的字符变为空白,这样就看不到了。如果是\n,则会将光标往前移动一行。同时还对Ctrl+ Q和Ctrl+ S进行处理。

注意:在secondary可以有\n字符,但是在屏幕上则必须实现其功能,即光标移动。对于删除操作,对于secondary,直接移动head即可,但对于write_q来说,必须发送127(DEL)或者8(backspace),这样才会在con_write中移动光标。如在secondary可以有\t(ASCII=9),但在屏幕上必须表现为至多8个空格。这些都是在con_write操作显示屏时进行实现的,copy_to_cooked只是对某些控制字符进行了操作,对\t不做处理。

如果tty设置了ISIG标志,则允许通过按键发送信号。按下Ctrl+ C,向整个前台进程组发送INT信号。按下Ctrl+ \,发送退出信号(产生进程映像的core文件)。

其中tty_intr(tty,INTMASK),函数位于第111行:

void tty_intr(structtty_struct * tty, int mask){    int i;    if (tty->pgrp <= 0)        return;    for (i=0; i<NR_TASKS; i++)        if (task[i] &&task[i]->pgrp==tty->pgrp)            task[i]->signal |= mask;}

如果有tty设置回显标志,则在写入secondary队列的同时,将数据写入到write_q中,并立即调用tty->write(tty)实时在显示屏显示,或者通过串口输出。如果当前ASCII码是个换行符(\n,ASCII = 10),或者是文件结束符(Ctrl+ D),则行数加一(tty->secondary.data++)。这些特殊字符会被写入到secondary中,包括Ctrl+ D。

4.2、串口输入输出的驱动程序

串口的驱动程序是rs1_interrupt,rs2_interrupt。这两个函数位于kernel/chr_drv/rs_io.s(p213)中:

/*

* linux/kernel/rs_io.s

*

* (C) 1991 Linus Torvalds

*/

/*

* rs_io.s

*

* This module implements thers232 io interrupts.

*/

.code32

.text

.globlrs1_interrupt,rs2_interrupt

size = 1024 /* must bepower of two !

and must match thevalue

in tty_io.c!!! */

/* these are the offsets intothe read/write buffer structures */

rs_addr = 0

head = 4

tail = 8

proc_list = 12

buf = 16

startup = 256 /* chars leftin write queue when we restart it */

/*

* These are the actualinterrupt routines. They look where

* the interrupt is comingfrom, and take appropriate action.

*/

.align 2

rs1_interrupt:

pushl $table_list+8

jmp rs_int

.align 2

rs2_interrupt:

pushl $table_list+16

rs_int:

pushl %edx

pushl %ecx

pushl %ebx

pushl %eax

push %es

push %ds /* as this is aninterrupt, we cannot */

pushl $0x10 /* know that bsis ok. Load it */

pop %ds

pushl $0x10

pop %es

movl 24(%esp),%edx

movl (%edx),%edx

movl rs_addr(%edx),%edx

addl $2,%edx /* interruptident. reg */

rep_int:

xorl %eax,%eax

inb %dx,%al

testb $1,%al

jne end

cmpb $6,%al /* thisshouldn’t happen, but … */

ja end

movl 24(%esp),%ecx

pushl %edx

subl $2,%edx

call jmp_table(,%eax,2) /* NOTE! not *4, bit0 is 0 already */

popl %edx

jmp rep_int

end: movb $0x20,%al

outb %al,$0x20 /* EOI */

pop %ds

pop %es

popl %eax

popl %ebx

popl %ecx

popl %edx

addl $4,%esp # jump over_table_list entry

iret

jmp_table:

.longmodem_status,write_char,read_char,line_status

.align 2

modem_status:

addl $6,%edx /* clear intrby reading modem status reg */

inb %dx,%al

ret

.align 2

line_status:

addl $5,%edx /* clear intrby reading line status reg. */

inb %dx,%al

ret

.align 2

read_char:

inb %dx,%al

movl %ecx,%edx

subl $table_list,%edx

shrl $3,%edx

movl (%ecx),%ecx #read-queue

movl head(%ecx),%ebx

movb %al,buf(%ecx,%ebx)

incl %ebx

andl $size-1,%ebx

cmpl tail(%ecx),%ebx

je 1f

movl %ebx,head(%ecx)

1: pushl %edx

call do_tty_interrupt

addl $4,%esp

ret

.align 2

write_char:

movl 4(%ecx),%ecx #write-queue

movl head(%ecx),%ebx

subl tail(%ecx),%ebx

andl $size-1,%ebx # nr charsin queue

je write_buffer_empty

cmpl $startup,%ebx

ja 1f

movl proc_list(%ecx),%ebx #wake up sleeping process

testl %ebx,%ebx # is thereany?

je 1f

movl $0,(%ebx)

1: movl tail(%ecx),%ebx

movb buf(%ecx,%ebx),%al

outb %al,%dx

incl %ebx

andl $size-1,%ebx

movl %ebx,tail(%ecx)

cmpl head(%ecx),%ebx

je write_buffer_empty

ret

.align 2

write_buffer_empty:

movl proc_list(%ecx),%ebx #wake up sleeping process

testl %ebx,%ebx # is thereany?

je 1f

movl $0,(%ebx)

1: incl %edx

inb %dx,%al

jmp 1f

1: jmp 1f

1: andb $0xd,%al /* disabletransmit interrupt */

outb %al,%dx

ret

两个中断主要是数据保存的read_q不同,主串口在tty_table[1],而辅串口在tty_table[2]。

上面函数加粗部分表示获得read_q的地址,并通过read_q.data获得数据端口。对于主串口,数据端口是0x3F8,而辅串口则是0x2F8。之后将通过加2获得中断发生寄存器端口0x3FA。如果该寄存器的最后一位(0位)置空,表示有中断。有中断时第1,2位构成四个可能的值,对应四种可能的中断,作为索引(EAX已经乘以2了)分别执行jmp_table所在处的函数。

使用寄存器传递参数。传递的参数主要是ECX=存放read_q指针的地址,EDX=0x3F8数据口。对于read_char类型的中断,直接通过数据口读取一个字节的数据并放到read_q,而且head++,最后调用do_tty_interrupt(1或2),也就是copy_to_interrupt(1或2)。

键盘并没有写操作,所以控制台把键盘和显示屏绑在了一起,作为一个可以读写的tty设备。对于write_char,ECX+4为write_q的地址,对该队列进行操作即可,数据依旧是发送到0x3F8。注意一次中断只发送一个字符,如果write_q还有字符则不屏蔽写中断允许,可以继续进行写。否则将0x3F9(设置4个中断允许的寄存器)的第1位置位,表示不允许发生写中断。这个位可以在rs_write中被恢复设置。另外两种中断是状态变化的中断。

五、tty设备操作结构图 注意,读进程没有任何数据到达,写进程内核缓冲区已满,都会进入可中断的休眠状态,而且是一个链表。条件满足时会被中断例程唤醒。

希望有一天,自己也像他们一样,踩着单车上路,

Linux 0.11 字符设备的使用

相关文章:

你感兴趣的文章:

标签云: