Linux 0.11 中字符设备的使用

Posted ac_dao_di

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 0.11 中字符设备的使用相关的知识,希望对你有一定的参考价值。

Linux 0.11 字符设备的使用

一、概述

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

二、上层接口

2.1sys_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=20include/linux/fs.hp395,第43行),看是否有未使用的文件句柄fd,然后将句柄设置为在加载新的程序文件时不关闭,即可以在两个进程共享。

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

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

        在fs/read_write.c(p304,55)实现了sys_readsys_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,而却像一个大文件一样读写(拥有文件读取位置)。这也就是为什么所有的文件都可以用readwrite来读写,且只需传递fd即可。将字符设备号(保存在字符设备节点inode->i_zone[0])传递给rw_char函数,而一个文件指针的作用仅是保存文件的当前位置(file->f_pos)。值得注意的是文件的当前位置对字符设备来说没有作用。

2.2rw_char源码及分析

        rw_char位于fs/char_dev.cp303,第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.cp301,第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_readtty_write,传递的都是次设备号,且文件位置pos不起作用。只不过控制台终端要求进程必须有控制终端,传进来的minor次设备号被忽略,使用当前进程的控制终端代替(current->tty)。注意rw_char操作的是设备号,而不是inode。

2.3 上层接口结构图



三、操作tty设备

3.1tty_readtty_write源码及分析

        这两个函数位于linux/kernel/chr_drv/tty_io.cp216,第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.hp409,第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.hp374,第53行)

#define NCCS 17
struct 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 1024

struct 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_qwrite_qdata都是数据口的地址,而secondarydatasecondary中数据的行数。

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

3.2con_writers_write源码及分析

        con_write位于kernel/chr_dev/console.cp201,第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列,同时还要注意设置字符的属性,也就是前景和背景的颜色等)。对于ASCII0– 31, 127其实是控制字符,必须进行特殊处理。如\\n= 10表示换行调到下一行的相同位置,\\r = 13表示回车回到当前行的开头,BEL= 7表示扬声器发声,8表示退格符删除左边一个字符,\\t= 9向下个8的整数倍的位置移动光标。控制序列(CSI)以ESCASCII=27)开头,如ESC[7m]是将字符显示为白底黑字(反显)。代码中的case1-case4都是在处理以ESC开头的控制序列。例子:write(fd,“hello\\tworld!\\n”, 20)

       上面的gotoxy在(kernel/chr_drv/console.cp193,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,表示一个屏幕的大小25x80列,而且是以字符为单位的。这里的字符要占用两个字节,低字节用于设置字符的ASCII,高字节用于设置字符属性,最多可以显示2000个字符。而video_size_row表示一行占用的字节数,video_size_row= 160

        这里的origin是显示屏显示区域的起始地址,而且是个虚拟地址。而x,y是坐标,0<=x<=80,0<=y<25pos是当前光标的虚拟地址,不过它是针对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以上是关于Linux 0.11 中字符设备的使用的主要内容,如果未能解决你的问题,请参考以下文章

Linux 0.11-打开终端设备文件-33

Linux 0.11-打开终端设备文件-33

字符设备驱动程序

Linux 0.11Makefile

Linux 0.11-硬盘初始化-20

Linux 0.11-硬盘初始化-20