目录
写在前面
之前做项目的时候,有前辈告诉自己,要去学一下Linux内核,对很多方面都有帮助,现在闲下来,来花时间学一下这一部分的知识点,也算是一个学习笔记
目前跟着B站UP主——简说linux 的教程《Linux内核开发100讲》学习,链接如下:
简说linux个人空间
本章学习参考链接:
printk和printf的区别
《Linux内核设计与实现》
在学习的过程中,我也会对遇到的各种问题进行深一步学习, 从而总结知识点到博客当中,这就会出现内容可能会四处跳跃,但是这种跳跃符合我的学习过程。
整体环境
为了学习代码,我们需要一个一套Linux环境,因为为了方便自己记笔记和学习,没有用双系统,直接在windows10下面用VMware建了一个虚拟机进行试验。
开发环境:VMWare虚拟机 Ubuntu 18.04
Linux源码版本:linux4.9.229
学习笔记
这一章是关于Linux内核的一个总体印象,以及应用层和驱动层之间相互调用的逻辑关系。
操作系统和内核简介
printf()
和prinfk()
在Linux内核学习(二)里面,我们就在我们自己的设备驱动里面用了printk()函数来进行信息的输出从而进行调试,但为啥不用C语言里面的printf函数呢?
因为大部分的C语言库里面的函数在内核中都得到了实现,但是printf()函数是并没有被实现的,因此在我们前面的内核驱动设备的C语言代码中,我们是无法调用printf()
函数的,但是没有了这个函数,内核中使用了另一个函数,printk()
printk
基本上在任何时候和任何地方都能够调用,它的使用弹性极佳printk
可以指定一个日志警告级别,内核可以根据这个警告级别来判断是否需要在终端上打印信息。内核会把级别比某个特定值低的所有消息显示在终端上。记录等级如下:
记录等级 | 描述 | 记录等级 |
---|---|---|
KERN_EMEG | 一个紧急情况 | 0 |
KERN_ALERT | 一个需要立即被注意到的错误 | 1 |
KERN_CRIT | 一个临界情况 | 2 |
KERN_ERR | 一个错误 | 3 |
KERN_WARNING | 一个警告 | 4 |
KERN_NOTICE | 一个普通的,不过也有可能需要被注意的情况 | 5 |
KERN_INFO | 一条非正式的消息 | 6 |
KERN_DEBUG | 一条调试信息——一般是冗余信息 | 7 |
如果没有特别指定一个记录等级,函数会选用默认的DEFAULT_MESSAGE_LOGLEVEL,默认等级是KERN_WARNING,内核里面最重要的记录等级是KERN_EMEG,按照表格从上往下对应从重要到不重要的记录等级,最无关紧要的是KERN_DEBUG。当记录等级低于默认等级的时候,不会在终端里面显示,而显示在日志里面
sudo dmesg
查看日志信息
sudo dmesg -C
清除日志信息
我们可以查看当前的记录等级
cat /proc/sys/kernel/printk
我们试着输入一下:返回了 4 4 1 7
cat /proc/sys/kernel/printk
4 4 1 7
输出结果中的四个数字分别代表当前记录等级
,默认等级
,最小记录等级
,和最大记录等级
。
由前面得知,低于默认等级的时候,不会在终端显示,而在日志显示,因此等级0-3
会输出到终端,4-7
只会显示在日志当中.
我们可以使用sudo echo "6" > /proc/sys/kernel/printk
来改变系统的默认等级。注:此改变方法,可能需要使用sudo su
切换到root用户才可以修改。
应用层对内核的调用
在我前面写的Linux内核学习(二)里面,我们已经实现了一个设备驱动的插入,并且其设备具有三个操作,open
,write
,read
。那当我们有了一个设备之后,我们就可以在我们应用层通过系统调用的接口调用设备驱动,从而实现对硬件设备的调用。当然,由于我们是没有具体的硬件的,所以我们省略最后一步设备驱动对硬件设备的调用,只了解应用层如果实现对内核里面的设备驱动的调用
从例子看原理:应用层的write()
如何调用内核中的write()
调用过程实践
要实现这个例子,我们当然首先要手写一个应用层的程序test.c,随便放到哪,我把学习过程中的笔记按照注释写在代码中,具体如下:
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#define DATA_NUM (64)
int main(int argc, char *argv[])
{
int fd,i;
int r_len,w_len;
fd_set fdset;
char buf[DATA_NUM]="hello world"; //创建一个字符缓冲数组,以便后续使用
memset(buf,0,DATA_NUM); //将DATA_NUM中剩余的值填充到buf中
fd = open("/dev/hello", O_RDWR); //用可读写的权限打开设备驱动hello
printf("%d\r\n", fd);
if(-1 == fd) //判断文件是否打开
{
perror("open file error\r\n");
return -1;
}
else
{
printf("open successe\r\n");
}
w_len = write(fd, buf, DATA_NUM); //打开成功 就调用write和read
r_len = read(fd, buf, DATA_NUM);
printf("%d%d\r\n", w_len, r_len); //将返回值打印出来
printf("%s\r\n",buf);
return 0;
}
然后我们来运行这个运行这个程序
我们先按照最基本的C语言的流程,编译它。
gcc -o test test.c
然后我们运行它
./test
但我们发现它无法打开文件,返回以下错误
-1
open file error
: No such file or directory
其原因是,虽然我们在内核里面注册了我们的内核驱动,但是我们在应用层里面没有建立这样的一个文设备件。虽然在现在的Linux内核可以自动生成这样的设备文件,但UP主给我们演示了具体的实现过程:
首先,我们需要创建一个设备文件,需要使用mknod
命令,其用法如下:
mknod [OPTION ]NAME TYPE [MAJOR MINOR]
TYPE是设备的类型,MAJOR和MINOR指的是主设备号和次设备号
mknod /dev/hello c 232 0
# 由于我们应用层中写的打开设备时hello,所以这里名字和代码中的文件名一样
# c 代表着这是一个字符设备
# 232 0 是我们上一节写的内核注册的驱动文件中的主次设备号
ls -l /dev/hello
# 此时再用ls命令就可以看到返回的设备文件啦
然后我们再清空一下我们的日志,并执行测试程序,
sudo dmesg -C
./test
发现返回了以下数据
这样,我们就运行完了了我们应用层的软件代码了,上述的过程中,我们是调用了我们前面写的驱动来进行的,而在我们前一节的笔记中,刚好在内核的驱动文件中,有三个对应应用层软件代码中open
,write
,read
的函数,并对其进行了指向具体。代码如下:
int hello_open(struct inode *p, struct file *f)
{
printk(KERN_EMERG"hello_open\r\n");
return 0;
}
ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_write\r\n");
return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_read\r\n");
return 0;
}
ps:函数里面的loff_t是一个类型的声明,其本质就是一个long long类型
这个时候,我们再使用dmesg
查看日志的时候,发现这个应用层确实调用了驱动层中的驱动设备,并输出了对应的日志内容,那么具体是怎么实现的呢?
实现原理
具体的过程用图片表示如下:
以之前的应用层的write()
为例,具体的过程如下:
首先应用层的代码执行之后,产生一个中断,然后被系统调用程序进行处理。具体的就是一个SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,size_t, count)
这样的一个函数
然后这个调用会将其调用到ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos)
的函数,就是在这个函数中,将我们设备驱动中的read
,open
,write
三个函数给调用了。
最终逐层传递回参数给用户空间,从而实现了整个过程
学习笔记
在上述的test.c文件中,有以下几个需要注意的点
- 我们在上面的test.c文件中调用的是
printf()
函数,而不是printk()
,原因是这个c文件是一个应用层的软件,而不是内核中的驱动文件,所以他是可以直接调用C库中的函数,并打印到终端中。 - 我们打开设备的操作是
open()
,这是因为Linux系统中,把所有的东西都文件化了,因此,一个设备也是一个文件,而要使用一个文件,第一步当然是先打开这个文件啦