之前忘了在哪了,看到一个面试题:在 Linux 中如果不允许你写内核驱动,但是要访问内核寄存器,那应该怎么做?
答案就是使用 mmap()
系统调用,搭配 Linux 的一个设备节点 /dev/mem
。
本文地址:https://segmentfault.com/a/1190000008381626
Reference
/dev/mem
mmap详解
Linux驱动虚拟地址和物理地址的映射
嵌入式 Linux应用程序如何读取(修改)芯片寄存器的值
file - C - Bus error when using mmap - Stack Overflow
mmap() 和 /dev/mem
mmap()
大家都知道,是用来做内存映射的,可以将一个文件描述符映射到内存当中,实现对该文件描述符的直接读写。
/dev/mem
则关注的人比较少。这个设备节点在 Linux 中都有,是物理内存的完整映像。这个厉害了,如果你访问这个节点,实际可以这么说:你就取得了对整个系统的最高权限。在用户空间中实现设备驱动,实际上就是通过这个节点实现的。
很多跑 Linux 的芯片,会将自己的寄存器映射到内存空间的一个段中。这些相对于 /dev/mem
来说就是绝对地址上的一段内存空间而已,因此通过 mmap 对应的地址段,就可以实现对指定寄存器的访问。
Bus error
道理是这么讲,但是实际上写代码,直接尝试去映射 /dev/mem
的时候,内核却直接抛出 “Bus error” 的错误信息,把我的程序中止了。查阅资料,才知道很重要的一个信息:内存映射必须以页对齐
。
这个时候就需要用 getpagesize()
来获取页的大小啦。思路就是:当上层调用需要获取某段地址的时候,程序首先找到其对应的段起始地址,mmap 了之后再找偏移。
代码
废话少说,直接上代码(很短):
static int read_global_mem(void *out, size_t length, off_t offset)
{
uint8_t *data = NULL;
int ret = -1;
off_t pageSize = getpagesize();
size_t actualLen = 0;
off_t actualOffset = 0;
if (NULL == out || 0 == length) {
errno = EINVAL;
goto ENDS;
}
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
goto ENDS;
}
actualOffset = offset & ~(pageSize - 1); // 找到对应的段起始
actualLen = length + (offset - actualOffset); // 判断实际需要 mmap 的长度
data = mmap(NULL, actualLen, PROT_READ, MAP_SHARED, fd, actualOffset);
if (NULL == data) {
goto ENDS;
}
memmove(out, data + (offset - actualOffset), length);
/* success */
ret = 0;
ENDS:
if (fd) {
close(fd);
fd = -1;
}
if (data) {
munmap(data, length);
data = NULL;
}
return ret;
}
寄存器写
上面的代码是关于寄存器读的。寄存器写的代码基本上是差不多的,不同的有几个:
-
mmap()
的时候,改成 “PROT_READ | PROT_WRITE
” - 在共享内存中修改了指定的数据段之后,调用
msync()
将被修改了的寄存器值写回内核
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。