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.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 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_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以上是关于Linux 0.11 中字符设备的使用的主要内容,如果未能解决你的问题,请参考以下文章