目录
title: 驱动调试(一)-printk
date: 2019/1/9 19:35:14 toc: true ---驱动调试(一)-printk
引入
uboot的启动参数中定义了我们内核启动时的信息输出
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
如果去除console=ttySAC0
,则内核复制后没有信息输出,可以看下lcd,已经有显示了
#OpenJTAG> set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrcStarting kernel ...Uncompressing Linux.............................................. done, booting the kernel.
也可以设置为tty1
,直接在LCD上输出,这个需要有lcd驱动程序了(废话 哈哈),这里我试了tty0和tty1 和tty2,tty3都是在lcd显示
set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc console=tty1
其实也可以使用多个终端输出,比如这样
set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc console=tty1 console=ttySAC0
注意 这里设置参数后不要使用save
保存到flash,直接退回到menu
,输入b
启动即可
那么内核的printk
是怎么根据console=xxx
找到输出的硬件设备的?
框架
入口console_setup
搜索console=
,在有以下代码__setup("console=", console_setup);
,这个宏是用来处理启动参数的
文件在kernel\printk.c
/* * Set up a list of consoles. Called from init/main.c */static int __init console_setup(char *str){ char name[sizeof(console_cmdline[0].name)]; char *s, *options; int idx; /* * Decode str into name, index, options. */ // 先复制8字节到name if (str[0] >= '0' && str[0] <= '9') { strcpy(name, "ttyS"); strncpy(name + 4, str, sizeof(name) - 5); } else { strncpy(name, str, sizeof(name) - 1); } name[sizeof(name) - 1] = 0; // 判断是否有"," 也就是是不是有选项字节 if ((options = strchr(str, ',')) != NULL) *(options++) = 0;#ifdef __sparc__ if (!strcmp(str, "ttya")) strcpy(name, "ttyS0"); if (!strcmp(str, "ttyb")) strcpy(name, "ttyS1");#endif //从name中 找数字 for (s = name; *s; s++) if ((*s >= '0' && *s <= '9') || *s == ',') break; idx = simple_strtoul(s, NULL, 10); *s = 0; // 这里就会添加控制台了,也就是记录下来,还没有找到硬件 add_preferred_console(name, idx, options); return 1;}__setup("console=", console_setup);
add_preferred_console
这里是将命令行参数解析后存入全绝的结构体变量console_cmdline
,这里只是存起来,并没有去解析
add_preferred_console{ struct console_cmdline *c; // 这里有个全局变量 console_cmdline,保存所有的终端,这里支持8个 //#define MAX_CMDLINECONSOLES 8 //static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES]; //step1 判断是否存在了已经 for(i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) ... //指向最后一个命令行的参数console_cmdline selected_console = i; //step2 存入这个全局的数组 包括name,序号,选项 c = &console_cmdline[i]; memcpy(c->name, name, sizeof(c->name)); c->name[sizeof(c->name) - 1] = 0; c->options = options; c->index = idx; }
register_console
继续搜索这个全局变量,可以看到注册函数,匹配命令行的name和注册的驱动后加入到链表中
selected_console 在add_preferred_console 处理命令参数的时候 指向最后一个命令行的参数console_cmdline
register_console // 如果没有注册过console,preferred_console 指向selected_console 也就是最后一个命令行参数的console if (preferred_console < 0 || bootconsole || !console_drivers) preferred_console = selected_console; // 如果没有注册过console,会先来一个初始化这第一个来注册的console if (preferred_console < 0) console->setup(console, NULL) console->index = 0; //没有注册时,强制赋值0 for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];i++) ... // 1. 比较命令行的名字与注册的驱动的名字,如果name匹配 // 2. 执行带有option的 console->setup(console, console_cmdline[i].options) // 3. 如果成功,设置标志 console->flags |= CON_ENABLED; console->index = console_cmdline[i].index; //4. 选择一个作为preferred_console,如果匹配到最后一个命令行,preferred_console就等于这个selected_console=最后一个命令行 if (i == selected_console) { console->flags |= CON_CONSDEV; preferred_console = selected_console; // 5.加入到链表 console_drivers ,注册的console本身也包含了一个链表指向 if ((console->flags & CON_CONSDEV) || console_drivers == NULL) { console->next = console_drivers; console_drivers = console; if (console->next) console->next->flags &= ~CON_CONSDEV; } else { console->next = console_drivers->next; console_drivers->next = console; }
这里的链表结构应该是如下这样的:
s3c24xx_serial_initconsole
搜索这个注册函数的调用,发现s3c24xx_serial_initconsole
使用了这个注册函数,可以看到s3c24xx_serial_console
的name正是"ttySAC"
s3c24xx_serial_initconsole // 驱动相关,先看看是不是有硬件驱动 struct platform_device *dev = s3c24xx_uart_devs[0]; //注册console register_console(&s3c24xx_serial_console);static struct console s3c24xx_serial_console ={ .name = S3C24XX_SERIAL_NAME, .device = uart_console_device, .flags = CON_PRINTBUFFER, .index = -1, .write = s3c24xx_serial_console_write, .setup = s3c24xx_serial_console_setup};#define S3C24XX_SERIAL_NAME "ttySAC"#define S3C24XX_SERIAL_MAJOR 204#define S3C24XX_SERIAL_MINOR 64
write
可以在这个结构体里面发现write
函数操作了实际的硬件,也就是write
是printk
是实际的写硬件函数
s3c24xx_serial_console_write >uart_console_write(cons_uart, s, count, s3c24xx_serial_console_putchar); >wr_regb(cons_uart, S3C2410_UTXH, ch);
printk
asmlinkage int printk(const char *fmt, ...){ va_start(args, fmt); r = vprintk(fmt, args); va_end(args);}
vprintk
这个函数最后会查找console_drivers
这个链表来进行打印处理,通过msg_level
判断是否输出到硬件
vprintk(const char *fmt, va_list args){ // 解析数据到一个buf /* Emit the output into the temporary buffer */ printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args); //对buf 进行特殊处理printk_buf,填充到 log_buf for (p = printk_buf; *p; p++) { //如果没有 形如 <数字> 的开头,自动补上也就是<4>,提取这个lev //如果有,同样提取这个lev emit_log_char(c) } if (cpu_online(smp_processor_id()) || have_callable_console()) { //have_callable_console 遍历 console_drivers // > for (con = console_drivers; con; con = con->next) // 这个链表就是register_console 中注册的了 console_may_schedule = 0; // 打印输出 release_console_sem(); { .... } }} 数字>
release_console_sem
先将数据输出到LOG_BUF
,实际的输出到硬件会去判断一个打印级别,不论是否到达打印级别,都可以使用dmesg
显示这个log_buf[]
release_console_sem(){ // 静态全局变量 _con_start = con_start; _log_end = log_end; call_console_drivers(_con_start, _log_end); { // 提取打印等级 msg_level = LOG_BUF(cur_index + 1) - '0'; _call_console_drivers(start_print, cur_index, msg_level); { // lev < 设置的log lev,则打印 if ((msg_log_level < console_loglevel || ignore_loglevel) && console_drivers && start != end) { //遍历console驱动链表,判断是否有write函数,如果有,执行write函数 __call_console_drivers(start, end); { for (con = console_drivers; con; con = con->next) { if ((con->flags & CON_ENABLED) && con->write...) con->write(con, &LOG_BUF(start), end - start); } } } } }}
打印级别
我们在里面使用的是_call_console_drivers
中判断if msg_log_level < console_loglevel
,也就是说默认的级别就是console_loglevel
,也就是默认小于<7>才打印
#define console_loglevel (console_printk[0]) ==7
可以使用cat /proc/sys/kernel/printk
查看是不是这个,这个值就是数组console_printk[4]
# cat /proc/sys/kernel/printk7 4 1 7
- 第一个参数 7表示小于7优先级消息才会被输出到控制台
- 第二个参数4 表示默认的printk消息优先级别,即printk(“hell world”);优先级为4, 由于4<7,故可以被打印到控制台。
- 第三个参数1 表示可接收的最高优先级,当printk disable控制台输出时,设置第一个参数为1,但是,从内核等级来看,还有优先级0,这个是printk最高级优先级,一般用于内核严重消息打印。比如内存错误或者 watchdog reset.也可以设置第一个和第三个参数为0
- 第四个参数7 默认控制台优先级,即第一个参数的默认优先级。
具体相关的定义在这里,可以使用include\linux\kernel.h
查看
int console_printk[4] = { //=7 DEFAULT_CONSOLE_LOGLEVEL, /* console_loglevel */ //=4 DEFAULT_MESSAGE_LOGLEVEL, /* default_message_loglevel */ //=1 MINIMUM_CONSOLE_LOGLEVEL, /* minimum_console_loglevel */ //=7 DEFAULT_CONSOLE_LOGLEVEL, /* default_console_loglevel */};/* printk's without a loglevel use this.. */#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING *//* We show everything that is MORE important than this.. */#define MINIMUM_CONSOLE_LOGLEVEL 1 /* Minimum loglevel we let people use */#define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */#define KERN_EMERG "<0>" // 系统崩溃#define KERN_ALERT "<1>" //必须紧急处理#define KERN_CRIT "<2>" // 临界条件,严重的硬软件错误#define KERN_ERR "<3>" // 报告错误#define KERN_WARNING "<4>" //警告#define KERN_NOTICE "<5>" //普通但还是须注意#define KERN_INFO "<6>" // 信息#define KERN_DEBUG "<7>" // 调试信息
使用printk
格式可以加上打印级别,形式如下:
printk(KERN_EMERG "abc") === printk( "<0>abc");
修改打印级别
临时修改
/proc/sys/kernel/printk
,重启后失效,下述命令关闭打印,也就是设置小于DEFAULT_CONSOLE_LOGLEVEL=1
才打印echo "1 4 1 7" > /proc/sys/kernel/printk
修改初始化的数组或者是那个判断的函数
int console_printk[4] = { 1,//DEFAULT_CONSOLE_LOGLEVEL, /* console_loglevel */ DEFAULT_MESSAGE_LOGLEVEL, /* default_message_loglevel */ MINIMUM_CONSOLE_LOGLEVEL, /* minimum_console_loglevel */ DEFAULT_CONSOLE_LOGLEVEL, /* default_console_loglevel */};
或者修改 内核源代码
static void _call_console_drivers(unsigned long start, unsigned long end, int msg_log_level){ //if ((msg_log_level < console_loglevel || ignore_loglevel) && if ((msg_log_level < 1 || ignore_loglevel) && console_drivers && start != end) { if ((start & LOG_BUF_MASK) > (end & LOG_BUF_MASK)) { /* wrapped write */ __call_console_drivers(start & LOG_BUF_MASK, log_buf_len); __call_console_drivers(0, end & LOG_BUF_MASK); } else { __call_console_drivers(start, end); } }}
设置
uboot
传递的参数,我们可以看到级别定义如下,搜索文本console_loglevel
,可以找到一些函数更多的参考文档可以查看
Documentation\kernel-parameters.txt
,搜索console
#define console_loglevel (console_printk[0])
//init\main.cstatic int __init debug_kernel(char *str){ if (*str) return 0; console_loglevel = 10; return 1;}static int __init quiet_kernel(char *str){ if (*str) return 0; console_loglevel = 4; return 1;}__setup("debug", debug_kernel);__setup("quiet", quiet_kernel);static int __init loglevel(char *str){ get_option(&str, &console_loglevel); return 1;}__setup("loglevel=", loglevel);
也就是说可以用这些参数传递打印级别
loglevel=0 console=ttySA0,115200debug # 使用级别10quiet # 使用级别4set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc loglevel=0 console=ttySAC0boot# cat /proc/sys/kernel/printk0 4 1 7set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc debug console=ttySAC0boot# cat /proc/sys/kernel/printk10 4 1 7set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc quiet console=ttySAC0boot# cat /proc/sys/kernel/printk4 4 1 7
使用dmesg打印所有日志
使用这个命令可以打印那些被屏蔽的缓冲,可以保存到文本里面去看
# dmesg ....?...................................... done, booting the kernel.Linux version 2.6.22.6 (book@100ask) (gcc version 3.4.5) #3 Wed Jan 9 15:33:52 CST 2019CPU: ARM920T [41129200] revision 0 (ARMv4T), cr=c0007177
测试
修改级别为0,都可以实现如下效果,不打印内核启动信息
Starting kernel ...Uncompressing Linux...................................................................................................................... done, booting the kernel.init started: BusyBox v1.7.0 (2018-11-13 23:35:45 CST)starting pid 766, tty '': '/etc/init.d/rcS'Please press Enter to activate this console.starting pid 771, tty '/dev/console': 'bin/sh'
也可以# cat /proc/sys/kernel/printk
来查看,除了那个修改函数中的if
判断的,都可以打印出来如下效果
# cat /proc/sys/kernel/printk1 4 1 7
修改if
判断的依然是7 4 1 7
,因为这个文件本质上就是这个数组显示 哈哈
小结
内核解析uboot传递的命令行参数,来寻找实际的硬件来输出信息
console_setup >add_preferred_console
注册实际的硬件驱动,加入到
console_drivers
链表s3c24xx_serial_initconsole >register_console >比较命令行的name 与 硬件驱动的name ,如果匹配,加入到 console_drivers链表中
使用
printk( lev "...")
来输出信息,如果没有指定lev,以默认的lev=4输出,具体是在console_drivers
中寻找驱动找到他的write
函数输出printk >vprintk >release_console_sem >判断lev 打印
使用
dmesg
可以打印所有日志,包括被屏蔽的
参考文档
- Documentation\kernel-parameters.txt ,搜索
console
查看命令行参数传递 - 3.4内核代码分析 http://blog.chinaunix.net/uid-27717694-id-3495612.html
- cnblog https://www.cnblogs.com/lifexy/p/7993136.html
附录(3.4内核的分析)
这个代码讲的比较具体,先不去仔细分析了
http://blog.chinaunix.net/uid-27717694-id-3495612.html
console驱动:一、基本概念终端是一种字符型设备,通常使用tty简称各种类型的终端。linux的终端类型:/dev/ttySn,串行口终端 /dev/pty,伪终端 /dev/tty,当前进程的控制终端,可以是介绍的其它任何一种终端 /dev/ttyn,tty1~tty6是虚拟终端,tty0当前虚拟终端的别名。 /dev/console,控制台终端(显示器) 二、uboot传参数的处理linux启动时uboot传递进console=ttyS2,115200n8的参数内核中用__setup()宏声明参数处理的方法:__setup("console=", console_setup); 1.console_cmdline结构体struct console_cmdline { char name[8]; //驱动名 int index; //次设备号 char *options; //选项 #ifdef CONFIG_A11Y_BRAILLE_CONSOLE char *brl_options; #endif }; 2.内核调用console_setup()函数处理uboot传进的console参数static int __init console_setup(char *str) { char buf[sizeof(console_cmdline[0].name) + 4]; //分配驱动名+index的缓冲区,分配12个字节 char *s, *options, *brl_options = NULL; int idx; #ifdef CONFIG_A11Y_BRAILLE_CONSOLE if (!memcmp(str, "brl,", 4)) { brl_options = ""; str += 4; } else if (!memcmp(str, "brl=", 4)) { brl_options = str + 4; str = strchr(brl_options, ','); if (!str) { printk(KERN_ERR "need port name after brl=\n"); return 1; } *(str++) = 0; } #endif if (str[0] >= '0' && str[0] <= '9') { //第一个参数属于[0,9] strcpy(buf, "ttyS"); //则将其驱动名设为ttyS strncpy(buf + 4, str, sizeof(buf) - 5);//将次设备号放其后面 } else { strncpy(buf, str, sizeof(buf) - 1); //否则直接将驱动名+设备号拷贝到buf中 } buf[sizeof(buf) - 1] = 0; if ((options = strchr(str, ',')) != NULL) //获取options,即“115200n8” *(options++) = 0; #ifdef __sparc__ if (!strcmp(str, "ttya")) strcpy(buf, "ttyS0"); if (!strcmp(str, "ttyb")) strcpy(buf, "ttyS1"); #endif for (s = buf; *s; s++) if ((*s >= '0' && *s <= '9') || *s == ',')//移动指针s到次设备号处 break; idx = simple_strtoul(s, NULL, 10); //获取次设备号,字符串转换成unsigend long long型数据,s表示字符串的开始,NULL表示字符串的结束,10表示进制 //这里返回的是次设备号=2 *s = 0; __add_preferred_console(buf, idx, options, brl_options); console_set_on_cmdline = 1; return 1; } 3.__add_preferred_console()函数//整体的作用是根据uboot传递的参数设置全局console_cmdline数组//该数组及全局selected_console,在register_console中会使用到static int __add_preferred_console(char *name, int idx, char *options,char *brl_options) { struct console_cmdline *c; int i; for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)//可以最多8个console if (strcmp(console_cmdline[i].name, name) == 0 && console_cmdline[i].index == idx) { //比较已注册的console_cmdline数组中的项的名字及次设备号,若console_cmdline已经存在 if (!brl_options) selected_console = i;//设置全局selected_console索引号 return 0;//则返回 } if (i == MAX_CMDLINECONSOLES)//判断console_cmdline数组是否满了 return -E2BIG; if (!brl_options) selected_console = i; //设置全局selected_console索引号 c = &console_cmdline[i];//获取全局console_cmdline数组的第i项地址 strlcpy(c->name, name, sizeof(c->name)); //填充全局console_cmdline的驱动名“ttyS2” c->options = options; //填充配置选项115200n8 #ifdef CONFIG_A11Y_BRAILLE_CONSOLE c->brl_options = brl_options; #endif c->index = idx; //填充索引号2,即次设备号 return 0; } 三、在console初始化之前能使用printk,使用内核提供的early printk支持。//在调用console_init之前调用printk也能打印出信息,这是為什麼呢?在start_kernel函数中很早就调用了 parse_early_param函数,//该函数会调用到链接脚本中.init.setup段的函数。其中就有 setup_early_serial8250_console函数。//该函数通过 register_console(&early_serial8250_console);//注册了一个比较简单的串口设备。可以用来打印内核启 动早期的信息。 //对于early printk的console注册往往通过内核的early_param完成。early_param(“earlycon”,setup_early_serial8250_console);//定义一个earlycon的内核参数,内核解析这个参数时调用setup_early_serial8250_console()函数 1.setup_early_serial8250_console()函数//earlycon = uart8250,mmio,0xff5e0000,115200n8int __init setup_early_serial8250_console(char *cmdline){ char *options; int err; options = strstr(cmdline, "uart8250,");//找到“uart8250,”字符串,返回此字符串的起始位置 if (!options) { options = strstr(cmdline, "uart,"); if (!options) return 0; } options = strchr(cmdline, ',') + 1;//options指针指向第一个逗号后边的字符串地址 err = early_serial8250_setup(options);//进行配置 if (err < 0) return err; /* static struct console early_serial8250_console __initdata = { .name = "uart", .write = early_serial8250_write, .flags = CON_PRINTBUFFER | CON_BOOT,//所用具有CON_BOOT属性的console都会在内核初始化到late initcall阶段被注销,相互消他们的函数是 .index = -1, }; */ //注册一个早期的console,到真正的console_init时,此console会被注销,因为设置了CON_BOOT标志 register_console(&early_serial8250_console); return 0;} static int __init early_serial8250_setup(char *options){ struct early_serial8250_device *device = &early_device; int err; if (device->port.membase || device->port.iobase)//early_device设备的端口地址若配置过则返回 return 0; err = parse_options(device, options);//解析参数并配置early_device设备对应的uart_port结构 if (err < 0) return err; init_port(device);//early_device设备对应的初始化uart_port结构 return 0;} static int __init parse_options(struct early_serial8250_device *device,char *options){ struct uart_port *port = &device->port;//找到early_device设备对应的uart_port结构 int mmio, mmio32, length; if (!options) return -ENODEV; port->uartclk = BASE_BAUD * 16;//串口时钟 mmio = !strncmp(options, "mmio,", 5);//查找"mmio,"字符串,找到mmio=1 mmio32 = !strncmp(options, "mmio32,", 7);//mmio32=0 if (mmio || mmio32) { port->iotype = (mmio ? UPIO_MEM : UPIO_MEM32);//串口类型设为UPIO_MEM=2 port->mapbase = simple_strtoul(options + (mmio ? 5 : 7),&options, 0);//获得串口的配置寄存器基础地址(物理地址),这里是得到0xff5e0000 if (mmio32) port->regshift = 2;#ifdef CONFIG_FIX_EARLYCON_MEM set_fixmap_nocache(FIX_EARLYCON_MEM_BASE,port->mapbase & PAGE_MASK); port->membase =(void __iomem *)__fix_to_virt(FIX_EARLYCON_MEM_BASE); port->membase += port->mapbase & ~PAGE_MASK;#else port->membase = ioremap_nocache(port->mapbase, 64);//映射到内存的配置寄存器基础地址 if (!port->membase) { printk(KERN_ERR "%s: Couldn't ioremap 0x%llx\n", __func__,(unsigned long long) port->mapbase); return -ENOMEM; }#endif } else if (!strncmp(options, "io,", 3)) { port->iotype = UPIO_PORT; port->iobase = simple_strtoul(options + 3, &options, 0); mmio = 0; } else return -EINVAL; options = strchr(options, ',');//指针移到“115200n8”字符串处 if (options) {//存在 options++; device->baud = simple_strtoul(options, NULL, 0);//取得波特率115200 length = min(strcspn(options, " "), sizeof(device->options)); strncpy(device->options, options, length);//将字符串115200n8拷贝到设备的device->options字段中 } else { device->baud = probe_baud(port); snprintf(device->options, sizeof(device->options), "%u",device->baud); } if (mmio || mmio32) printk(KERN_INFO "Early serial console at MMIO%s 0x%llx (options '%s')\n",mmio32 ? "32" : "",(unsigned long long)port->mapbase,device->options); else printk(KERN_INFO "Early serial console at I/O port 0x%lx (options '%s')\n",port->iobase,device->options); return 0;} static void __init init_port(struct early_serial8250_device *device){ struct uart_port *port = &device->port; unsigned int divisor; unsigned char c; serial_out(port, UART_LCR, 0x3); /* 8n1 */ serial_out(port, UART_IER, 0); /* no interrupt */ serial_out(port, UART_FCR, 0); /* no fifo */ serial_out(port, UART_MCR, 0x3); /* DTR + RTS */ divisor = port->uartclk / (16 * device->baud);//根据波特率设置分频 c = serial_in(port, UART_LCR); serial_out(port, UART_LCR, c | UART_LCR_DLAB); serial_out(port, UART_DLL, divisor & 0xff); serial_out(port, UART_DLM, (divisor >> 8) & 0xff); serial_out(port, UART_LCR, c & ~UART_LCR_DLAB);} void register_console(struct console *newcon){ int i; unsigned long flags; struct console *bcon = NULL; /* 现在是注册一个early console,即 static struct console early_serial8250_console __initdata = { .name = "uart", .write = early_serial8250_write, .flags = CON_PRINTBUFFER | CON_BOOT,//所用具有CON_BOOT属性的console都会在内核初始化到late initcall阶段被注销,相互消他们的函数是 .index = -1, }; */ if (console_drivers && newcon->flags & CON_BOOT) {//注册的是否是引导控制台。early console的CON_BOOT置位,表示只是一个引导控制台,以后会被注销 for_each_console(bcon) {遍历全局console_drivers数组 if (!(bcon->flags & CON_BOOT)) {//判断是否已经有引导控制台了,有了的话就直接退出 printk(KERN_INFO "Too late to register bootconsole %s%d\n",newcon->name, newcon->index); return; } } } if (console_drivers && console_drivers->flags & CON_BOOT)//如果注册的是引导控制台 bcon = console_drivers;//让bcon指向全局console_drivers if (preferred_console < 0 || bcon || !console_drivers) preferred_console = selected_console;//设置preferred_console为uboot命令选择的selected_console(即索引) if (newcon->early_setup)//early console没有初始化early_setup字段,以下这个函数不执行 newcon->early_setup();//调用serial8250_console_early_setup() if (preferred_console < 0) { if (newcon->index < 0) newcon->index = 0; if (newcon->setup == NULL ||newcon->setup(newcon, NULL) == 0) { newcon->flags |= CON_ENABLED; if (newcon->device) { newcon->flags |= CON_CONSDEV; preferred_console = 0; } } } //传给内核参数: //Kernel command line: console=ttyS2,115200n8 rw root=/dev/ram0 initrd=0xc2000000,20M mem=128M ip=192.168.1.220::192.168.1.1:255.255.255.0::eth0:off //所以这里将根据传参console=ttyS2,115200来配置作为console的ttyS2串口 for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];i++) {//遍历全局console_cmdline找到匹配的 if (strcmp(console_cmdline[i].name, newcon->name) != 0)//比较终端名称“ttyS” continue; if (newcon->index >= 0 &&newcon->index != console_cmdline[i].index)//console_cmdline[i].index=2。//比较次设备号 continue; if (newcon->index < 0) newcon->index = console_cmdline[i].index;//将终端号赋值给serial8250_console->index #ifdef CONFIG_A11Y_BRAILLE_CONSOLE//没有定义,下边不执行 if (console_cmdline[i].brl_options) { newcon->flags |= CON_BRL; braille_register_console(newcon,console_cmdline[i].index,console_cmdline[i].options,console_cmdline[i].brl_options); return; }#endif //console_cmdline[i].options = "115200n8",对于early console而言setup字段未被初始化,故下边的函数不执行 if (newcon->setup &&newcon->setup(newcon, console_cmdline[i].options) != 0)//调用serial8250_console_setup()对终端进行配置 break; newcon->flags |= CON_ENABLED; //设置标志为CON_ENABLE(这个在printk调用中使用到) newcon->index = console_cmdline[i].index;//设置索引号 if (i == selected_console) { //索引号和uboot指定的console的一样 newcon->flags |= CON_CONSDEV;//设置标志CON_CONSDEV(全局console_drivers链表中靠前) preferred_console = selected_console; } break; }//for循环作用大致是查看注册的console是否是uboot知道的引导console,是则设置相关标志和preferred_console if (!(newcon->flags & CON_ENABLED)) return; if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))//防止重复打印 newcon->flags &= ~CON_PRINTBUFFER; acquire_console_sem(); if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {//如果是preferred控制台 newcon->next = console_drivers; console_drivers = newcon;//添加进全局console_drivers链表前面位置(printk中会遍历该表调用合适的console的write方法打印信息) if (newcon->next) newcon->next->flags &= ~CON_CONSDEV; } else {//如果不是preferred控制台 newcon->next = console_drivers->next; console_drivers->next = newcon; //添加进全局console_drivers链表后面位置 } //主册console主要是刷选preferred_console放置在全局console_drivers链表前面,剩下的console放置链表靠后的位置,并设置相应的flags, //console_drivers最终会在printk函数的层层调用中遍历到,并调用console的write方法将信息打印出来 if (newcon->flags & CON_PRINTBUFFER) { spin_lock_irqsave(&logbuf_lock, flags); con_start = log_start; spin_unlock_irqrestore(&logbuf_lock, flags); } release_console_sem(); if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) { printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n",newcon->name, newcon->index); for_each_console(bcon) if (bcon->flags & CON_BOOT) unregister_console(bcon); } else {//调用这里 printk(KERN_INFO "%sconsole [%s%d] enabled\n",(newcon->flags & CON_BOOT) ? "boot" : "" ,newcon->name, newcon->index); }} 四、在未对console进行初始化之前,内核使用early console进行打印。之后内核进行真正的console初始化//console_init()在start_kernel()中调用,用来对控制台初始化,这个函数执行完成后,串口可以看到内核用printk()函数打印的信息void __init console_init(void){ initcall_t *call; /* Setup the default TTY line discipline. */ //此函数调用tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY) //#define N_TTY 0 /*struct tty_ldisc_ops tty_ldisc_N_TTY = { .magic = TTY_LDISC_MAGIC, .name = "n_tty", .open = n_tty_open, .close = n_tty_close, .flush_buffer = n_tty_flush_buffer, .chars_in_buffer = n_tty_chars_in_buffer, .read = n_tty_read, .write = n_tty_write, .ioctl = n_tty_ioctl, .set_termios = n_tty_set_termios, .poll = n_tty_poll, .receive_buf = n_tty_receive_buf, .write_wakeup = n_tty_write_wakeup }; 内核定义一个tty_ldiscs数组,然后根据数组下标来存放对应的线路规程的操作集,而这里的数组下标表示的就是具体的协议,在头文件中已经通过宏定义好了。例如N_TTY 0。 所以可以发现:ldisc[0] 存放的是N_TTY对应的线路规程操作集 ldisc[1]存放的是N_SLIP对应的线路规程操作集 ldisc[2]存放的就是N_MOUSE对应的线路规程操作集 依次类推。此处就是ldisc[N_TTY] = tty_ldisc_N_TTY。 int tty_register_ldisc(int disc, struct tty_ldisc_ops *new_ldisc) { unsigned long flags; int ret = 0; if (disc < N_TTY || disc >= NR_LDISCS) return -EINVAL; spin_lock_irqsave(&tty_ldisc_lock, flags); tty_ldiscs[disc] = new_ldisc;//tty_ldiscs[0]存放的是N_TTY对应的线路规程操作集 new_ldisc->num = disc;//0 new_ldisc->refcount = 0; spin_unlock_irqrestore(&tty_ldisc_lock, flags); return ret; }*/ tty_ldisc_begin();//这段代码前面是注册了第0个(逻辑上1)线路规程 //依次调用从__con_initcall_start到__con_initcall_end之间的函数指针 //会调用两个函数就是con_init()和serial8250_console_init() call = __con_initcall_start; while (call < __con_initcall_end) { (*call)(); call++; }} static int __init serial8250_console_init(void){ if (nr_uarts > UART_NR)//串口数量不能大于3个 nr_uarts = UART_NR; serial8250_isa_init_ports();//对三个串口的uart_8250_port结构静态常量serial8250_ports结构进行初始化,主要是将up->port.ops = &serial8250_pops /* static struct console serial8250_console = { .name = "ttyS", .write = serial8250_console_write,//写方法 .device = uart_console_device,//tty驱动 .setup = serial8250_console_setup,//设置串口波特率,也就是设置串口。很重要,里面涉及到平台特性,波特率相关。 .early_setup = serial8250_console_early_setup, .flags = CON_PRINTBUFFER | CON_ANYTIME, .index = -1, .data = &serial8250_reg, }; */ register_console(&serial8250_console);//在这里注册serial8250_console真正的console终端 return 0;}console_initcall(serial8250_console_init);/*serial8250_console_init()函数会比serial8250_probe()先调用,所以调用register_console的时候,port还没有初始化,所以当register_console调用serial8250_console_setup()设置buad,parity bits的时候,serial8250_console_setup()会检测port->iobase和port->membase是否是有效值,如果不是就返回,放弃初始化console,所以实际上,console不是在serial8250_console_init()里边初始化,如果要在serial8250_console_init初始化,需要将port静态初始化. 当serial8250_probe()调用uart_add_one_port->uart_configure_port:if (port->cons && !(port->cons->flags & CON_ENABLED)){ printk("%s retister console\n", __FUNCTION__); register_console(port->cons);}该函数会检查console有没有初始化,如果没有初始化,则调用register_console来初始化.所以console放在这里初始化也是比较好一些,可以将console_initcall(serial8250_console_init) comment.*/ //对三个串口的uart_8250_port结构静态常量serial8250_ports结构进行初始化,主要是将up->port.ops = &serial8250_popsstatic void __init serial8250_isa_init_ports(void){ struct uart_8250_port *up; static int first = 1; int i, irqflag = 0; if (!first)//静态变量,serial8250_console_init()第一次进入这个函数,之后serial8250_init()再进入这个函数就会直接返回 return; first = 0; //对三个串口的uart_8250_port结构serial8250_ports结构体进行初始化 for (i = 0; i < nr_uarts; i++) { struct uart_8250_port *up = &serial8250_ports[i]; up->port.line = i;//0代表串口0,1代表串口1 spin_lock_init(&up->port.lock); init_timer(&up->timer);//初始化定时器 up->timer.function = serial8250_timeout;//初始化定时器的超时函数 //ALPHA_KLUDGE_MCR needs to be killed. up->mcr_mask = ~ALPHA_KLUDGE_MCR; up->mcr_force = ALPHA_KLUDGE_MCR; //初始化uart_8250_port指向的uart_port字段port的操作 up->port.ops = &serial8250_pops; /* static struct uart_ops serial8250_pops = { .tx_empty = serial8250_tx_empty, .set_mctrl = serial8250_set_mctrl, .get_mctrl = serial8250_get_mctrl, .stop_tx = serial8250_stop_tx, .start_tx = serial8250_start_tx, .stop_rx = serial8250_stop_rx, .enable_ms = serial8250_enable_ms, .break_ctl = serial8250_break_ctl, .startup = serial8250_startup, .shutdown = serial8250_shutdown, .set_termios = serial8250_set_termios, .set_ldisc = serial8250_set_ldisc, .pm = serial8250_pm, .type = serial8250_type, .release_port = serial8250_release_port, .request_port = serial8250_request_port, .config_port = serial8250_config_port, .verify_port = serial8250_verify_port, #ifdef CONFIG_CONSOLE_POLL .poll_get_char = serial8250_get_poll_char, .poll_put_char = serial8250_put_poll_char, #endif }; */ } if (share_irqs)//中断是否共享(这里设置成不共享) irqflag = IRQF_SHARED; //条件不满足,不会进来初始化 for (i = 0, up = serial8250_ports;i < ARRAY_SIZE(old_serial_port) && i < nr_uarts;i++, up++) {/* up->port.iobase = old_serial_port[i].port; up->port.irq = irq_canonicalize(old_serial_port[i].irq); up->port.irqflags = old_serial_port[i].irqflags; up->port.uartclk = old_serial_port[i].baud_base * 16; up->port.flags = old_serial_port[i].flags; up->port.hub6 = old_serial_port[i].hub6; up->port.membase = old_serial_port[i].iomem_base; up->port.iotype = old_serial_port[i].io_type; up->port.regshift = old_serial_port[i].iomem_reg_shift; set_io_from_upio(&up->port); up->port.irqflags |= irqflag; if (serial8250_isa_config != NULL) serial8250_isa_config(i, &up->port, &up->capabilities);*/ }} //下边再次调用register_console()注册serial8250_console真正的console终端void register_console(struct console *newcon){ int i; unsigned long flags; struct console *bcon = NULL; /* 现在是注册一个serial8250_console,即 static struct console serial8250_console = { .name = "ttyS", .write = serial8250_console_write,//写方法 .device = uart_console_device,//tty驱动 .setup = serial8250_console_setup,//设置串口波特率,也就是设置串口。很重要,里面涉及到平台特性,波特率相关。 .early_setup = serial8250_console_early_setup, .flags = CON_PRINTBUFFER | CON_ANYTIME, .index = -1, .data = &serial8250_reg, }; */ if (console_drivers && newcon->flags & CON_BOOT) {//注册的是serial8250_console,CON_BOOT没有置位,不是引导控制台。下边不会进去遍历 for_each_console(bcon) {遍历全局console_drivers数组 if (!(bcon->flags & CON_BOOT)) {//判断是否已经有引导控制台了,有了的话就直接退出 printk(KERN_INFO "Too late to register bootconsole %s%d\n",newcon->name, newcon->index); return; } } } if (console_drivers && console_drivers->flags & CON_BOOT)//如果注册的是引导控制台,serial8250_console不是引导控制台 bcon = console_drivers;//这里不执行 if (preferred_console < 0 || bcon || !console_drivers) preferred_console = selected_console;//设置preferred_console为uboot命令选择的selected_console(即在Uboot传入的参数“console=ttyS2,115200n8”在console_cmdline[]数组中的索引) //这里preferred_console =0 if (newcon->early_setup)//serial8250_console初始化early_setup字段 newcon->early_setup();//调用serial8250_console_early_setup() if (preferred_console < 0) {//由于preferred_console =0,不会进入下边 if (newcon->index < 0) newcon->index = 0; if (newcon->setup == NULL ||newcon->setup(newcon, NULL) == 0) { newcon->flags |= CON_ENABLED; if (newcon->device) { newcon->flags |= CON_CONSDEV; preferred_console = 0; } } } //传给内核参数: //Kernel command line: console=ttyS2,115200n8 rw root=/dev/ram0 initrd=0xc2000000,20M mem=128M ip=192.168.1.220::192.168.1.1:255.255.255.0::eth0:off //所以这里将根据传参console=ttyS2,115200来配置作为console的ttyS2串口 for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];i++) {//遍历全局console_cmdline找到匹配的,i=0就是匹配的“ttyS2” if (strcmp(console_cmdline[i].name, newcon->name) != 0)//比较终端名称“ttyS” continue; if (newcon->index >= 0 &&newcon->index != console_cmdline[i].index)//console_cmdline[i].index=2。//比较次设备号 continue; if (newcon->index < 0) newcon->index = console_cmdline[i].index;//将终端号赋值给serial8250_console->index,这里是2 //console_cmdline[i].options = "115200n8",对于serial8250_console而言setup字段已初始化 if (newcon->setup && newcon->setup(newcon, console_cmdline[i].options) != 0)//调用serial8250_console_setup()对终端进行配置,调用不成功 break; //在这里注册serial8250_console时,调用serial8250_console_setup()由于port->iobase和port->membase不是有效值, //故返回错误,这样下边的操作不会执行,直接break跳出,从flag1出跳出函数。即在这里serial8250_console没有注册成功 //由于内核在下边的操作队串口进行初始化时,还会调用register_console()来注册serial8250_console,在那时注册就会成功 newcon->flags |= CON_ENABLED; //设置标志为CON_ENABLE,表示console使能(这个在printk调用中使用到) newcon->index = console_cmdline[i].index;//设置索引号 if (i == selected_console) { //索引号和uboot指定的console的一样 newcon->flags |= CON_CONSDEV;//设置标志CON_CONSDEV(全局console_drivers链表中靠前) preferred_console = selected_console; } break; }//for循环作用大致是查看注册的console是否是uboot知道的引导console,是则设置相关标志和preferred_console //flag1: if (!(newcon->flags & CON_ENABLED))//若前边没有设置CON_ENABLED标志,就退出 return; if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))//防止重复打印 newcon->flags &= ~CON_PRINTBUFFER; acquire_console_sem(); if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {//如果是preferred控制台 newcon->next = console_drivers; console_drivers = newcon;//添加进全局console_drivers链表前面位置(printk中会遍历该表调用合适的console的write方法打印信息) if (newcon->next) newcon->next->flags &= ~CON_CONSDEV; } else {//如果不是preferred控制台 newcon->next = console_drivers->next; console_drivers->next = newcon; //添加进全局console_drivers链表后面位置 } //主册console主要是刷选preferred_console放置在全局console_drivers链表前面,剩下的console放置链表靠后的位置,并设置相应的flags, //console_drivers最终会在printk函数的层层调用中遍历到,并调用console的write方法将信息打印出来 if (newcon->flags & CON_PRINTBUFFER) { spin_lock_irqsave(&logbuf_lock, flags); con_start = log_start; spin_unlock_irqrestore(&logbuf_lock, flags); } release_console_sem(); if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) { printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n",newcon->name, newcon->index); for_each_console(bcon) if (bcon->flags & CON_BOOT) unregister_console(bcon); } else {//调用这里 printk(KERN_INFO "%sconsole [%s%d] enabled\n",(newcon->flags & CON_BOOT) ? "boot" : "" ,newcon->name, newcon->index); }} //serial8250_console_early_setup()-->serial8250_find_port_for_earlycon()int serial8250_find_port_for_earlycon(void){ struct early_serial8250_device *device = &early_device;//early console初始化时对early_device结构的初始化 struct uart_port *port = &device->port; int line; int ret; if (!device->port.membase && !device->port.iobase)//early_device结构初始化时已经配置好 return -ENODEV; //early console注册时不会调用此函数。 //当真正的console初始化时,会调用此函数。 //真正的console初始化时,会查找early console注册时用的是哪一个串口号,从serial8250_ports[]中根据uart_port->mapbase地址来比对 line = serial8250_find_port(port);//根据uart_port结构找到串口号,比对没有找到串口号,line返回负值 if (line < 0) return -ENODEV;//从这里返回,下边的不再执行 //若找到early console用的串口号,更新当初传入内核参数使用的console_cmdline[i],名称改成ttyS。。。。 ret = update_console_cmdline("uart", 8250, "ttyS", line, device->options); if (ret < 0) ret = update_console_cmdline("uart", 0,"ttyS", line, device->options); return ret;} static int __init serial8250_console_setup(struct console *co, char *options){ struct uart_port *port; int baud = 9600; int bits = 8; int parity = 'n'; int flow = 'n'; if (co->index >= nr_uarts)//console的索引,这里是2,即ttyS2 co->index = 0; port = &serial8250_ports[co->index].port;//找到对应的ttyS2的uart_port结构 //由于console_init在注册serial8250_console时调用的register_console()函数调用serial8250_console_setup() //进入这个函数时,由于ttyS2的uart_port结构没有初始化,port->iobase 和port->membase值都未设置,所以直接从下边返回 //当进行串口初始化时,还会回来注册serial8250_console,再调用到这里,由于设置了ttyS2的uart_port结构,所以下边的配置就会成功 if (!port->iobase && !port->membase)//第一次注册时,由于未设置,从这里直接返回 return -ENODEV; if (options)//如果options不为空,就将options里的数值写给baud, &parity, &bits, &flow uart_parse_options(options, &baud, &parity, &bits, &flow); //没有配置options,则使用缺省值,否则使用传下来的的参数options里的串口配置 return uart_set_options(port, co, baud, parity, bits, flow);} 五、通过四知道,在对console注册时,没有成功,由于串口还没有配置。当对串口配置时再对console注册就能成功。serial8250_console就能注册到内核全局变量console_drivers中。这样终端打印时就通过注册的serial8250_console就能将信息打印到终端上。 //内核的打印函数asmlinkage int printk(const char *fmt, ...){ va_list args; //可变参数链表 int r; #ifdef CONFIG_KGDB_KDB if (unlikely(kdb_trap_printk)) { va_start(args, fmt); r = vkdb_printf(fmt, args); va_end(args); return r; }#endif va_start(args, fmt); //获取第一个可变参数 r = vprintk(fmt, args); //调用vprintk函数 va_end(args); //释放可变参数链表指针 return r;} //vprintk函数asmlinkage int vprintk(const char *fmt, va_list args){ int printed_len = 0; int current_log_level = default_message_loglevel; unsigned long flags; int this_cpu; char *p; boot_delay_msec(); printk_delay(); preempt_disable(); raw_local_irq_save(flags); this_cpu = smp_processor_id(); if (unlikely(printk_cpu == this_cpu)) { if (!oops_in_progress) { recursion_bug = 1; goto out_restore_irqs; } zap_locks(); } lockdep_off(); spin_lock(&logbuf_lock); printk_cpu = this_cpu; if (recursion_bug) { recursion_bug = 0; strcpy(printk_buf, recursion_bug_msg); printed_len = strlen(recursion_bug_msg); } printed_len += vscnprintf(printk_buf + printed_len,sizeof(printk_buf) - printed_len, fmt, args); p = printk_buf; if (p[0] == '<') {//处理打印级别字段 unsigned char c = p[1]; if (c && p[2] == '>') { switch (c) { case '0' ... '7': /* loglevel */ current_log_level = c - '0'; case 'd': /* KERN_DEFAULT */ if (!new_text_line) { emit_log_char('\n'); new_text_line = 1; } case 'c': /* KERN_CONT */ p += 3; break; } } } for ( ; *p; p++) { if (new_text_line) { /* Always output the token */ emit_log_char('<'); emit_log_char(current_log_level + '0'); emit_log_char('>'); printed_len += 3; new_text_line = 0; if (printk_time) { //打印时间信息 /* Follow the token with the time */ char tbuf[50], *tp; unsigned tlen; unsigned long long t; unsigned long nanosec_rem; t = cpu_clock(printk_cpu); nanosec_rem = do_div(t, 1000000000); tlen = sprintf(tbuf, "[%5lu.%06lu] ",(unsigned long) t,nanosec_rem / 1000); for (tp = tbuf; tp < tbuf + tlen; tp++) emit_log_char(*tp); printed_len += tlen; } if (!*p) break; } emit_log_char(*p); if (*p == '\n') new_text_line = 1; } if (acquire_console_semaphore_for_printk(this_cpu)) release_console_sem(); lockdep_on();out_restore_irqs: raw_local_irq_restore(flags); preempt_enable(); return printed_len;} //接着调用release_console_sem函数 void release_console_sem(void){ unsigned long flags; unsigned _con_start, _log_end; unsigned wake_klogd = 0; if (console_suspended) { up(&console_sem); return; } console_may_schedule = 0; for ( ; ; ) { spin_lock_irqsave(&logbuf_lock, flags); wake_klogd |= log_start - log_end; if (con_start == log_end) break; /* Nothing to print */ _con_start = con_start; _log_end = log_end; con_start = log_end; /* Flush */ spin_unlock(&logbuf_lock); stop_critical_timings(); /* don't trace print latency */ call_console_drivers(_con_start, _log_end); start_critical_timings(); local_irq_restore(flags); } console_locked = 0; up(&console_sem); spin_unlock_irqrestore(&logbuf_lock, flags); if (wake_klogd) wake_up_klogd();}EXPORT_SYMBOL(release_console_sem); //调用call_console_drivers函数static void call_console_drivers(unsigned start, unsigned end){ unsigned cur_index, start_print; static int msg_level = -1; BUG_ON(((int)(start - end)) > 0); cur_index = start; start_print = start; while (cur_index != end) { if (msg_level < 0 && ((end - cur_index) > 2) &&LOG_BUF(cur_index + 0) == '<' &&LOG_BUF(cur_index + 1) >= '0' &&LOG_BUF(cur_index + 1) <= '7' &&LOG_BUF(cur_index + 2) == '>') { msg_level = LOG_BUF(cur_index + 1) - '0'; cur_index += 3; start_print = cur_index; } while (cur_index != end) { char c = LOG_BUF(cur_index); cur_index++; if (c == '\n') { if (msg_level < 0) { msg_level = default_message_loglevel; } _call_console_drivers(start_print, cur_index, msg_level); msg_level = -1; start_print = cur_index; break; } } } _call_console_drivers(start_print, end, msg_level);}_call_console_drivers函数 //调用console的写方法static void __call_console_drivers(unsigned start, unsigned end) { struct console *con; for_each_console(con) {//遍历console_drivers数组 #define for_each_console(con) for (con = console_drivers; con != NULL; con = con->next) if ((con->flags & CON_ENABLED) && con->write &&(cpu_online(smp_processor_id()) ||(con->flags & CON_ANYTIME))) con->write(con, &LOG_BUF(start), end - start); //调用console的写方法 } } //由于已经注册的终端是serial8250_console,这个终端的写方法是调用serial8250_console_write()函数--->uart_console_write()--->serial8250_console_putchar()//--->serial_out()最终打印在串口2终端上/* static struct console serial8250_console = { .name = "ttyS", .write = serial8250_console_write,//写方法 .device = uart_console_device,//tty驱动 .setup = serial8250_console_setup,//设置串口波特率,也就是设置串口。很重要,里面涉及到平台特性,波特率相关。 .early_setup = serial8250_console_early_setup, .flags = CON_PRINTBUFFER | CON_ANYTIME, .index = -1, .data = &serial8250_reg, }; */console_drivers链表在register_console中会设置