操作系统之打印Hello World

调用printf

// 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的函数指针,接着执行其内部函数。

分析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]就是设备号。

分析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]这片内存中。

分析rw_char函数

// 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这个指针。找到了这个指针,下面我们来分析这个函数。

分析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。好了,那么下面我们开始分析这个函数。

分析tty_write函数

// 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这个指针。下面我们来分析这个函数。

分析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驱动时,就只用注册一些表中的函数指针和一些设备属性就可以啦。

猜你喜欢

转载自blog.csdn.net/m0_38099380/article/details/89165903