Linux内核学习(三)应用层和内核

写在前面

之前做项目的时候,有前辈告诉自己,要去学一下Linux内核,对很多方面都有帮助,现在闲下来,来花时间学一下这一部分的知识点,也算是一个学习笔记
目前跟着B站UP主——简说linux 的教程《Linux内核开发100讲》学习,链接如下:
简说linux个人空间
本章学习参考链接:
printk和printf的区别
《Linux内核设计与实现》
在学习的过程中,我也会对遇到的各种问题进行深一步学习, 从而总结知识点到博客当中,这就会出现内容可能会四处跳跃,但是这种跳跃符合我的学习过程。

整体环境

为了学习代码,我们需要一个一套Linux环境,因为为了方便自己记笔记和学习,没有用双系统,直接在windows10下面用VMware建了一个虚拟机进行试验。
开发环境:VMWare虚拟机 Ubuntu 18.04
Linux源码版本:linux4.9.229

学习笔记

这一章是关于Linux内核的一个总体印象,以及应用层和驱动层之间相互调用的逻辑关系。

操作系统和内核简介

在《Linux内核设计与实现》中认为,操作系统是指在整个系统中负责完成最基本功能和系统管理的那些部分。这些部分应该包括内核、设备驱动程序、启动引导程序、命令行Shell或者其他种类的用户界面、基本的文件管理工具和系统工具。
而内核时操作系统的核心所在,系统的其他部分都必须依靠内核这部分软件提供的服务,比如管理硬件设备、分配系统资源等等。

内核组成
负责响应中断的中断服务程序
负责管理多个进程从而分享处理器时间的调度程序
负责管理进程地址空间的内存管理程序
网络,进程间通信等服务程序

对于提供保护机制的现代系统来说,内核独立于普通应用程序,一般处于系统态,拥有受保护的内存空间和访问硬件设备的所有权限,这种系统态被保护起来的内存空间,统称为内核空间。
而与这个相对的是应用程序在用户空间执行。只能看到允许它们使用的部分系统资源,不能直接访问硬件,也不能访问其他人的内存范围。执行一个应用程序的时候,系统将以用户态进入用户空间执行。
系统中运行的应用程序通过系统调用来实现与内核通信。具体的图如下:
在这里插入图片描述
应用程序通常调用库函数(比如C库函数)再由库函数通过系统调用界面,让内核代其完成各种不同任务。一些库调用提供了系统调用不具备的许多功能。

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查看日志的时候,发现这个应用层确实调用了驱动层中的驱动设备,并输出了对应的日志内容,那么具体是怎么实现的呢?
在这里插入图片描述

实现原理

由于用户空间的程序是无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间。如果进程可以直接在内核的地址空间上读写的话,系统的安全系和稳定性将不复存在
因此,应用程序需要通过某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序在内核空间执行系统调用。
通知内核的机制是靠软中断来实现的,通过引发一个异常来促使系统切换到内核态去执行异常处理程序。这个异常处理程序实质上就是一个系统调用处理程序。处理的程序就叫做system_call()

具体的过程用图片表示如下:
在这里插入图片描述
以之前的应用层的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系统中,把所有的东西都文件化了,因此,一个设备也是一个文件,而要使用一个文件,第一步当然是先打开这个文件啦

猜你喜欢

转载自blog.csdn.net/scarecrow_sun/article/details/124377364