我们读一个文件的时候是需要系统调用的,也就是从用户态切换到内核态,然后内核态读取数据,再将数据拷贝到用户态,这中间的拷贝到底是个什么过程,内存里我们读取的这个文件的数据到底有几份,我觉得应该是一份,内核态拷贝到用户态应该只是把这份文件的地址给了用户态,不知道我理解的有没有问题...
1
crclz 2020-07-04 15:20:36 +08:00
ssize_t read (int fd, void *buf, size_t count); 这是一个系统调用。拷贝就指的是将磁盘上的数据拷贝到 buf (第二个参数)的过程.
|
2
zhgg0 2020-07-04 15:22:19 +08:00
我的理解是将内核态的数据拷了一份到用户态,不是给地址。
|
3
zhgg0 2020-07-04 15:25:39 +08:00
接#2
假设有两个进程都要读某一个文件,A 先读,B 再读; A 读的时候操作系统把文件加载到内核态,把这个数据直接拷了一份给 A,B 再度,此时大概率操作系统还缓存了数据,再拷贝一份给 B ;如果直接给地址的话,不好处理 A 、B 同时修改内容,或者 A 和操作系统同时要修改内容这种情况。 |
4
billlee 2020-07-04 15:25:57 +08:00
不是的,真要拷贝,给地址的话那岂不是还要考虑内存回收问题
|
5
xeaglex 2020-07-04 15:27:10 +08:00 via Android
你先了解一下 DMA 的功能,剩下的你就全懂了
|
6
ZRS 2020-07-04 15:39:40 +08:00
就是真的在内存里复制一遍。。
|
7
reus 2020-07-04 15:44:09 +08:00
都说了拷贝了,怎么可能是给地址
但凡知道 read 调用的都不会问出这种问题 |
8
Jooooooooo 2020-07-04 15:55:03 +08:00
如果是地址的话, 你要真的访问数据得去磁盘上拿这得多慢啊
各种都是都是有缓存的, 网卡, 磁盘, 内存, cpu 全是 cache 在干活, cpu 真的要计算东西也是本地缓存跑, 每次都是内存拿东西要慢死 |
9
louettagfh 2020-07-04 17:26:09 +08:00
是真的拷贝一次啊
函数就是 copy_to_user() unsigned long copy_to_user ( void __user * to, const void * from, unsigned long n); |
10
louettagfh 2020-07-04 17:31:41 +08:00 1
接上一条 拷贝的原因是因为进程申请的 buf 都是用户空间的虚拟地址,就如你用 mmap 也是可以直接访问内核态的地址空间,这就省去了一次拷贝
|
11
DoctorCat 2020-07-04 19:40:05 +08:00 2
顶楼上,再补充一下:
1. 文件读取操作是需要用户态 和 内核态一起互动的,为啥?文件系统层 VFS 就这么设计的,可能是迄今为止最周到最稳固的设计。执行过程要考虑到方方面面,例如页缓存、文件于页映射、块缓存等等复杂的机制。 2. mmap 这个系统调用可以直接映射到可执行文件代码和只读数据。脱离了文件系统层( 见内核函数实现 do_mmap_gpoff ) |
12
zivyou 2020-07-04 23:45:37 +08:00
顶 @louettagfh 就是 copy_to_user()。至于读文件的过程,默认情况下内核会有个缓冲区
|
13
zmxnv123 2020-07-05 00:38:17 +08:00 via iPhone
文件系统是操作系统的一部分。对于硬盘中的每个 block 会对应内核中的一块相同大小的 buffer,从硬盘拷贝到内核的 buffer 是第一次拷贝,发生在内核态,之后要把内核 buffer 中的内容拷贝到用户态的 buffer 。
|
14
vk42 2020-07-05 00:42:40 +08:00
@louettagfh mmap 也是用户空间,并不会直接访问内核地址空间……
|
15
janxin 2020-07-05 07:28:13 +08:00
Linux 新内核的机制 io_uring 就是提升这个性能的,具体可以看一下相关介绍里也有提到
|
17
louettagfh 2020-07-05 09:59:51 +08:00
@vk42 自己看源码吧 懒得解释了
|
18
vk42 2020-07-05 10:33:53 +08:00
@louettagfh 你确定你能搞清虚拟地址空间和物理地址区别? mmap 可以把 page cache 映射到进程的用户态空间以实现 zero copy,但这不是访问内核态地址空间好么
|
19
louettagfh 2020-07-05 10:40:55 +08:00
@vk42 vma 映射的不是内核地址空间? 访问 vma 不是访问内核地址空间?
|
20
vk42 2020-07-05 10:48:04 +08:00
@louettagfh
你去查一下内核地址空间的定义再说吧(我怀疑你把物理地址理解成所谓的内核地址空间了) 另外 vma 并不是真正映射的地方,vma 只是对地址空间做 book keeping,真正的映射是 page table 的活。 |
21
louettagfh 2020-07-05 10:54:47 +08:00
@vk42 去看看源码 mmap_region() --> call_mmap(file, vma)
vma 指向的是哪里 你说 page cache, 那 page cache 在哪里? 块设备接口读的内容存在哪里的? |
22
vk42 2020-07-05 11:07:54 +08:00
@louettagfh
无语了,你先把两个基本概念内核地址空间和物理地址搞清楚好吧。你不妨做个实验,看看 page cache 的地址和用户 mmap 得到的地址是一个地址吗? (怎么感觉又回到以前给学生出题的时候了……) |
23
louettagfh 2020-07-05 11:12:35 +08:00
@vk42 听不懂我说什么就算了
|
24
vk42 2020-07-05 11:16:00 +08:00
@louettagfh
你高兴就好哈,我也是无聊到不行在这种低级错误上这么较真…… |
25
louettagfh 2020-07-05 11:25:56 +08:00
|
26
lyi4ng 2020-07-05 11:29:44 +08:00
是真的在内存里拷贝一遍啊,就是把物理内存 A 段上的内容拷贝到 B 段,而 B 段有个虚拟内存映射,只不过再校验下 flag 决定是不是 COW,不然会是一个 zero page,涉及到的概念有虚拟内存和物理内存之间的分页机制还有 COW
|
27
vk42 2020-07-05 11:33:00 +08:00
@louettagfh
摆脱你自己基本概念都搞不懂就先别扯别的了好吧。源码我写文件系统的时候已经看够多了,而且你这个问题根本不需要拿源码说事。那我不误人子弟你不妨指教一下 page cache 在哪里? |
28
aheadlead 2020-07-05 11:33:07 +08:00
@louettagfh #25 。。。。映射后的也是用户地址空间啊
|
29
louettagfh 2020-07-05 12:05:16 +08:00
@aheadlead 我当然知道 mmap 返回的是用户地址空间
读写文件: 用户调用 mmap --> 申请 VMa --> VMa->file 指向对应的 file --> address_space --> page cache 真正读写的时候触发缺页中断,kernel 读 page 至 page cache, 用户通过 VMa 可以直接访问 page, 难道用户进程还能直接读文件? |
30
aheadlead 2020-07-05 12:24:59 +08:00
|
32
no1xsyzy 2020-07-05 14:31:22 +08:00
@louettagfh #10 请区分:地址空间 vs 内存
你术语用错了…… 访问“属于内核的内存”并不一定通过“内核地址空间”,直接访问内核地址空间那还叫 “映射”( map ) 干嘛…… 何况,地址空间映射目标不一定在内存上,甚至不一定是电子元件。 |
33
no1xsyzy 2020-07-05 14:34:49 +08:00
@Jooooooooo #8 我觉得说的 “地址” 是指 “指针” 而不是 “目录” 或者……
直白地讲,指针是 C 叫法,地址是 ASM 叫法。 |
34
Jooooooooo 2020-07-05 14:36:20 +08:00
@no1xsyzy 不用扣这个细节. 简单说就是你拿着这个东西能不能知道这个东西内容是什么, 如果能就是内容, 如果不能, 就是索引 /指针 /地址.
|
35
no1xsyzy 2020-07-05 14:44:21 +08:00
@Jooooooooo #34 我是说楼主并不是认为 read 之后还得再去磁盘读,而是 read 完之后返回一个指针,这个指针内已经存储了需要的内容。
何况 C 里面超过 4 字节(或者 64-bit 后 8 字节)就没有内容,只有指针。 |
36
liuxu 2020-07-05 15:34:04 +08:00 1
@louettagfh #10 https://man7.org/linux/man-pages/man2/mmap.2.html
mmap() creates a new mapping in the virtual address space of the calling process. The starting address for the new mapping is specified in addr. The length argument specifies the length of the mapping (which must be greater than 0). If addr is NULL, then the kernel chooses the (page-aligned) address at which to create the mapping; this is the most portable method of creating a new mapping. If addr is not NULL, then the kernel takes it as a hint about where to place the mapping; on Linux, the kernel will pick a nearby page boundary (but always above or equal to the value specified by /proc/sys/vm/mmap_min_addr) and attempt to create the mapping there. If another mapping already exists there, the kernel picks a new address that may or may not depend on the hint. The address of the new mapping is returned as the result of the call. The contents of a file mapping (as opposed to an anonymous mapping; see MAP_ANONYMOUS below), are initialized using length bytes starting at offset offset in the file (or other object) referred to by the file descriptor fd. offset must be a multiple of the page size as returned by sysconf(_SC_PAGE_SIZE). After the mmap() call has returned, the file descriptor, fd, can be closed immediately without invalidating the mapping. |
37
Jooooooooo 2020-07-05 15:36:10 +08:00
@no1xsyzy 啥叫指针存储了需要的内容, 一个指针好几兆大小?
|
38
yangyuhan12138 OP @louettagfh
@zhgg0 @vk42 @lyi4ng @no1xsyzy @Jooooooooo 谢谢大家的热心解答,不过感觉大家说的都太专业了,我是做 Java 开发的,确实对底层的系统层面的知识不是很了解,主要是我最近看了 fork,还有零拷贝这些知识,对有些概念还是很模糊,有个大概认识,比如 fork 就是将虚拟内存考了一份,然后写时复制,然后就是读文件在内核态和用户态进行切换的问题了(我在试图把这些内容串起来理解) 我再把我的想法描述清楚点,我认为内核态和用户态可以粗略的理解为内核进程(权限高,想干嘛就干嘛)和用户进程(权限低很多事干不了)吧?(我不知道对不对),然后是因为用户进程没办法和磁盘进行交互读写,所以需要调用内核进程来完成相应的功能,于是我们调用读文件的时候,其实系统是切换到了内核进程执行读文件的代码,然后将文件内容读到了内存里,看大家完大家的说法,感觉这个应该是读到了内核进程的专属一块内存里,用户进程依然没法访问,所以才又将内容拷贝了一份?现在内存里有了两份文件的内容? 但是我还是有点不明白,如果是按照进程来理解的话我们操作的都应该是虚拟地址才对,为啥内核不直接把读进来的内容在内存上的物理地址告诉用户进程(比如是哪几页),然后用户进程维护个虚拟地址就好了,为啥还要在物理内存上考一份,这个文件的内容在物理内存里到底是一份还是两份? 我觉得我现在的问题可能是不太明白啥是用户态和内核态 |
39
yangyuhan12138 OP @louettagfh
@zhgg0 @vk42 @lyi4ng @no1xsyzy @Jooooooooo 是否内存在一开始就已经被分为了两部分 一部分是内核可访问的,一部分是用户可访问的,但是读出来的数据被放在了内核可访问的内存区域,所以要将他拷贝到用户可访问的内存区域 |
40
FutherAll 2020-07-05 17:55:19 +08:00
一般是用 PTE (页表项)的权限位去控制的,至于为什么要二次拷贝,我理解也是为了权限控制,进程用户态的内存空间是当前进程可访问的;内核态的 Page Cache 可以共享给多个进程,由内核控制
用 mmap 可以避免二次拷贝。 |
41
no1xsyzy 2020-07-05 18:00:53 +08:00
物理地址是不暴露给用户的,你就算拿到也基本不能用。
因为内核访问用户空间方便,用户态访问内核空间要更麻烦地精细控制。所以 read 是用户提供一个(比如通过 mallocate 申请之后)分配到的地址空间,然后通知内核存进去。至于如何存,存完内核是否仍然保留,这是内核具体实现决定的。一次 call 至多存多少,存到哪是用户 call 参数决定的。 因此文件内存映射是一种优化,把文件读到内核,然后把某些地址空间设置为映射,对其的访问翻译为对已读取的文件的访问。这其中没有内存整片复制过程。 内存不会被直接访问,由内核来控制访问。分“页”来标记。用户空间内访问地址会被翻译为具体的真实内存地址(内核控制,CPU 实际操作)。甚至用户空间存在的页不一定真实有一个对应的内存,待到访问的时候触发缺页中断,再去问内核这块该是啥。 @Jooooooooo #37 指针内存储了 ✔ 指针存储了 ✘ 指针内 一般就是指指针指向的地址空间。大概。 |
42
ltoddy 2020-07-05 18:31:07 +08:00
首先应该解释为什么有内核态,和用户态。内核态可以操作比如硬件,我们不想把这些危险的操作直接暴露给用户,而是封装起来给用户。内核态与用户态分别实现,当用户态的程序挂了之后,内核态不受影响,我们的操作系统照样运行的好好的。
那么这时候,自己思考一下,为什么要拷贝,如果不拷贝,会怎么样。 |
43
aheadlead 2020-07-05 20:35:54 +08:00
|
44
xyjincan 2020-07-06 09:41:34 +08:00
用户态的东西,用户有读写权限,内核态可能用户只有读权限,如果用户态只读一个内核地址,那不是可以直接绕过内核态修改来了
|
45
lyi4ng 2020-07-06 12:06:25 +08:00 1
这些东西都要考虑发展史的,内核态用户态的设计是一种非对称访问机制,这主要是为了安全考虑,即将仅有的一块内存划分成两部分区分出权限来,这是迎合 CPU 的特权级,因此实际上来说按照 CPU 特权级才有用户态内核态一说,而按照内存空间划分叫做内核空间 /用户空间。你再想一个事情,32 位 CPU 寻址能力是 4G,那我差了 8G 内存条怎么说?就光说内核态要求能访问到所有的内存单元,你知道实现方式吗?高端内存因为 CPU 的寻址能力发展直接在 64 位里被淘汰了,然后再说你想的虚拟地址物理地址,内核空间里也有虚拟地址,只不过和物理地址的关系上不像用户空间那样复杂,而用户空间大家告诉你是分页,那为什么就要分页?为什么要四级分页?一级二级不行吗?一级二级分页都是出现过的,但是因为计算机的发展又慢慢被淘汰了,多看背景再学机制,因为 kernel 的那群人大部分都很骄傲,如果没有需求驱动或者不是灵光一闪的话,你以为他们真的会改进代码,笑话,当年 COW 出脏牛的时候 linus 还不是发邮件喷人了。
(来自于因为大雨而迟到的发泄,语气过激请见谅) |
47
yannxia 2020-07-06 13:43:51 +08:00
不明白很正常……
讲道理按照逻辑说,的确是应该将 地址直接从内核态作为返回值直接给用户态就可以了。但是在实际上因为安全 /硬件 还有一些历史原因导致,最基础的传递是通过 Copy 来的,之后才出现的 Zero Copy 的技术。 OS 因为要配合硬件,所以不是一个很纯粹的软件设计思想,这个只能按照他的思路来。 |