// linux-0.11/init/main.c
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
// 核心代码,再调用write
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
上面调用的那个write接口如下:
// linux-0.11/lib/write.c
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
进入write后,会进入系统调用sys_write这个接口中:
// linux-0.11/include/linux/sys.h
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,...
从上面的这个sys_call_table数组中找到sys_write的函数指针,接着执行其内部函数。
// linux-o.11/fs/read_write.c
int sys_write(unsigned int fd,char * buf,int count)
{
struct file * file;
struct m_inode * inode;
// 从当前进程pcb中读取携带的文件指针
if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))
return -EINVAL;
if (!count)
return 0;
// 获取文件的信息inode
inode=file->f_inode;
if (inode->i_pipe)
return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
// 判断是否为字符类型设备,如果是,则继续调用rw_write字符设备写接口
// 对于printf来说打印的字符显示在控制台终端,所以是字符型设备
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;
}
上面的rw_char接口传递的参数比较难理解的有两个:inode->i_zone[0]和file->f_pos。
其中file->f_pos就是文件打开时光标的位置,也可以理解为文件中内存的下标。inode->i_zone[0]就是设备号。
// linux-0.11/fs/namei.c
int sys_mknod(const char * filename, int mode, int dev)
{
...
if (S_ISBLK(mode) || S_ISCHR(mode))
inode->i_zone[0] = dev;
if (S_ISBLK(mode) || S_ISCHR(mode))
inode->i_zone[0] = dev;
...
}
这上面的代码表示在创建设备节点时将设备号保存在inode->i_zone[0]这片内存中。
// linux-0.11/fs/char_dev.c
int rw_char(int rw,int dev, char * buf, int count, off_t * pos)
{
crw_ptr call_addr;
if (MAJOR(dev)>=NRDEVS)
return -ENODEV;
// 核心代码是通过dev这个设备号找到对应设备操作的函数指针call_addr
if (!(call_addr=crw_table[MAJOR(dev)]))
return -ENODEV;
// 通过这个函数指针进一步操作读写设备
return call_addr(rw,MINOR(dev),buf,count,pos);
}
接着会进入call_addr这个函数。这个函数怎么查找呢,先要看一下crw_table这个数组:
// linux-0.11/fs/char_dev.c
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 */
从这个数组可以知道,上面的函数指针就是这数组中一项指针。怎么找这个指针呢?可以通过MAJOR(dev)这个接口获取主设备号来获取上述数组的下标。那么终端的主设备号又是多少呢?请看下图:
上面的图表示终端tty0的主设备号为4,次设备号是0,所以上面的那个函数指针最终是数组中的rw_ttyx这个指针。找到了这个指针,下面我们来分析这个函数。
// linux-0.11/fs/char_dev.c
static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos)
{
// 下面的代码的意思是先判断rw是读还是写操作,如果是读操作,则执行tty_read函数;
// 如果是写操作,则执行tty_write函数,显然printf在这里是写操作,执行tty_write
return ((rw==READ)?tty_read(minor,buf,count):
tty_write(minor,buf,count));
}
先分析下传入tty_write函数的参数minor,这个参数表示的意思是待操作设备的次设备号,minor怎么来的呢,注意前面的传参中有这句MINOR(dev),它表示的意思是获取dev的次设备号,从前面得知次设备号是0。好了,那么下面我们开始分析这个函数。
// linux-0.11/kernel/chr_drv/tty_io.c
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_table这个数组中找到对应的tty设备操作函数指针
tty = channel + tty_table;
while (nr>0) {
// 判断tty设备的写队列是否满了,如果满了就让它睡一会,执行其它的进程
// 等写队列不再满时,唤醒该进程继续向下执行
sleep_if_full(&tty->write_q);
if (current->signal)
break;
while (nr>0 && !FULL(tty->write_q)) {
// 上述那个传进来的buf是用用户态传进来的指针,所以需要使用
// fs转换成内核方式读取待写数据
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);
}
// 总之,上面blabla一大堆是为了将写的数据写保存到tty这个结构体指针变量中
// 然后tty设备的write函数指针进行写操作
tty->write(tty);
if (nr>0)
schedule();
}
return (b-buf);
}
上面的tty->write函数是在哪里找的呢?想要解决这个问题,我们首先待知道tty对应的是什么。还记得tty = channel + tty_table;这句代码吗?其中tty_table中就列举了tty的指针:
// linux-0.11/kernel/chr_drv/tty_io.c
struct tty_struct tty_table[] = {
{
{ICRNL, /* change incoming CR to NL */
OPOST|ONLCR, /* change outgoing 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,""}
}
};
因为上面的channel是0,也就是其次设备号是0,所以对应表中的第一项。那么tty->write就对应表中con_write这个指针。下面我们来分析这个函数。
// linux-0.11/kernel/chr_drv/console.c
void con_write(struct tty_struct * tty)
{
int nr;
char c;
// 获取写队列的字符数量
nr = CHARS(tty->write_q);
while (nr--) {
// 每次获取一个字符c
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();
}
// 写操作的核心代码。这句嵌入汇编代码的意思是先把寄存器ax中存放
// 字符c和其属性,其中字符在低位。然后将ax中的数据存放在地址
// 为pos的内存处。这样就完成了一个字符的printf,后面的
// 字符也是大同小异地写入到pos地址的内存处.
// 这里的movw严格来说应该用out这种指令的,因为是操作外设,为啥
// 这里使用movw呢?因为这里的外设也就是终端屏幕的地址空间与cpu
// 的地址空间在一起,所以用movw效果也一样。
__asm__("movb attr,%%ah\n\t"
"movw %%ax,%1\n\t"
::"a" (c),"m" (*(short *)pos)
);
pos += 2;
x++;
}
...
}
set_cursor();
}
到此步,其实已经把字符写到终端上了。这里的核心写代码就是那句嵌入汇编代码。其他的都是附带的操作,目的是为了这句嵌入汇编代码被人们更方便使用。
其实分析完这个printf后,我们可以发现io操作并不难。只不过繁琐的是io操作时 的地址与对应的数据需要一一对应,为了不让我们容易出错,所以建立了一个统一的方式或者架构,供我们利用,并最后使用那句嵌入汇编代码。那么以后我们写一些io驱动时,就只用注册一些表中的函数指针和一些设备属性就可以啦。