最近的同事的组内分享的内容就是标题的内容,这是一道大数据处理的算法的经典面试题,以前也经常碰到,用到的是mmap的知识,了解过,但是没有自己实际去写过。趁这个机会,就再一次回顾下,用swift来实现一把。
参考文献
特权级, 用户态,内核态,用户空间,内核空间,线程上下文,中断上下文, MMU等概念请参考下文
分析
由于指定数据的内容比较大,所以我们不能直接通过read,write的方式将大文件直接读入内容,因为这样内存会不够用,所以我们需要借助linux中的mmap的知识,进行内存映射。
mmap工作原理
mmap的工作原理,当你发起这个调用的时候,它只是在你的虚拟空间中分配了一段空间,连真实的物理地址都不会分配的,当你访问这段空间,CPU陷入OS内核执行异常处理,然后异常处理会在这个时间分配物理内存,并用文件的内容填充这片内存,然后才返回你进程的上下文,这时你的程序才会感知到这片内存里有数据
mmap函数
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
具体参数含义
start : 指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length: 代表将文件中多大的部分映射到内存。
prot : 映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
flags : 影响映射区域的各种特性。在调用mmap()时必须要指定
MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
fd : 要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,
然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是PAGE_SIZE的整数倍。
复制代码
返回值: 若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
错误代码: EBADF 参数fd 不是有效的文件描述词 EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。 EINVAL 参数start、length 或offset有一个不合法。 EAGAIN 文件被锁住,或是有太多内存被锁住。 ENOMEM 内存不足。
用户层的调用很简单,其具体功能就是直接将物理内存直接映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。
munmap函数
通过mmap映射出来的内存,通过munmap来解除映射关系
int munmap( void * addr, size_t len )
在进程地址空间中解除一个映射关系,当映射关系解除后,对原来映射地址的访问将导致段错误发生。
void * addr :调用mmap()时返回的地址
size_t len :映射区的大小
复制代码
msync函数
int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以调用msync()实现磁盘上文件与共享内存区的内容一致。
void * addr :调用mmap()时返回的地址
size_t len :映射区的大小
int flags :MS_ASYN: 异步写,MS_SYN : 同步写,MS_INVALIDAT : 无效的cache 数据。
复制代码
具体代码如下
func handleFile() {
let filePath = Bundle.main.path(forResource: "data", ofType: "txt")
let filePathOut = Bundle.main.path(forResource: "dataout", ofType: ".txt")
let fhIn = FileHandle.init(forReadingAtPath: filePath!)
let fhOut = FileHandle.init(forWritingAtPath: filePathOut!)
// 清空输出文件的内容
fhOut?.truncateFile(atOffset: 0)
// 获取文件大小
let fileSize = fhIn?.seekToEndOfFile()
// 由于要处理的文件较大,所以不能一次将文件全部映射进内存,采用一个合适的映射内存大小。
// 通过count来执行映射次数
let count = fileSize! / MEM_SIZE
// 剩余部分内存
let leftSize = fileSize! % MEM_SIZE
// 两部分分开处理,剩余部分写入
// 文件最后的,写入文件的最开始位置
let leftPart = mmap(UnsafeMutableRawPointer.init(bitPattern: 0), Int(leftSize), PROT_READ, MAP_SHARED, fhIn!.fileDescriptor, off_t(MEM_SIZE * count))
if leftPart == MAP_FAILED {
print("剩余部分映射失败)")
return
}
let leftBuf = malloc(Int(leftSize))
memcpy(leftBuf, leftPart, Int(leftSize))
var data = Data.init(bytes: leftBuf!, count: Int(leftSize))
data.reverse()
fhOut?.write(data)
fhOut?.synchronizeFile()
munmap(leftPart, Int(leftSize))
free(leftBuf)
print("剩余部分写入成功")
// 多线程处理大小的内存,加快处理速度
let queue = OperationQueue.init()
queue.maxConcurrentOperationCount = 5 // 设置最大并发数,线程太多,因为线程切换,速度反而也降低
// 使用信号量,防止资源写入的时候,多线程seek文件的问题
let semaphore = DispatchSemaphore.init(value: 1)
// 从第0到count段数据,分别写入文件的相应位置
for i in 0..<count {
queue.addOperation {
let part = mmap(UnsafeMutableRawPointer.init(bitPattern: 0), Int(MEM_SIZE), PROT_READ, MAP_SHARED, fhIn!.fileDescriptor, off_t(i * MEM_SIZE))
if part == MAP_FAILED {
print("映射失败 i = \(i)")
return
}
let buf = malloc(Int(MEM_SIZE))
memcpy(buf, part, Int(MEM_SIZE))
var data = Data.init(bytes: buf!, count: Int(MEM_SIZE))
data.reverse()
free(buf)
semaphore.wait() // 抢占信号资源
fhOut?.seek(toFileOffset: leftSize + MEM_SIZE * (count - i - 1))
fhOut?.write(data)
fhOut?.synchronizeFile()
semaphore.signal() // 释放信号资源
munmap(part, Int(MEM_SIZE))
print("操作成功 i= \(i)")
}
}
//等队列中所有操作结束,才能执行后面的close句柄的操作
queue.waitUntilAllOperationsAreFinished()
fhIn?.closeFile()
fhOut?.closeFile()
}
复制代码