【一】mmap函数
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
addr: 指定映射区的首地址。通常传 NULL,表示让系统自动分配
length:共享内存映射区的大小。(<= 文件的实际大小)
*如果使用length > 文件大小,那么就会出现总线错误*
prot: 共享内存映射区的读写属性。PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
shared 意思是修改会反映到磁盘上
private 表示修改不反映到磁盘上
fd: 用于创建共享内存映射区的那个文件的 文件描述符。
offset:默认 0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
返回值:
成功:映射区的首地址。
失败:MAP_FAILED (void*(-1)), errno
函数注释写的非常详尽了,其中DDR的单页大小也是4k与mmap保持一致。
【二】DDR读数据代码(写是差不多)
#define MAP_SIZE 4096UL
#define MAP_MASK (MAP_SIZE -1)
/**
* @brief 从实际物理地址读取数据。
* @details 通过 mmap 映射关系,找到对应的实际物理地址对应的虚拟地址,然后读取数据。
* 读取长度,每次最低4字节。
* @param[in] readAddr, unsigned long, 需要操作的物理地址。
* @param[out] buf,unsigned char *, 读取数据的buf地址。
* @param[in] bufLen,unsigned long , buf 参数的容量,4字节为单位,如 unsigned long buf[100],那么最大能接收100个4字节。
* 用于避免因为buf容量不足,导致素组越界之类的软件崩溃问题。
* @return len,unsigned long, 读取的数据长度,字节为单位。如果读取出错,则返回0,如果正确,则返回对应的长度。
*/
static int Devmem_Read(unsigned long readAddr, unsigned long* buf, unsigned long len)
{
unsigned long i = 0;
int fd;
long unsigned int offset_len = 0;
void *map_base, *virt_addr;
off_t addr = readAddr;
if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) == -1)
{
printf("Error at line %d, file %s\n", __LINE__, __FILE__);
return 0;
}
/* Map one page */ //将内核空间映射到用户空间
map_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, addr & ~MAP_MASK);
if(map_base == (void *) -1)
{
printf("Error at line %d, file %s\n", __LINE__, __FILE__);
close(fd);
return 0;
}
for (i = 0; i < len; i++)
{
// 翻页处理
if(offset_len >= MAP_MASK)
{
offset_len = 0;
if(munmap(map_base, MAP_SIZE) == -1)
{
printf("Error at line %d, file %s\n", __LINE__, __FILE__);
close(fd);
return 0;
}
map_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, addr & ~MAP_MASK);
if(map_base == (void *) -1)
{
printf("Error at line %d, file %s\n", __LINE__, __FILE__);
close(fd);
return 0;
}
}
virt_addr= map_base + (addr & MAP_MASK); // 将内核空间映射到用户空间操作
buf[i] = *((unsigned long *) virt_addr); // 读取数据
addr += 4;
offset_len += 4;
}
if(munmap(map_base, MAP_SIZE) == -1)
{
printf("Error at line %d, file %s\n", __LINE__, __FILE__);
close(fd);
return 0;
}
close(fd);
return i;
}
这里有几个变量需要注意,map_base(映射后基地址),virt_addr(映射后访问地址),Readaddr(需要读的物理地址)。
我们来细分一下读取的步骤:
(1)打开/dev/mem设备
(2)mmap映射物理地址
因为一次性映射4k,多余4k则需要翻页(重新映射),所以在offset中填入的是[ addr & ~MAP_MASK],原式是:addr & ~(4096- 1)→addr整除4096后的起始数。
得到了基地址map_base之后,则根据读取位置的偏移来进行地址偏移:virt_addr= map_base + (addr & MAP_MASK),即在map_base的基础上加上addr整除4096后的余数。
如果用公式表示addr,设addr = 4096*a + b,可得:
addr &~ (4096 -1) = 4096*a。
addr & (4096 - 1) = b。
用图形来表示的话则如下图:
如果你需要操作的内容长度大于4k了,则在指针运行至4096*(a+1)时就需要进行跨页,然后被映射的扇区就是4096*(a+1)了。
(3)对于映射的片区进行操作,读/写都可
值得注意的是,这个demo的读写都是按照uint32的形式来做的,即每四个字节存到一个buf里,从而地址的偏移为每操作一次+4。这里需要区分len和offset_len,len是按照uint32的长度来取的,在循环中填写的为循环执行len次。offset_len则是按照uint8来记录目前操作的长度,当offset_len超过了MAP_MASK时,则进行翻页。
疑点:
(1)根据目前针对于MMAP的学习来看,length长度只需要小于文件实际大小/长度即可,那么对于DDR来说,是否可以意味着我可以一次性映射8k、16k甚至以mb为单位的长度,然后按照映射以4k为单位,假如我设长度为6k,实际上分配的空间就是8k。这样的话,是否意味着我可以不用翻页?现在尚不知能否在长度上超过/dev/mem的大小,如果以4G为大小的话,基本上不存在设置大小大于文件大小的情况。无论是文件大小本身等于0,还是设置大小大于文件大小,这两种情况都可以视为出错,不在讨论的范围内。经过尝试,我已在DDR上一次性mmap 16K的空间,并对其进行连续的读写,事实证明没必要以4K为大小进行循环映射。但是在mmap的最后一个参数,一定要是0或者4k的倍数,因为需要进行对齐!
(2)offset_len这里的处理有待商榷,如果我的理解是对的,那么在读取地址有余数时,假设为0x10001010,则addr & MAP_MASK = 0x10,那么我从这个地址开始写,offset_len初始为0,addr写过了4K扇区的区域,offset_len依然没有大于 等于MAP_MASK。假如循环进行1020次,那么此时addr为:0x10001010 + 0xFF0 = 0x10002000,但是offset = 1020*4 = 0xFF0 < 0xFFF,那么此时按道理说就已经需要跨页了,但并没有触发跨页的条件。因此,个人认为offset_len应该在第一次mmap之后,赋初值为:offset_len = addr & MAP_MASK,再在循环内进行+4。
【后记】
经过了思考与对比,最后对mmap进行一个总结,个人认为最为重要的就是“取多少,用多少”,往往大部分segment fault都出现在取的少,用的多,哪怕你是取的多,用的少都无所谓。下面我将用图去详解mmap应该注意的问题:
(1)取出大小和offset对齐之间的关系
(2)map_base的真实含义与偏移取值
假如我这样设置代码:
#define MAP_SIZE 1024*16
#define MAP_MASK (4096UL-1)
#define PL_WRITE_DDR_ADDR 0x10000100
offset_len = PL_WRITE_DDR_ADDR&MAP_MASK;
map_base = mmap(0,MAP_SIZE,PROT_READ | PROT_WRITE, MAP_SHARED, fd,PL_WRITE_DDR_ADDR & ~MAP_MASK);
for(i = 0;i<LEN;i++)
{
virt_addr = map_base + i*4 + offset_len;
*((unsigned long *)virt_addr) = buf[i];
printf("addr = 0x%08x,Write buf[%d] = %d\n",addr,i,buf[i]);//运行到addr = 0x0x10003ffc时报错
addr += 4;
}
乍一看,就是从PL_WRITE_DDR_ADDR 映射16K的空间嘛,结果呢,在操作到0x10004000时,发生了segment fault,为什么?因为offset的4K对齐限制,所以我们在mmap的offset填写的是(PL_WRITE_DDR_ADDR &~ MAP_MASK),看起来很合理,这里没有问题。但是就是因为如此,实质上的offset的值是0x10000000,也就是说,实际上,我取出的空间是:0x10000000~0x10004000。但是我读写的时候,是不是要严格按照PL_WRITE_DDR_ADDR 来?否则它的设置就没有意义了,因此,我在使用时在写入第一个数据的时候是不是就对map_base进行了偏移,偏移量是PL_WRITE_DDR_ADDR&MAP_MASK = 0x100。那么我在循环里,还是按照文件已有的长度16k去寻址的话,是不是就是从0x10000100~0x10004100。很明显,我使用的空间超过了我取出的空间。
图形说明如下:
所以正确的操作应该是,我应该取出(MAP_SIZE + offset_len)的空间,这样就可以取到后面的数据了,这也是为什么,当ADDR是4k的整数倍时程序正常运行,但稍微剩余一点程序就报错。
offset_len = PL_WRITE_DDR_ADDR&MAP_MASK;
map_base = mmap(0,(MAP_SIZE+offset_len),PROT_READ | PROT_WRITE, MAP_SHARED, fd,PL_WRITE_DDR_ADDR & ~MAP_MASK);