├── README.md ├── arp-neighbour.md ├── cfs-scheduler.md ├── cgroup-principle.md ├── cgroup.md ├── concurrency-synchronize.md ├── copy-on-write.md ├── direct-io.md ├── eBPF.md ├── epoll-principle.md ├── filesystem-generic-block-layer.md ├── hugepage.md ├── hugepages-source-code-analysis.md ├── images ├── 8259A.png ├── APIC.gif ├── APIC.png ├── aio │ ├── aio.png │ ├── io_submit_once.png │ ├── kioctx-struct.png │ ├── linux-native-aio.png │ └── ring-buffer.png ├── arp-broadcast.png ├── arp-header.png ├── buffered-io-2.jpg ├── buffered-io.png ├── cgroup-base.jpg ├── cgroup-links.jpg ├── cgroup-rule1.jpeg ├── cgroup-rule2.jpeg ├── cgroup-rule3.jpeg ├── cgroup-rule4.jpeg ├── cgroup-state-memory.jpg ├── cgroup-subsys-state.jpg ├── cgroup-task-cssset.jpg ├── concurrency-synchronize-1.png ├── concurrency-synchronize-2.png ├── concurrency-synchronize-3.jpg ├── concurrency-synchronize-4.jpg ├── concurrency-synchronize-semaphore.jpg ├── concurrency-synchronize-spinlock.jpg ├── cpu-timeline.png ├── csf-runqueue.png ├── device-block.jpg ├── dr-arch.jpg ├── dr-package.jpg ├── eflags-register.png ├── epoll-eventpoll.jpg ├── epoll_principle.jpg ├── ext2-filesystem.png ├── ext2_filesystem.png ├── hugepages │ ├── hugepages-free-list.png │ ├── hugepages-mapping.png │ ├── mmap-syscall.png │ ├── vmemory-mapping.png │ └── vmemory-pmemory.png ├── inotify2 │ ├── inotify-device-handle-watch.png │ ├── inotify-device.png │ ├── inotify-events-list.png │ ├── inotify-principle.png │ └── inotify-watch-list.png ├── interrupt_hardware.gif ├── ip-address-1.png ├── ip-address.png ├── ip-header.png ├── ip-message.png ├── ip-network-2.png ├── ip-package.png ├── ip-router.png ├── irq_desc_t.jpg ├── kprobes │ └── kprobe-struct.png ├── kswapd.png ├── lining-physical-mapping.jpg ├── linux-filesystem.jpg ├── lvs-connection-process.png ├── lvs-connection.png ├── lvs-hooks.png ├── lvs-roles.png ├── lvs-scheduler.png ├── memory-address.jpeg ├── memory-mapping │ ├── copy-on-write.png │ ├── memory-mapping.png │ └── share-memory.png ├── memory_free_area.jpg ├── memory_free_list.png ├── memory_lru.jpg ├── memory_map.jpg ├── memory_page.jpg ├── memory_slab.png ├── memory_slab_global.png ├── memory_zone.gif ├── minix-filesystem-read.jpg ├── minix_filesystem.png ├── minix_filesystem_inode.jpg ├── mmap-memory-address.png ├── nat-arch.jpg ├── nat-package.jpg ├── neighbour-arp-queue.png ├── neighbour-nodes.png ├── neighbour-struct-new.png ├── neighbour-struct.png ├── net-bridge │ ├── bridge-packet.jpg │ ├── bridge.jpg │ ├── docker-bridge.png │ ├── net-bridge.png │ └── switch.png ├── netfilter-hooks.png ├── netfilter-hooks2.png ├── network │ ├── fib-structs.jpg │ └── ip-route.jpg ├── overlayfs-map.png ├── overlayfs-mount.jpg ├── overlayfs-relation.jpg ├── pid-namespace-level.png ├── pid-namespace-structs.png ├── process-schedule-o1-move.jpg ├── process-schedule-o1.jpg ├── process_vm.jpg ├── qrcode_linux_naxieshi.jpg ├── rcu-grace-period.png ├── read-write-system-call.png ├── red-black-tree.png ├── select-model.png ├── semgent-selector.png ├── semget-selector-table.png ├── seqlock.png ├── shm-map.jpg ├── signal-kernel-stack.png ├── signal-user-stack.png ├── signal1.png ├── single-trace.jpg ├── socket-layer.jpg ├── socket_interface.jpg ├── socket_unix_socket_call_stack.jpg ├── stat.png ├── system_call.gif ├── task_list.png ├── task_stack.png ├── task_state.png ├── tcp-ip-layer.png ├── tcp │ ├── syn-flood.png │ ├── tcp-established-hash.png │ ├── tcp-header.png │ ├── tcp-ip-layer.png │ ├── tcp-pseudo-header.png │ └── three-way-handshake.png ├── timer-Wheel.jpg ├── timer-heap.jpg ├── timer-list.jpg ├── timer-tree.jpg ├── timer-vts-pointer.jpg ├── timer-vts.jpg ├── timer.jpg ├── top.png ├── traceme.jpg ├── udp │ ├── tcp-ip-layer.png │ ├── udp-header-2.png │ ├── udp-header.png │ ├── udp-recv-process.png │ ├── udp-schedule.png │ └── udp-sendmsg.png ├── vfs-struct.jpg ├── vfs.jpg ├── vfs_struct.jpg ├── virtaul-memory-manager │ ├── elf-file-format.png │ ├── elf-sections-list.png │ ├── virtual-memory-layout.png │ └── vm-area-struct-layout.png ├── vm_address.png ├── vma-pma-maping.png ├── vmalloc-address-manager.jpg ├── vmalloc-map.jpg ├── vmalloc-memory.jpg ├── waitqueue.jpg ├── workqueue │ └── workqueue.png ├── x86-segment.png └── zerocopy │ ├── read.png │ ├── sendfile.png │ ├── sendfile2.png │ ├── userspace-kernelspace.png │ └── write.png ├── in-interrupt-principle.md ├── inotify-source-code-analysis.md ├── interrupt_hardware.md ├── interrupt_softward.md ├── iowait.md ├── ip-source-code.md ├── ipc-shm.md ├── kernel-timer.md ├── lvs-principle-and-source-analysis-part1.md ├── lvs-principle-and-source-analysis-part2.md ├── memory_mmap.md ├── memory_swap.md ├── minix_file_system.md ├── multiplexing-io.md ├── namespace.md ├── native-aio.md ├── net_bridge.md ├── overlayfs.md ├── physical-memory-buddy-system.md ├── physical-memory-managemen.md ├── physical-memory-slab-algorithm.md ├── process-management.md ├── process-schedule-o1.md ├── process-schedule.md ├── process-virtual-memory-manage.md ├── ptrace.md ├── rcu.md ├── seqlock.md ├── seqlock.png ├── signal.md ├── smp.md ├── socket_interface.md ├── socket_unix.md ├── syscall.md ├── tcp-three-way-handshake-connect.md ├── tun-tap-principle.md ├── udp-source-code.md ├── unix-domain-sockets.md ├── virtual-memory-managemen.md ├── virtual-physical-address-mapping.md ├── virtual_file_system.md ├── virtual_memory_address_manager.md ├── vmalloc-memory-implements.md ├── waitqueue.md ├── workqueue.md └── zero-copy.md /README.md: -------------------------------------------------------------------------------- 1 | # Linux源码分析 2 | 3 | ## 目录 4 | 5 | * 进程管理 6 | * [进程管理](https://github.com/liexusong/linux-source-code-analyze/blob/master/process-management.md) 7 | * [进程调度](https://github.com/liexusong/linux-source-code-analyze/blob/master/process-schedule.md) 8 | * 同步机制 9 | * [并发同步](https://github.com/liexusong/linux-source-code-analyze/blob/master/concurrency-synchronize.md) 10 | * [等待队列](https://github.com/liexusong/linux-source-code-analyze/blob/master/waitqueue.md) 11 | * [顺序锁](https://github.com/liexusong/linux-source-code-analyze/blob/master/seqlock.md) 12 | * 内存管理 13 | * [物理内存管理](https://github.com/liexusong/linux-source-code-analyze/blob/master/physical-memory-managemen.md) 14 | * [伙伴分配算法](https://github.com/liexusong/linux-source-code-analyze/blob/master/physical-memory-buddy-system.md) 15 | * [Slab分配算法](https://github.com/liexusong/linux-source-code-analyze/blob/master/physical-memory-slab-algorithm.md) 16 | * [虚拟内存管理](https://github.com/liexusong/linux-source-code-analyze/blob/master/virtual_memory_address_manager.md) 17 | * [mmap完全剖析](https://github.com/liexusong/linux-source-code-analyze/blob/master/memory_mmap.md) 18 | * [内存交换](https://github.com/liexusong/linux-source-code-analyze/blob/master/memory_swap.md) 19 | * [vmalloc原理与实现](https://github.com/liexusong/linux-source-code-analyze/blob/master/vmalloc-memory-implements.md) 20 | * [写时复制](https://github.com/liexusong/linux-source-code-analyze/blob/master/copy-on-write.md) 21 | * [零拷贝技术](https://github.com/liexusong/linux-source-code-analyze/blob/master/zero-copy.md) 22 | * [虚拟内存空间管理](https://github.com/liexusong/linux-source-code-analyze/blob/master/process-virtual-memory-manage.md) 23 | * 中断机制 24 | * [硬件相关](https://github.com/liexusong/linux-source-code-analyze/blob/master/interrupt_hardware.md) 25 | * [中断处理](https://github.com/liexusong/linux-source-code-analyze/blob/master/interrupt_softward.md) 26 | * [系统调用](https://github.com/liexusong/linux-source-code-analyze/blob/master/syscall.md) 27 | * 文件系统 28 | * [虚拟文件系统](https://github.com/liexusong/linux-source-code-analyze/blob/master/virtual_file_system.md) 29 | * [MINIX文件系统](https://github.com/liexusong/linux-source-code-analyze/blob/master/minix_file_system.md) 30 | * [通用块层](https://github.com/liexusong/linux-source-code-analyze/blob/master/filesystem-generic-block-layer.md) 31 | * [直接I/O](https://github.com/liexusong/linux-source-code-analyze/blob/master/direct-io.md) 32 | * [原生异步I/O](https://github.com/liexusong/linux-source-code-analyze/blob/master/native-aio.md) 33 | * [inotify源码分析](https://github.com/liexusong/linux-source-code-analyze/blob/master/inotify-source-code-analysis.md) 34 | * 进程间通信 35 | * [信号处理机制](https://github.com/liexusong/linux-source-code-analyze/blob/master/signal.md) 36 | * [共享内存](https://github.com/liexusong/linux-source-code-analyze/blob/master/ipc-shm.md) 37 | * 网络 38 | * [Socket接口](https://github.com/liexusong/linux-source-code-analyze/blob/master/socket_interface.md) 39 | * [Unix Domain Socket](https://github.com/liexusong/linux-source-code-analyze/blob/master/unix-domain-sockets.md) 40 | * [TUN/TAP设备原理与实现](https://github.com/liexusong/linux-source-code-analyze/blob/master/tun-tap-principle.md) 41 | * [LVS原理与实现 - 原理篇](https://github.com/liexusong/linux-source-code-analyze/blob/master/lvs-principle-and-source-analysis-part1.md) 42 | * [LVS原理与实现 - 实现篇](https://github.com/liexusong/linux-source-code-analyze/blob/master/lvs-principle-and-source-analysis-part2.md) 43 | * [ARP协议与邻居子系统剖析](https://github.com/liexusong/linux-source-code-analyze/blob/master/arp-neighbour.md) 44 | * [IP协议源码分析](https://github.com/liexusong/linux-source-code-analyze/blob/master/ip-source-code.md) 45 | * [UDP协议源码分析](https://github.com/liexusong/linux-source-code-analyze/blob/master/udp-source-code.md) 46 | * [TCP源码分析 - 三次握手之 connect 过程](https://github.com/liexusong/linux-source-code-analyze/blob/master/tcp-three-way-handshake-connect.md) 47 | * [Linux网桥工作原理与实现](https://github.com/liexusong/linux-source-code-analyze/blob/master/net_bridge.md) 48 | * 其他 49 | * [定时器实现](https://github.com/liexusong/linux-source-code-analyze/blob/master/kernel-timer.md) 50 | * [多路复用I/O](https://github.com/liexusong/linux-source-code-analyze/blob/master/multiplexing-io.md) 51 | * [GDB原理之ptrace](https://github.com/liexusong/linux-source-code-analyze/blob/master/ptrace.md) 52 | * 容器相关 53 | * [docker实现原理之 - namespace](https://github.com/liexusong/linux-source-code-analyze/blob/master/namespace.md) 54 | * [docker实现原理之 - CGroup介绍](https://github.com/liexusong/linux-source-code-analyze/blob/master/cgroup.md) 55 | * [docker实现原理之 - CGroup实现原理](https://github.com/liexusong/linux-source-code-analyze/blob/master/cgroup-principle.md) 56 | * [docker实现原理之 - OverlayFS实现原理](https://github.com/liexusong/linux-source-code-analyze/blob/master/overlayfs.md) 57 | * 2.6+内核分析 58 | * [Epoll原理与实现](https://github.com/liexusong/linux-source-code-analyze/blob/master/epoll-principle.md) 59 | * [RCU原理与实现](https://github.com/liexusong/linux-source-code-analyze/blob/master/rcu.md) 60 | * [O(1)调度算法](https://github.com/liexusong/linux-source-code-analyze/blob/master/process-schedule-o1.md) 61 | * [完全公平调度算法](https://github.com/liexusong/linux-source-code-analyze/blob/master/cfs-scheduler.md) 62 | * [HugePages原理与使用](https://github.com/liexusong/linux-source-code-analyze/blob/master/hugepage.md) 63 | * [HugePages实现剖析](https://github.com/liexusong/linux-source-code-analyze/blob/master/hugepages-source-code-analysis.md) 64 | * [什么是iowait](https://github.com/liexusong/linux-source-code-analyze/blob/master/iowait.md) 65 | 66 | ## 其他版本Linux 67 | 68 | ### 1、Linux-3.x 69 | 70 | ### 2、Linux-4.x 71 | * eBPF 72 | * [eBPF源码分析 - kprobe模块](https://github.com/liexusong/linux-source-code-analyze/blob/master/eBPF.md) 73 | 74 | ### 3、Linux-5.x 75 | * 文件系统与I/O 76 | * io_uring 77 | 78 | ### 我们的公众号 79 | 80 | ![qrcode](https://image-static.segmentfault.com/376/558/3765589661-607fef350658b_fix732) 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /cgroup.md: -------------------------------------------------------------------------------- 1 | ## CGroup 介绍 2 | 3 | `CGroup` 全称 `Control Group` 中文意思为 `控制组`,用于控制(限制)进程对系统各种资源的使用,比如 `CPU`、`内存`、`网络` 和 `磁盘I/O` 等资源的限制,著名的容器引擎 `Docker` 就是使用 `CGroup` 来对容器进行资源限制。 4 | 5 | ### CGroup 使用 6 | 7 | 本文主要以 `内存子系统(memory subsystem)` 作为例子来阐述 `CGroup` 的原理,所以这里先介绍怎么通过 `内存子系统` 来限制进程对内存的使用。 8 | 9 | > `子系统` 是 `CGroup` 用于控制某种资源(如内存或者CPU等)使用的逻辑或者算法 10 | 11 | `CGroup` 使用了 `虚拟文件系统` 来进行管理限制的资源信息和被限制的进程列表等,例如要创建一个限制内存使用的 `CGroup` 可以使用下面命令: 12 | ```bash 13 | $ mount -t cgroup -o memory memory /sys/fs/cgroup/memory 14 | ``` 15 | 上面的命令用于创建内存子系统的根 `CGroup`,如果系统已经存在可以跳过。然后我们使用下面命令在这个目录下面创建一个新的目录 `test`, 16 | ```bash 17 | $ mkdir /sys/fs/cgroup/memory/test 18 | ``` 19 | 这样就在内存子系统的根 `CGroup` 下创建了一个子 `CGroup`,我们可以通过 `ls` 目录来查看这个目录下有哪些文件: 20 | ```bash 21 | $ ls /sys/fs/cgroup/memory/test 22 | cgroup.clone_children memory.kmem.max_usage_in_bytes memory.limit_in_bytes memory.numa_stat memory.use_hierarchy 23 | cgroup.event_control memory.kmem.slabinfo memory.max_usage_in_bytes memory.oom_control notify_on_release 24 | cgroup.procs memory.kmem.tcp.failcnt memory.memsw.failcnt memory.pressure_level tasks 25 | memory.failcnt memory.kmem.tcp.limit_in_bytes memory.memsw.limit_in_bytes memory.soft_limit_in_bytes 26 | memory.force_empty memory.kmem.tcp.max_usage_in_bytes memory.memsw.max_usage_in_bytes memory.stat 27 | memory.kmem.failcnt memory.kmem.tcp.usage_in_bytes memory.memsw.usage_in_bytes memory.swappiness 28 | memory.kmem.limit_in_bytes memory.kmem.usage_in_bytes memory.move_charge_at_immigrate memory.usage_in_bytes 29 | ``` 30 | 可以看到在目录下有很多文件,每个文件都是 `CGroup` 用于控制进程组的资源使用。我们可以向 `memory.limit_in_bytes` 文件写入限制进程(进程组)使用的内存大小,单位为字节(bytes)。例如可以使用以下命令写入限制使用的内存大小为 `1MB`: 31 | ```bash 32 | $ echo 1048576 > /sys/fs/cgroup/memory/test/memory.limit_in_bytes 33 | ``` 34 | 然后我们可以通过以下命令把要限制的进程加入到 `CGroup` 中: 35 | ```bash 36 | $ echo task_pid > /sys/fs/cgroup/memory/test/tasks 37 | ``` 38 | 上面的 `task_pid` 为进程的 `PID`,把进程PID添加到 `tasks` 文件后,进程对内存的使用就受到此 `CGroup` 的限制。 39 | 40 | ### CGroup 基本概念 41 | 42 | 在介绍 `CGroup` 原理前,先介绍一下 `CGroup` 几个相关的概念,因为要理解 `CGroup` 就必须要理解他们: 43 | 44 | * `任务(task)`。任务指的是系统的一个进程,如上面介绍的 `tasks` 文件中的进程; 45 | 46 | * `控制组(control group)`。控制组就是受相同资源限制的一组进程。`CGroup` 中的资源控制都是以控制组为单位实现。一个进程可以加入到某个控制组,也从一个进程组迁移到另一个控制组。一个进程组的进程可以使用 `CGroup` 以控制组为单位分配的资源,同时受到 `CGroup` 以控制组为单位设定的限制; 47 | 48 | * `层级(hierarchy)`。由于控制组是以目录形式存在的,所以控制组可以组织成层级的形式,即一棵控制组组成的树。控制组树上的子节点控制组是父节点控制组的孩子,继承父控制组的特定的属性; 49 | 50 | * `子系统(subsystem)`。一个子系统就是一个资源控制器,比如 `CPU子系统` 就是控制 CPU 时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制组都受到这个子系统的控制。 51 | 52 | 他们之间的关系如下图: 53 | 54 | ![cgroup-base](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/cgroup-base.jpg) 55 | 56 | 我们可以把 `层级` 中的一个目录当成是一个 `CGroup`,那么目录里面的文件就是这个 `CGroup` 用于控制进程组使用各种资源的信息(比如 `tasks` 文件用于保存这个 `CGroup` 控制的进程组所有的进程PID,而 `memory.limit_in_bytes` 文件用于描述这个 `CGroup` 能够使用的内存字节数)。 57 | 58 | 而附加在 `层级` 上的 `子系统` 表示这个 `层级` 中的 `CGroup` 可以控制哪些资源,每当向 `层级` 附加 `子系统` 时,`层级` 中的所有 `CGroup` 都会产生很多与 `子系统` 资源控制相关的文件。 59 | 60 | ### CGroup 操作规则 61 | 62 | 使用 `CGroup` 时,必须按照 `CGroup` 一些操作规则来进行操作,否则会出错。下面介绍一下关于 `CGroup` 的一些操作规则: 63 | 64 | 1. 一个 `层级` 可以附加多个 `子系统`,如下图: 65 | 66 | ![cgroup-rule1](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/cgroup-rule1.jpeg) 67 | 68 | 2. 一个已经被挂载的 `子系统` 只能被再次挂载在一个空的 `层级` 上,不能挂载到已经挂载了其他 `子系统` 的 `层级`,如下图: 69 | 70 | ![cgroup-rule2](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/cgroup-rule2.jpeg) 71 | 72 | 3. 每个 `任务` 只能在同一个 `层级` 的唯一一个 `CGroup` 里,并且可以在多个不同层级的 `CGroup` 中,如下图: 73 | 74 | ![cgroup-rule3](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/cgroup-rule3.jpeg) 75 | 76 | 4. 子进程在被 `fork` 出时自动继承父进程所在 `CGroup`,但是 `fork` 之后就可以按需调整到其他 `CGroup`,如下图: 77 | 78 | ![cgroup-rule4](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/cgroup-rule4.jpeg) 79 | 80 | 关于 `CGroup` 的介绍和使用就到这里,接下来我们来分析一下内核是怎么实现 `CGroup` 的。 -------------------------------------------------------------------------------- /concurrency-synchronize.md: -------------------------------------------------------------------------------- 1 | ## 并发同步 2 | 3 | `并发` 是指在某一时间段内能够处理多个任务的能力,而 `并行` 是指同一时间能够处理多个任务的能力。并发和并行看起来很像,但实际上是有区别的,如下图(图片来源于网络): 4 | 5 | ![concurrency-parallelism](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/concurrency-synchronize-1.png) 6 | 7 | 上图的意思是,有两条在排队买咖啡的队列,并发只有一架咖啡机在处理,而并行就有两架的咖啡机在处理。咖啡机的数量越多,并行能力就越强。 8 | 9 | 可以把上面的两条队列看成两个进程,并发就是指只有单个CPU在处理,而并行就有两个CPU在处理。为了让两个进程在单核CPU中也能得到执行,一般的做法就是让每个进程交替执行一段时间,比如让每个进程固定执行 `100毫秒`,执行时间使用完后切换到其他进程执行。而并行就没有这种问题,因为有两个CPU,所以两个进程可以同时执行。如下图: 10 | 11 | ![concurrency-parallelism](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/concurrency-synchronize-2.png) 12 | 13 | ### 原子操作 14 | 15 | 上面介绍过,并发有可能会打断当前执行的进程,然后替切换成其他进程执行。如果有两个进程同时对一个共享变量 `count` 进行加一操作,由于C语言的 `count++` 操作会被翻译成如下指令: 16 | ```asm 17 | mov eax, [count] 18 | inc eax 19 | mov [count], eax 20 | ``` 21 | 那么在并发的情况下,有可能出现如下问题: 22 | 23 | ![concurrency-problem](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/concurrency-synchronize-4.jpg) 24 | 25 | 假设count变量初始值为0: 26 | * 进程1执行完 `mov eax, [count]` 后,寄存器eax内保存了count的值0。 27 | * 进程2被调度执行。进程2执行 `count++` 的所有指令,将累加后的count值1写回到内存。 28 | * 进程1再次被调度执行,计算count的累加值仍为1,写回到内存。 29 | 30 | 虽然进程1和进程2执行了两次 `count++` 操作,但是count最后的值为1,而不是2。 31 | 32 | 要解决这个问题就需要使用 `原子操作`,原子操作是指不能被打断的操作,在单核CPU中,一条指令就是原子操作。比如上面的问题可以把 `count++` 语句翻译成指令 `inc [count]` 即可。Linux也提供了这样的原子操作,如对整数加一操作的 `atomic_inc()`: 33 | ```cpp 34 | static __inline__ void atomic_inc(atomic_t *v) 35 | { 36 | __asm__ __volatile__( 37 | LOCK "incl %0" 38 | :"=m" (v->counter) 39 | :"m" (v->counter)); 40 | } 41 | ``` 42 | 43 | 在多核CPU中,一条指令也不一定是原子操作,比如 `inc [count]` 指令在多核CPU中需要进行如下过程: 44 | 1. 从内存将count的数据读取到cpu。 45 | 2. 累加读取的值。 46 | 3. 将修改的值写回count内存。 47 | 48 | `Intel x86 CPU` 提供了 `lock` 前缀来锁住总线,可以让指令保证不被其他CPU中断,如下: 49 | ```asm 50 | lock 51 | inc [count] 52 | ``` 53 | 54 | ### 锁 55 | 56 | `原子操作` 能够保证操作不被其他进程干扰,但有时候一个复杂的操作需要由多条指令来实现,那么就不能使用原子操作了,这时候可以使用 `锁` 来实现。 57 | 58 | 计算机科学中的 `锁` 与日常生活的 `锁` 有点类似,举个例子:比如要上公厕,首先找到一个没有人的厕所,然后把厕所门锁上。其他人要使用的话,必须等待当前这人使用完毕,并且把门锁打开才能使用。在计算机中,要对某个公共资源进行操作时,必须对公共资源进行上锁,然后才能使用。如果不上锁,那么就可能导致数据混乱的情况。 59 | 60 | 在Linux内核中,比较常用的锁有:`自旋锁`、`信号量`、`读写锁` 等,下面介绍一下自旋锁和信号量的实现。 61 | 62 | #### 自旋锁 63 | 64 | `自旋锁` 只能在多核CPU系统中,其核心原理是 `原子操作`,原理如下图: 65 | 66 | ![spinlock](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/concurrency-synchronize-spinlock.jpg) 67 | 68 | 使用 `自旋锁` 时,必须先对自旋锁进行初始化(设置为1),上锁过程如下: 69 | 1. 对自旋锁 `lock` 进行减一操作,判断结果是否等于0,如果是表示上锁成功并返回。 70 | 2. 如果不等于0,表示其他进程已经上锁,此时必须不断比较自旋锁 `lock` 的值是否等于1(表示已经解锁)。 71 | 3. 如果自旋锁 `lock` 等于1,跳转到第一步继续进行上锁操作。 72 | 73 | 由于Linux的自旋锁使用汇编实现,所以比较苦涩难懂,这里使用C语言来模拟一下: 74 | ```cpp 75 | void spin_lock(amtoic_t *lock) 76 | { 77 | again: 78 | result = --(*lock); 79 | if (result == 0) { 80 | return; 81 | } 82 | 83 | while (true) { 84 | if (*lock == 1) { 85 | goto again; 86 | } 87 | } 88 | } 89 | ``` 90 | 上面代码将 `result = --(*lock);` 当成原子操作,解锁过程只需要把 `lock` 设置为1即可。由于自旋锁会不断尝试上锁操作,并不会对进程进行调度,所以在单核CPU中可能会导致 100% 的CPU占用率。另外,自旋锁只适合粒度比较小的操作,如果操作粒度比较大,就需要使用信号量这种可调度进程的锁。 91 | 92 | #### 信号量 93 | 94 | 与 `自旋锁` 不一样,当当前进程对 `信号量` 进行上锁时,如果其他进程已经对其进行上锁,那么当前进程会进入睡眠状态,等待其他进程对信号量进行解锁。过程如下图: 95 | 96 | ![semaphore](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/concurrency-synchronize-semaphore.jpg) 97 | 98 | 在Linux内核中,信号量使用 `struct semaphore` 表示,定义如下: 99 | ```cpp 100 | struct semaphore { 101 | raw_spinlock_t lock; 102 | unsigned int count; 103 | struct list_head wait_list; 104 | }; 105 | ``` 106 | 各个字段的作用如下: 107 | * `lock`:自旋锁,用于对多核CPU平台进行同步。 108 | * `count`:信号量的计数器,上锁时对其进行减一操作(count--),如果得到的结果为大于等于0,表示成功上锁,如果小于0表示已经被其他进程上锁。 109 | * `wait_list`:正在等待信号量解锁的进程队列。 110 | 111 | `信号量` 上锁通过 `down()` 函数实现,代码如下: 112 | ```cpp 113 | void down(struct semaphore *sem) 114 | { 115 | unsigned long flags; 116 | 117 | spin_lock_irqsave(&sem->lock, flags); 118 | if (likely(sem->count > 0)) 119 | sem->count--; 120 | else 121 | __down(sem); 122 | spin_unlock_irqrestore(&sem->lock, flags); 123 | } 124 | ``` 125 | 126 | 上面代码可以看出,`down()` 函数首先对信号量进行自旋锁操作(为了避免多核CPU竞争),然后比较计数器是否大于0,如果是对计数器进行减一操作,并且返回,否则调用 `__down()` 函数进行下一步操作。`__down()` 函数实现如下: 127 | 128 | ```cpp 129 | static noinline void __sched __down(struct semaphore *sem) 130 | { 131 | __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); 132 | } 133 | 134 | static inline int __down_common(struct semaphore *sem, 135 | long state, long timeout) 136 | { 137 | struct task_struct *task = current; 138 | struct semaphore_waiter waiter; 139 | 140 | // 把当前进程添加到等待队列中 141 | list_add_tail(&waiter.list, &sem->wait_list); 142 | waiter.task = task; 143 | waiter.up = 0; 144 | 145 | for (;;) { 146 | ... 147 | __set_task_state(task, state); 148 | spin_unlock_irq(&sem->lock); 149 | timeout = schedule_timeout(timeout); 150 | spin_lock_irq(&sem->lock); 151 | if (waiter.up) // 当前进程是否获得信号量锁? 152 | return 0; 153 | } 154 | ... 155 | } 156 | ``` 157 | 158 | `__down()` 函数最终调用 `__down_common()` 函数,而 `__down_common()` 函数的操作过程如下: 159 | 160 | 1. 把当前进程添加到信号量的等待队列中。 161 | 2. 切换到其他进程运行,直到被其他进程唤醒。 162 | 3. 如果当前进程获得信号量锁(由解锁进程传递),那么函数返回。 163 | 164 | 接下来看看解锁过程,解锁过程主要通过 `up()` 函数实现,代码如下: 165 | ```cpp 166 | void up(struct semaphore *sem) 167 | { 168 | unsigned long flags; 169 | 170 | raw_spin_lock_irqsave(&sem->lock, flags); 171 | if (likely(list_empty(&sem->wait_list))) // 如果没有等待的进程, 直接对计数器加一操作 172 | sem->count++; 173 | else 174 | __up(sem); // 如果有等待进程, 那么调用 __up() 函数进行唤醒 175 | raw_spin_unlock_irqrestore(&sem->lock, flags); 176 | } 177 | 178 | static noinline void __sched __up(struct semaphore *sem) 179 | { 180 | // 获取到等待队列的第一个进程 181 | struct semaphore_waiter *waiter = list_first_entry( 182 | &sem->wait_list, struct semaphore_waiter, list); 183 | 184 | list_del(&waiter->list); // 把进程从等待队列中删除 185 | waiter->up = 1; // 告诉进程已经获得信号量锁 186 | wake_up_process(waiter->task); // 唤醒进程 187 | } 188 | ``` 189 | 190 | 解锁过程如下: 191 | 1. 判断当前信号量是否有等待的进程,如果没有等待的进程, 直接对计数器加一操作 192 | 2. 如果有等待的进程,那么获取到等待队列的第一个进程。 193 | 3. 把进程从等待队列中删除。 194 | 4. 告诉进程已经获得信号量锁 195 | 5. 唤醒进程 196 | -------------------------------------------------------------------------------- /copy-on-write.md: -------------------------------------------------------------------------------- 1 | # Linux 写时复制机制原理 2 | 3 | 在 Linux 系统中,调用 `fork` 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 `写时复制` 机制。 4 | 5 | 下面我们将分析 Linux `写时复制(Copy On Write)` 机制的原理。 6 | 7 | ## 虚拟内存与物理内存 8 | 9 | 进程的内存可分为 `虚拟内存` 和 `物理内存`。 10 | 11 | * `物理内存`:就是电脑安装的内存条,如果电脑安装了2GB的内存条,那么系统就用于 0 ~ 2GB 的物理内存空间。 12 | * `虚拟内存`:虚拟内存是使用软件虚拟的,在 32 位操作系统中,每个进程都独占 4GB 的虚拟内存空间。 13 | 14 | 应用程序使用的是 `虚拟内存`,比如 C 语言取地址操作符号 `&` 所得到的地址就是 `虚拟内存地址`。而 `虚拟内存地址` 需要映射到 `物理内存地址` 才能使用,如果使用没有映射的 `虚拟内存地址`,将会导致 `缺页异常`。 15 | 16 | `虚拟内存地址` 映射到 `物理内存地址` 如下图所示: 17 | 18 | ![memory-mapping](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/memory-mapping/memory-mapping.png) 19 | 20 | 如上图所示,进程A与进程B的相同 `虚拟内存地址` 映射到不同的 `物理内存地址`,这就是不同进程的相同虚拟内存地址互不影响的原因。 21 | 22 | ## 写时复制原理 23 | 24 | 前面介绍了 `虚拟内存` 与 `物理内存` 的概念,接下来将会介绍 Linux `写时复制` 的原理。 25 | 26 | 前面说过,`虚拟内存` 需要与 `物理内存` 进行映射才能使用,如果不同进程的 `虚拟内存地址` 映射到相同的 `物理内存地址`,那么就实现了共享内存的机制。如下图所示: 27 | 28 | ![share-memory](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/memory-mapping/share-memory.png) 29 | 30 | 由于进程A的 `虚拟内存M` 与进程B的 `虚拟内存M'` 映射到相同的 `物理内存G`,所以当修改进程A `虚拟内存M` 的数据时,进程B `虚拟内存M'` 的数据也会跟着改变。 31 | 32 | Linux 为了加速创建子进程过程与节省内存使用的原因,实现了 `写时复制` 的机制。 33 | 34 | `写时复制` 的原理大概如下: 35 | 36 | * 创建子进程时,将父进程的 `虚拟内存` 与 `物理内存` 映射关系复制到子进程中,并将内存设置为只读(设置为只读是为了当对内存进行写操作时触发 `缺页异常`)。 37 | 38 | * 当子进程或者父进程对内存数据进行修改时,便会触发 `写时复制` 机制:将原来的内存页复制一份新的,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写。 39 | 40 | `写时复制` 过程如下图所示: 41 | 42 | ![copy-on-write](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/memory-mapping/copy-on-write.png) 43 | 44 | 当创建子进程时,父子进程指向相同的 `物理内存`,而不是将父进程所占用的 `物理内存` 复制一份。这样做的好处有两个: 45 | 46 | * 加速创建子进程的速度。 47 | 48 | * 减少进程对物理内存的使用。 49 | 50 | 如上图所示,当父进程调用 `fork` 创建子进程时,父进程的 `虚拟内存页M` 与子进程的 `虚拟内存页M` 映射到相同的 `物理内存页G`,并且把父进程与子进程的 `虚拟内存页M` 都设置为只读(因为设置为只读后,对内存页进行写操作时,将会发生 `缺页异常`,从而内核可以在缺页异常处理函数中进行物理内存页的复制)。 51 | 52 | 当子进程对 `虚拟内存页M` 进行写操作,便会触发 `缺页异常`(因为已经将 `虚拟内存页M` 设置为只读)。在缺页异常处理函数中,对 `物理内存页G` 进行复制一份新的 `物理内存页G'`,并且将子进程的 `虚拟内存页M` 映射到 `物理内存页G'`,同时将父子进程的 `虚拟内存页M` 设置为可读写。 53 | 54 | ## 总结 55 | 56 | 本篇文章主要介绍了 Linux `写时复制` 的原理,`写时复制` 是 Linux 创建子进程高效的关键所在,而且还能节省对物理内存使用。我们将在下一篇文章中对 `写时复制` 的实现进行详细的分析。 57 | 58 | -------------------------------------------------------------------------------- /eBPF.md: -------------------------------------------------------------------------------- 1 | # eBPF 源码分析 2 | 3 | eBPF 使用 BPF_PROG_RUN() 宏来运行 eBPF 程序,其定义如下: 4 | 5 | ```c 6 | #define BPF_PROG_RUN(filter, ctx) (*filter->bpf_func)(ctx, filter->insnsi) 7 | ``` 8 | 9 | 一般来说,filter参数的类型为:`struct bpf_prog`,其定义如下: 10 | 11 | ```c 12 | struct bpf_prog { 13 | ... 14 | u16 jited:1, /* 是否已经编译程JIT? */ 15 | ... 16 | // 1. 如果是虚拟机执行,那么指向 __bpf_prog_run() 函数 17 | // 2. 如果是 JIT 执行,那么指向 eBPF 程序经过 JIT 转换后的可执行二进制字节码 18 | unsigned int (*bpf_func)(const struct sk_buff *skb, 19 | const struct bpf_insn *filter); 20 | /* eBPF字节码 */ 21 | union { 22 | struct sock_filter insns[0]; 23 | struct bpf_insn insnsi[0]; 24 | }; 25 | }; 26 | ``` 27 | 28 | 我们来看看 Linux 内核各个模块是怎么来执行 eBPF 程序的吧。 29 | 30 | 首先,我们来看看 perf 模块是怎么执行 eBPF 的。我们可以看到 trace_call_bpf() 函数中有以下一段代码: 31 | 32 | ```c 33 | unsigned int trace_call_bpf(struct bpf_prog *prog, void *ctx) 34 | { 35 | unsigned int ret; 36 | ... 37 | 38 | rcu_read_lock(); 39 | ret = BPF_PROG_RUN(prog, ctx); 40 | rcu_read_unlock(); 41 | ... 42 | 43 | return ret; 44 | } 45 | ``` 46 | 47 | 而 `trace_call_bpf()` 函数又被 `kprobe_perf_func()` 函数调用,如下代码所示: 48 | 49 | ```c 50 | static void 51 | kprobe_perf_func(struct trace_kprobe *tk, struct pt_regs *regs) 52 | { 53 | struct trace_event_call *call = &tk->tp.call; 54 | struct bpf_prog *prog = call->prog; 55 | ... 56 | 57 | if (prog && !trace_call_bpf(prog, regs)) 58 | return; 59 | 60 | ... 61 | } 62 | ``` 63 | 64 | 而 `kprobe_perf_func()` 又被 `kprobe_dispatcher()` 函数调用,其代码如下: 65 | 66 | ```c 67 | static int kprobe_dispatcher(struct kprobe *kp, struct pt_regs *regs) 68 | { 69 | struct trace_kprobe *tk = container_of(kp, struct trace_kprobe, rp.kp); 70 | ... 71 | 72 | #ifdef CONFIG_PERF_EVENTS 73 | if (tk->tp.flags & TP_FLAG_PROFILE) 74 | kprobe_perf_func(tk, regs); 75 | #endif 76 | 77 | return 0; 78 | } 79 | ``` 80 | 81 | 所以 perf 模块执行 eBPF 程序的调用链如下: 82 | 83 | ```c 84 | kprobe_dispatcher() 85 | -> kprobe_perf_func() 86 | -> trace_call_bpf() 87 | -> BPF_PROG_RUN() 88 | ``` 89 | 90 | 那么 `kprobe_dispatcher()` 函数在什么时候被调用呢?我们查看源码可以发现,内核并没有直接调用 `kprobe_dispatcher()` 函数的地方。那么这个函数是怎么被调用的呢? 91 | 92 | 这个问题涉及到 trace 模块,当我们使用以下命令设置一个 kprobe 事件时,将会触发调用 `create_trace_kprobe()` 函数。 93 | 94 | ```bash 95 | > echo "r:kretprobe_func sys_write $retval" /sys/kernel/debug/tracing/kprobe_events 96 | ``` 97 | 98 | 我们来看看 `create_trace_kprobe()` 函数的实现: 99 | 100 | ```c 101 | static int create_trace_kprobe(int argc, char **argv) 102 | { 103 | ... 104 | struct trace_kprobe *tk; 105 | 106 | tk = alloc_trace_kprobe(group, event, addr, symbol, offset, argc, 107 | is_return); 108 | ... 109 | 110 | ret = register_trace_kprobe(tk); 111 | if (ret) 112 | goto error; 113 | return 0; 114 | } 115 | ``` 116 | 117 | `create_trace_kprobe()` 函数主要完成 2 件事情: 118 | 119 | * 调用 `alloc_trace_kprobe()` 函数创建一个 `trace_kprobe` 结构,并且初始化它。 120 | * 调用 `register_trace_kprobe()` 函数对事件进行注册。 121 | 122 | 我们先来看看 `alloc_trace_kprobe()` 函数做了什么事情: 123 | 124 | ```c 125 | static struct trace_kprobe * 126 | alloc_trace_kprobe(const char *group, const char *event, void *addr, 127 | const char *symbol, unsigned long offs, int nargs, 128 | bool is_return) 129 | { 130 | struct trace_kprobe *tk; 131 | 132 | tk = kzalloc(SIZEOF_TRACE_KPROBE(nargs), GFP_KERNEL); 133 | ... 134 | 135 | if (is_return) 136 | tk->rp.handler = kretprobe_dispatcher; 137 | else 138 | tk->rp.kp.pre_handler = kprobe_dispatcher; 139 | 140 | ... 141 | return tk; 142 | } 143 | ``` 144 | 145 | 从 `alloc_trace_kprobe()` 函数的代码可以看出,其将 `trace_kprobe` 结构的 `rp.kp.pre_handler` 成员设置为 `kprobe_dispatcher()` 函数。我们可以看看 `trace_kprobe` 结构的定义: 146 | 147 | ```c 148 | struct trace_kprobe { 149 | ... 150 | struct kretprobe rp; 151 | ... 152 | }; 153 | 154 | struct kretprobe { 155 | struct kprobe kp; 156 | ... 157 | }; 158 | 159 | struct kprobe { 160 | ... 161 | kprobe_pre_handler_t pre_handler; 162 | ... 163 | }; 164 | ``` 165 | 166 | 看到这里,`kprobe_dispatcher()` 函数何时被调用已经有点眉目了。因为在 `alloc_trace_kprobe()` 函数中,我们看到了 `kprobe_dispatcher()` 函数的踪影。 167 | 168 | 我们接着来看 `register_trace_kprobe()` 函数的实现。 169 | 170 | 通过分析,可以看到 `register_trace_kprobe()` 最终会调用 `register_kprobe()` 函数注册一个 `kprobe` 跟踪点,跟踪点的回调就是 `register_kprobe()` 函数。调用链如下: 171 | 172 | ```c 173 | register_trace_kprobe() 174 | -> __register_trace_kprobe() 175 | -> register_kprobe() 176 | ``` 177 | 178 | 我们在 kprobe 源码分析一文中分析过,当跟踪点被触发时,首先会调用 `pre_handler` 成员指向的函数来处理。而通过上面的分析可知,`pre_handler` 成员指向了 `kprobe_dispatcher()` 函数。所以当跟踪点被触发时,将会调用 `kprobe_dispatcher()` 函数来进行处理。调用链如下: 179 | 180 | ```c 181 | do_int3() 182 | -> kprobe_int3_handler() 183 | -> kprobe_dispatcher() 184 | ``` 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /filesystem-generic-block-layer.md: -------------------------------------------------------------------------------- 1 | ## 概述 2 | 3 | 由于不同块设备(如磁盘,机械硬盘等)有着不同的设备驱动程序,为了让文件系统有统一的读写块设备接口,Linux实现了一个 `通用块层`。如下图中的红色部分: 4 | 5 | ![linux-filesystem](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/linux-filesystem.jpg) 6 | 7 | `通用块层` 的引入为了提供一个统一的接口让文件系统实现者使用,而不用关心不同设备驱动程序的差异,这样实现出来的文件系统就能用于任何的块设备。 8 | 9 | `通用块层` 将对不同块设备的操作转换成对逻辑数据块的操作,也就是将不同的块设备都抽象成是一个数据块数组,而文件系统就是对这些数据块进行管理。如下图: 10 | 11 | > 注意:不同的文件系统可能对逻辑数据块定义的大小不一样,比如 ext2文件系统 的逻辑数据块大小为 4KB。 12 | 13 | ![device-block](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/device-block.jpg) 14 | 15 | 通过对设备进行抽象后,不管是磁盘还是机械硬盘,对于文件系统都可以使用相同的接口对逻辑数据块进行读写操作。 16 | 17 | ## 通用块读写接口 18 | 19 | `通用块层` 提供了 `ll_rw_block()` 函数对逻辑块进行读写操作,`ll_rw_block()` 函数的原型如下: 20 | 21 | ```c 22 | void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]); 23 | ``` 24 | 25 | 在分析 `ll_rw_block()` 函数前,我们先来介绍一下 `buffer_head` 这个结构,因为要理解 `ll_rw_block()` 函数必须先了解 `buffer_head` 结构。 26 | 27 | `struct buffer_head` 结构代表一个要进行读或者写的数据块,其定义如下: 28 | 29 | ```c 30 | struct buffer_head { 31 | struct buffer_head *b_next; /* 用于快速查找数据块缓存 */ 32 | unsigned long b_blocknr; /* 数据块号 */ 33 | unsigned short b_size; /* 数据块大小 */ 34 | unsigned short b_list; /* List that this buffer appears */ 35 | kdev_t b_dev; /* 数据块所属设备 */ 36 | 37 | atomic_t b_count; /* 引用计数器 */ 38 | kdev_t b_rdev; /* 数据块所属真正设备 */ 39 | ... 40 | }; 41 | ``` 42 | 43 | 为了让读者更加清晰,上面忽略了 `buffer_head` 结构的某些字段,上面比较重要的是 `b_blocknr` 字段和 `b_size` 字段, `b_blocknr` 字段指定了要读写的数据块号,而 `b_size` 字段指定了数据块的大小。还有就是 `b_rdev` 字段,其指定了数据块所属的设备。 44 | 45 | 有了 `buffer_head` 结构,就可以对数据块进行读写操作。接下来,我们看看 `ll_rw_block()` 函数的实现: 46 | 47 | ```c 48 | void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]) 49 | { 50 | ... 51 | for (i = 0; i < nr; i++) { 52 | struct buffer_head *bh = bhs[i]; 53 | 54 | /* 上锁 */ 55 | if (test_and_set_bit(BH_Lock, &bh->b_state)) 56 | continue; 57 | 58 | /* 增加buffer_head的计数器 */ 59 | atomic_inc(&bh->b_count); 60 | bh->b_end_io = end_buffer_io_sync; 61 | ... 62 | submit_bh(rw, bh); 63 | } 64 | return; 65 | ... 66 | } 67 | ``` 68 | 69 | 下面介绍一下 `ll_rw_block()` 函数各个参数的作用: 70 | 71 | * `rw`:要进行的读或者写操作,一般可选的值为 `READ`、`WRITE` 或者 `READA` 等。 72 | * `nr`:`bhs` 数组的大小。 73 | * `bhs`:要进行读写操作的数据块数组。 74 | 75 | `ll_rw_block()` 函数的实现比较简单,遍历 `bhs` 数组,并且对所有的 `buffer_head` 进行上锁和增加其计数器,然后调用 `submit_bh()` 函数把其提交到 `IO调度层` 进行I/O操作。 76 | 77 | 我们接着看看 `submit_bh()` 函数的实现: 78 | 79 | ```c 80 | void submit_bh(int rw, struct buffer_head *bh) 81 | { 82 | int count = bh->b_size >> 9; // 一个数据块需要的扇区数 83 | ... 84 | set_bit(BH_Req, &bh->b_state); 85 | 86 | bh->b_rdev = bh->b_dev; 87 | bh->b_rsector = bh->b_blocknr * count; // 转换成真实的扇区号 88 | 89 | generic_make_request(rw, bh); 90 | 91 | switch (rw) { 92 | case WRITE: 93 | kstat.pgpgout += count; 94 | break; 95 | default: 96 | kstat.pgpgin += count; 97 | break; 98 | } 99 | } 100 | ``` 101 | 102 | 数据块是 `通用块层` 的概念,而真实的块设备是以扇区作为读写单元的。所以在进行IO操作前,必须将数据块号转换成真正的扇区号,而代码 `bh->b_blocknr * count` 就是用于将数据块号转换成扇区号。转换成扇区号后,`submit_bh()` 函数接着调用 `generic_make_request()` 进行下一步的操作。 103 | 104 | 我们接着分析 `generic_make_request()` 函数的实现: 105 | 106 | ```c 107 | void generic_make_request(int rw, struct buffer_head * bh) 108 | { 109 | request_queue_t *q; 110 | ... 111 | do { 112 | q = blk_get_queue(bh->b_rdev); // 获取块设备对应的I/O请求队列 113 | if (!q) { 114 | ... 115 | break; 116 | } 117 | } while (q->make_request_fn(q, rw, bh)); // 把I/O请求发送到块设备的I/O请求队列中 118 | } 119 | ``` 120 | 121 | 每一个块设备都有一个类型为 `request_queue_t` 的I/O请求队列,而 `blk_get_queue()` 函数用于获取块设备对应的I/O请求队列,然后调用I/O请求队列的 `mark_request_fn()` 方法把I/O请求添加到队列中。 122 | 123 | 那么I/O请求队列的 `mark_request_fn()` 方法到底是什么呢?这个方法由块设备驱动提供,也可以通过调用 `blk_init_queue()` 函数设置为默认的 `__make_request()` 方法。我们主要分析默认的 `__make_request()` 方法: 124 | 125 | ```c 126 | static int __make_request(request_queue_t *q, int rw, struct buffer_head *bh) 127 | { 128 | ... 129 | elevator_t *elevator = &q->elevator; 130 | 131 | count = bh->b_size >> 9; // 要读写的扇区数 132 | sector = bh->b_rsector; // 进行读写操作的开始扇区号 133 | ... 134 | again: 135 | req = NULL; 136 | head = &q->queue_head; // I/O请求队列头部 137 | spin_lock_irq(&io_request_lock); // 关闭中断并且上自旋锁 138 | insert_here = head->prev; // 插入到IO请求队列的最后 139 | ... 140 | // 尝试合并I/O请求 141 | el_ret = elevator->elevator_merge_fn(q, &req, head, bh, rw, max_sectors); 142 | switch (el_ret) { 143 | case ELEVATOR_BACK_MERGE: // 与其他I/O请求合并成功 144 | ... 145 | goto out; 146 | case ELEVATOR_FRONT_MERGE: // 与其他I/O请求合并成功 147 | ... 148 | goto out; 149 | case ELEVATOR_NO_MERGE: // 如果不能合并I/O请求 150 | if (req) 151 | insert_here = &req->queue; 152 | break; 153 | ... 154 | } 155 | 156 | req = get_request(q, rw); // 获取一个空闲的I/O请求对象 157 | ... 158 | req->cmd = rw; 159 | req->errors = 0; 160 | req->hard_sector = req->sector = sector; // 读写操作的开始扇区号 161 | req->hard_nr_sectors = req->nr_sectors = count; // 要读写多少个扇区 162 | req->current_nr_sectors = count; 163 | req->nr_segments = 1; 164 | req->nr_hw_segments = 1; 165 | req->buffer = bh->b_data; // 读写数据存放的缓冲区 166 | req->waiting = NULL; 167 | req->bh = bh; 168 | req->bhtail = bh; 169 | req->rq_dev = bh->b_rdev; 170 | add_request(q, req, insert_here); // 把I/O请求对象添加到I/O请求队列中 171 | out: 172 | ... 173 | return 0; 174 | } 175 | ``` 176 | 177 | `__make_request()` 函数首先通过调用 `elevator->elevator_merge_fn()` 方法尝试将当前I/O请求与其他正在排队的I/O请求进行合并,因为如果当前I/O请求与正在排队的I/O请求相邻,那么就可以合并为一个I/O请求,从而减少对设备I/O请求的次数。 178 | 179 | 如果不能与排队的I/O请求进行合并,那么就调用 `get_request()` 函数申请一个I/O请求对象,然后初始化此对象各个字段,再通过调用 `add_request()` 函数把I/O请求对象添加到I/O请求队列中。`add_request()` 函数实现如下: 180 | 181 | ```c 182 | static inline void 183 | add_request(request_queue_t *q, struct request *req, struct list_head *insert_here) 184 | { 185 | drive_stat_acct(req->rq_dev, req->cmd, req->nr_sectors, 1); 186 | ... 187 | list_add(&req->queue, insert_here); 188 | } 189 | ``` 190 | 191 | `add_request()` 函数的实现非常简单,首先调用 `drive_stat_acct()` 函数更新统计信息,然后调用 `list_add()` 函数把I/O请求添加到I/O请求队列中。 192 | 193 | ## 执行I/O请求 194 | 195 | `ll_rw_block()` 函数只是把I/O请求添加到设备的I/O请求队列中,那么I/O请求队列中的I/O请求什么时候会执行呢?答案就是当调用 `run_task_queue(&tq_disk)` 函数时。 196 | 197 | `run_task_queue()` 函数是 Linux 用于运行任务队列的入口,而 `tq_disk` 队列就是块设备I/O的任务队列。当执行 `run_task_queue(&tq_disk)` 函数时,便会处理 `tq_disk` 任务队列中的例程。 198 | 199 | 当调用 `ll_rw_block()` 函数添加I/O请求时,会触发调用 `generic_plug_device()` 函数,而 `generic_plug_device()` 函数会把设备的I/O请求队列添加到 `tq_disk` 任务队列中, `generic_plug_device()` 函数实现如下: 200 | 201 | ```c 202 | static void generic_plug_device(request_queue_t *q, kdev_t dev) 203 | { 204 | if (!list_empty(&q->queue_head) || q->plugged) 205 | return; 206 | 207 | q->plugged = 1; 208 | queue_task(&q->plug_tq, &tq_disk); // 把I/O请求队列添加到 tq_disk 任务队列中 209 | } 210 | ``` 211 | 212 | 通过 Linux 的任务队列机制,设备的I/O请求队列将会被执行。执行I/O请求主要是由块设备驱动完成,在块设备驱动程序初始化时可以通过调用 `blk_init_queue()` 函数指定处理I/O请求队列的方法。`blk_init_queue()` 函数原型如下: 213 | 214 | ```c 215 | void blk_init_queue(request_queue_t *q, request_fn_proc *rfn); 216 | ``` 217 | 218 | 参数 `rfn` 就是处理I/O请求队列的例程函数。 219 | 220 | -------------------------------------------------------------------------------- /hugepage.md: -------------------------------------------------------------------------------- 1 | # Linux HugePages(大内存页) 原理与使用 2 | 3 | 在介绍 `HugePages` 之前,我们先来回顾一下 Linux 下 `虚拟内存` 与 `物理内存` 之间的关系。 4 | 5 | * `物理内存`:也就是安装在计算机中的内存条,比如安装了 2GB 大小的内存条,那么物理内存地址的范围就是 0 ~ 2GB。 6 | * `虚拟内存`:虚拟的内存地址。由于 CPU 只能使用物理内存地址,所以需要将虚拟内存地址转换为物理内存地址才能被 CPU 使用,这个转换过程由 `MMU(Memory Management Unit,内存管理单元)` 来完成。在 32 位的操作系统中,虚拟内存空间大小为 0 ~ 4GB。 7 | 8 | 我们通过 图1 来描述虚拟内存地址转换成物理内存地址的过程: 9 | 10 | ![](./images/hugepages/vmemory-pmemory.png) 11 | 12 | 13 | 14 | 如 图1 所示,`页表` 保存的是虚拟内存地址与物理内存地址的映射关系,`MMU` 从 `页表` 中找到虚拟内存地址所映射的物理内存地址,然后把物理内存地址提交给 CPU,这个过程与 Hash 算法相似。 15 | 16 | 内存映射是以内存页作为单位的,通常情况下,一个内存页的大小为 4KB(如图1所示),所以称为 `分页机制`。 17 | 18 | ## 一、内存映射 19 | 20 | 我们来看看在 64 位的 Linux 系统中(英特尔 x64 CPU),虚拟内存地址转换成物理内存地址的过程,如图2: 21 | 22 | ![](./images/hugepages/vmemory-mapping.png) 23 | 24 | 25 | 26 | 从图2可以看出,Linux 只使用了 64 位虚拟内存地址的前 48 位(0 ~ 47位),并且 Linux 把这 48 位虚拟内存地址分为 5 个部分,如下: 27 | 28 | * `PGD索引`:39 ~ 47 位(共9个位),指定在 `页全局目录`(PGD,Page Global Directory)中的索引。 29 | * `PUD索引`:30 ~ 38 位(共9个位),指定在 `页上级目录`(PUD,Page Upper Directory)中的索引。 30 | * `PMD索引`:21 ~ 29 位(共9个位),指定在 `页中间目录`(PMD,Page Middle Directory)中的索引。 31 | * `PTE索引`:12 ~ 20 位(共9个位),指定在 `页表`(PT,Page Table)中的索引。 32 | * `偏移量`:0 ~ 11 位(共12个位),指定在物理内存页中的偏移量。 33 | 34 | 把 图1 中的 `页表` 分为 4 级:`页全局目录`、`页上级目录`、`页中间目录` 和 `页表` 目的是为了减少内存消耗(思考下为什么可以减少内存消耗)。 35 | 36 | > 注意:页全局目录、页上级目录、页中间目录 和 页表 都占用一个 4KB 大小的物理内存页,由于 64 位内存地址占用 8 个字节,所以一个 4KB 大小的物理内存页可以容纳 512 个 64 位内存地址。 37 | 38 | 另外,CPU 有个名为 `CR3` 的寄存器,用于保存 `页全局目录` 的起始物理内存地址(如图2所示)。所以,虚拟内存地址转换成物理内存地址的过程如下: 39 | 40 | * 从 `CR3` 寄存器中获取 `页全局目录` 的物理内存地址,然后以虚拟内存地址的 39 ~ 47 位作为索引,从 `页全局目录` 中读取到 `页上级目录` 的物理内存地址。 41 | * 以虚拟内存地址的 30 ~ 38 位作为索引,从 `页上级目录` 中读取到 `页中间目录` 的物理内存地址。 42 | * 以虚拟内存地址的 21 ~ 29 位作为索引,从 `页中间目录` 中读取到 `页表` 的物理内存地址。 43 | * 以虚拟内存地址的 12 ~ 20 位作为索引,从 `页表` 中读取到 `物理内存页` 的物理内存地址。 44 | * 以虚拟内存地址的 0 ~ 11 位作为 `物理内存页` 的偏移量,得到最终的物理内存地址。 45 | 46 | ## 二、HugePages 原理 47 | 48 | 上面介绍了以 4KB 的内存页作为内存映射的单位,但有些场景我们希望使用更大的内存页作为映射单位(如 2MB)。使用更大的内存页作为映射单位有如下好处: 49 | 50 | * 减少 `TLB(Translation Lookaside Buffer)` 的失效情况。 51 | * 减少 `页表` 的内存消耗。 52 | * 减少 PageFault(缺页中断)的次数。 53 | 54 | > Tips:`TLB` 是一块高速缓存,TLB 缓存虚拟内存地址与其映射的物理内存地址。MMU 首先从 TLB 查找内存映射的关系,如果找到就不用回溯查找页表。否则,只能根据虚拟内存地址,去页表中查找其映射的物理内存地址。 55 | 56 | 因为映射的内存页越大,所需要的 `页表` 就越小(很容易理解);`页表` 越小,TLB 失效的情况就越少。 57 | 58 | 使用大于 4KB 的内存页作为内存映射单位的机制叫 `HugePages`,目前 Linux 常用的 HugePages 大小为 2MB 和 1GB,我们以 2MB 大小的内存页作为例子。 59 | 60 | 要映射更大的内存页,只需要增加偏移量部分,如 图3 所示: 61 | 62 | ![](./images/hugepages/hugepages-mapping.png) 63 | 64 | 65 | 66 | 如 图3 所示,现在把偏移量部分扩展到 21 位(页表部分被覆盖了,21 位能够表示的大小范围为 0 ~ 2MB),所以 `页中间目录` 直接指向映射的 `物理内存页地址`。 67 | 68 | 这样,就可以减少 `页表` 部分的内存消耗。由于内存映射关系变少,所以 TLB 失效的情况也会减少。 69 | 70 | ## 三、HugePages 使用 71 | 72 | 了解了 HugePages 的原理后,我们来介绍一下怎么使用 HugePages。 73 | 74 | HugePages 的使用不像普通内存申请那么简单,而是需要借助 `Hugetlb文件系统` 来创建,下面将会介绍 HugePages 的使用步骤: 75 | 76 | ### 1. 挂载 Hugetlb 文件系统 77 | 78 | Hugetlb 文件系统是专门为 HugePages 而创造的,我们可以通过以下命令来挂载一个 Hugetlb 文件系统: 79 | 80 | ```shell 81 | $ mkdir /mnt/huge 82 | $ mount none /mnt/huge -t hugetlbfs 83 | ``` 84 | 85 | 执行完上面的命令后,我们就在 `/mnt/huge` 目录下挂载了 Hugetlb 文件系统。 86 | 87 | ### 2. 初始化 HugePages 88 | 89 | 要使用 HugePages,首先要向内核声明可以使用的 HugePages 数量。`/proc/sys/vm/nr_hugepages` 文件保存了内核可以使用的 HugePages 数量,我们可以使用以下命令设置新的可用 HugePages 数量: 90 | 91 | ```shell 92 | $ echo 20 > /proc/sys/vm/nr_hugepages 93 | ``` 94 | 95 | 上面命令设置了可用的 HugePages 数量为 20 个(也就是 20 个 2MB 的内存页)。 96 | 97 | ### 3. 编写申请 HugePages 的代码 98 | 99 | 要使用 HugePages,必须使用 `mmap` 系统调用把虚拟内存映射到 Hugetlb 文件系统中的文件,如下代码: 100 | 101 | ```c 102 | #include 103 | #include 104 | #include 105 | #include 106 | 107 | #define MAP_LENGTH (10*1024*1024) // 10MB 108 | 109 | int main() 110 | { 111 | int fd; 112 | void * addr; 113 | 114 | /* 1. 创建一个 Hugetlb 文件系统的文件 */ 115 | fd = open("/mnt/huge/hugepage1", O_CREAT|O_RDWR); 116 | if (fd < 0) { 117 | perror("open()"); 118 | return -1; 119 | } 120 | 121 | /* 2. 把虚拟内存映射到 Hugetlb 文件系统的文件中 */ 122 | addr = mmap(0, MAP_LENGTH, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); 123 | if (addr == MAP_FAILED) { 124 | perror("mmap()"); 125 | close(fd); 126 | unlink("/mnt/huge/hugepage1"); 127 | return -1; 128 | } 129 | 130 | strcpy(addr, "This is HugePages example..."); 131 | printf("%s\n", addr); 132 | 133 | /* 3. 使用完成后,解除映射关系 */ 134 | munmap(addr, MAP_LENGTH); 135 | close(fd); 136 | unlink("/mnt/huge/hugepage1"); 137 | 138 | return 0; 139 | } 140 | ``` 141 | 142 | 编译上面的代码并且执行,如果没有问题,将会输出以下信息: 143 | 144 | ``` 145 | This is HugePages example... 146 | ``` 147 | 148 | ## 四、总结 149 | 150 | 本文主要介绍了 HugePages 的原理和使用,虽然 HugePages 有很多优点,但也有其不足的地方。比如调用 `fork` 系统调用创建子进程时,内核使用了 `写时复制` 的技术(可参考《[Linux 写时复制机制原理](https://mp.weixin.qq.com/s/e6VESPfZPPWpy2emZEGK_g)》一文),在父子进程内存发生改变时,需要复制更大的内存页,从而影响性能。 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /hugepages-source-code-analysis.md: -------------------------------------------------------------------------------- 1 | # HugePages源码分析 2 | 3 | 在《[一文读懂 HugePages的原理](https://mp.weixin.qq.com/s/oOki5zw_Y0BL0KaehvcGrw)》一文中介绍了 HugePages(大内存页)的原理和使用,现在我们来分析一下 Linux 内核是怎么实现 HugePages 分配的。 4 | 5 | > 本文使用 Linux 内核 2.6.23 版本 6 | 7 | ## HugePages分配器初始化 8 | 9 | 在内核初始化时,会调用 `hugetlb_init` 函数对 HugePages 分配器进行初始化,其实现如下: 10 | 11 | ```c 12 | static int __init hugetlb_init(void) 13 | { 14 | unsigned long i; 15 | 16 | // 1. 初始化空闲大内存页链表 hugepage_freelists, 17 | // 内核使用 hugepage_freelists 链表把空闲的大内存页连接起来, 18 | // 为了分析简单,我们可以把 MAX_NUMNODES 当成 1 19 | for (i = 0; i < MAX_NUMNODES; ++i) 20 | INIT_LIST_HEAD(&hugepage_freelists[i]); 21 | 22 | // 2. max_huge_pages 为系统能够使用的大页内存的数量, 23 | // 由系统启动项 hugepages 指定, 24 | // 这里主要申请大内存页, 并且保存到 hugepage_freelists 链表中. 25 | for (i = 0; i < max_huge_pages; ++i) { 26 | if (!alloc_fresh_huge_page()) 27 | break; 28 | } 29 | 30 | max_huge_pages = free_huge_pages = nr_huge_pages = i; 31 | 32 | return 0; 33 | } 34 | ``` 35 | 36 | `hugetlb_init` 函数主要完成两个工作: 37 | 38 | * 初始化空闲大内存页链表 `hugepage_freelists`,这个链表保存了系统中能够使用的大内存。 39 | * 为系统申请空闲的大内存页,并且保存到 `hugepage_freelists` 链表中。 40 | 41 | 我们再来分析下 `alloc_fresh_huge_page` 函数是怎么申请大内存页的,其实现如下: 42 | 43 | ```c 44 | static int alloc_fresh_huge_page(void) 45 | { 46 | static int prev_nid; 47 | struct page *page; 48 | int nid; 49 | ... 50 | // 1. 申请一个大的物理内存页... 51 | page = alloc_pages_node(nid, htlb_alloc_mask|__GFP_COMP|__GFP_NOWARN, 52 | HUGETLB_PAGE_ORDER); 53 | 54 | if (page) { 55 | // 2. 设置释放大内存页的回调函数为 free_huge_page 56 | set_compound_page_dtor(page, free_huge_page); 57 | ... 58 | // 3. put_page 函数将会调用上面设置的 free_huge_page 函数把内存页放入到缓存队列中 59 | put_page(page); 60 | 61 | return 1; 62 | } 63 | 64 | return 0; 65 | } 66 | ``` 67 | 68 | 所以,`alloc_fresh_huge_page` 函数主要完成三个工作: 69 | 70 | * 调用 `alloc_pages_node` 函数申请一个大内存页(2MB)。 71 | * 设置大内存页的释放回调函数为 `free_huge_page`,当释放大内存页时,将会调用这个函数进行释放操作。 72 | * 调用 `put_page` 函数释放大内存页,其将会调用 `free_huge_page` 函数进行相关操作。 73 | 74 | 那么,我们来看看 `free_huge_page` 函数是怎么释放大内存页的,其实现如下: 75 | 76 | ```c 77 | static void free_huge_page(struct page *page) 78 | { 79 | ... 80 | enqueue_huge_page(page); // 把大内存页放置到空闲大内存页链表中 81 | ... 82 | } 83 | ``` 84 | 85 | `free_huge_page` 函数主要调用 `enqueue_huge_page` 函数把大内存页添加到空闲大内存页链表中,其实现如下: 86 | 87 | ```c 88 | static void enqueue_huge_page(struct page *page) 89 | { 90 | int nid = page_to_nid(page); // 我们假设这里一定返回 0 91 | 92 | // 把大内存页添加到空闲链表 hugepage_freelists 中 93 | list_add(&page->lru, &hugepage_freelists[nid]); 94 | 95 | // 增加计数器 96 | free_huge_pages++; 97 | free_huge_pages_node[nid]++; 98 | } 99 | ``` 100 | 101 | 从上面的实现可知,`enqueue_huge_page` 函数只是简单的把大内存页添加到空闲链表 `hugepage_freelists` 中,并且增加计数器。 102 | 103 | 假如我们设置了系统能够使用的大内存页为 100 个,那么空闲大内存页链表 `hugepage_freelists` 的结构如下图所示: 104 | 105 | ![](./images/hugepages/hugepages-free-list.png) 106 | 107 | 所以,HugePages 分配器初始化的调用链为: 108 | 109 | ```asp 110 | hugetlb_init() 111 | | 112 | +——> alloc_fresh_huge_page() 113 | | 114 | |——> alloc_pages_node() 115 | |——> set_compound_page_dtor() 116 | +——> put_page() 117 | | 118 | +——> free_huge_page() 119 | | 120 | +——> enqueue_huge_page() 121 | ``` 122 | 123 | ## hugetlbfs 文件系统 124 | 125 | 为系统准备好空闲的大内存页后,现在来了解下怎样分配大内存页。在《[一文读懂 HugePages的原理](https://mp.weixin.qq.com/s/oOki5zw_Y0BL0KaehvcGrw)》一文中介绍过,要申请大内存页,必须使用 `mmap` 系统调用把虚拟内存映射到 `hugetlbfs` 文件系统中的文件中。 126 | 127 | 免去繁琐的文件系统挂载过程,我们主要来看看当使用 `mmap` 系统调用把虚拟内存映射到 `hugetlbfs` 文件系统的文件时会发生什么事情。 128 | 129 | 每个文件描述符对象都有个 `mmap` 的方法,此方法会在调用 `mmap` 函数映射到文件时被触发,我们来看看 `hugetlbfs` 文件的 `mmap` 方法所对应的真实函数,如下: 130 | 131 | ```c 132 | const struct file_operations hugetlbfs_file_operations = { 133 | .mmap = hugetlbfs_file_mmap, 134 | .fsync = simple_sync_file, 135 | .get_unmapped_area = hugetlb_get_unmapped_area, 136 | }; 137 | ``` 138 | 139 | 从上面的代码可以发现,`hugetlbfs` 文件的 `mmap` 方法被设置为 `hugetlbfs_file_mmap` 函数。所以当调用 `mmap` 函数映射 `hugetlbfs` 文件时,将会调用 `hugetlbfs_file_mmap` 函数来处理。 140 | 141 | 而 `hugetlbfs_file_mmap` 函数最主要的工作就是把虚拟内存分区对象的 `vm_flags` 字段添加 `VM_HUGETLB` 标志位,如下代码: 142 | 143 | ```c 144 | static int 145 | hugetlbfs_file_mmap(struct file *file, struct vm_area_struct *vma) 146 | { 147 | ... 148 | vma->vm_flags |= VM_HUGETLB | VM_RESERVED; // 为虚拟内存分区添加 VM_HUGETLB 标志位 149 | ... 150 | return ret; 151 | } 152 | ``` 153 | 154 | 为虚拟内存分区对象设置 `VM_HUGETLB` 标志位的作用是:当对虚拟内存分区进行物理内存映射时,会进行特殊的处理,下面将会介绍。 155 | 156 | ## 虚拟内存与物理内存映射 157 | 158 | 使用 `mmap` 函数映射到 `hugetlbfs` 文件后,会返回一个虚拟内存地址。当对这个虚拟内存地址进行访问(读写)时,由于此虚拟内存地址还没有与物理内存地址进行映射,将会触发 `缺页异常`,内核会调用 `do_page_fault` 函数对 `缺页异常` 进行修复。 159 | 160 | 我们来看看整个流程,如下图所示: 161 | 162 | ![](./images/hugepages/mmap-syscall.png) 163 | 164 | 165 | 166 | 所以,最终会调用 `do_page_fault` 函数对 `缺页异常` 进行修复操作,我们来看看 `do_page_fault` 做了什么工作,实现如下: 167 | 168 | ```c 169 | asmlinkage void 170 | __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code) 171 | { 172 | ... 173 | struct mm_struct *mm; 174 | struct vm_area_struct *vma; 175 | unsigned long address; 176 | ... 177 | 178 | mm = tsk->mm; // 1. 获取当前进程对应的内存管理对象 179 | address = read_cr2(); // 2. 获取触发缺页异常的虚拟内存地址 180 | 181 | ... 182 | vma = find_vma(mm, address); // 3. 通过虚拟内存地址获取对应的虚拟内存分区对象 183 | ... 184 | 185 | // 4. 调用 handle_mm_fault 函数对异常进行修复 186 | fault = handle_mm_fault(mm, vma, address, write); 187 | ... 188 | 189 | return; 190 | } 191 | ``` 192 | 193 | 上面代码对 `do_page_fault` 进行了精简,精简后主要完成4个工作: 194 | 195 | * 获取当前进程对应的内存管理对象。 196 | * 调用 `read_cr2` 获取触发缺页异常的虚拟内存地址。 197 | * 通过触发 `缺页异常` 的虚拟内存地址获取对应的虚拟内存分区对象。 198 | * 调用 `handle_mm_fault` 函数对 `缺页异常` 进行修复。 199 | 200 | 我们继续来看看 `handle_mm_fault` 函数的实现,代码如下: 201 | 202 | ```c 203 | int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, 204 | unsigned long address, int write_access) 205 | { 206 | ... 207 | if (unlikely(is_vm_hugetlb_page(vma))) // 虚拟内存分区是否需要使用 HugePages 208 | return hugetlb_fault(mm, vma, address, write_access); // 如果使用 HugePages, 就调用 hugetlb_fault 进行处理 209 | ... 210 | } 211 | ``` 212 | 213 | 对 `handle_mm_fault` 函数进行精简后,逻辑就非常清晰。如果虚拟内存分区使用 HugePages,那么就调用 `hugetlb_fault` 函数进行处理(由于我们分析使用 HugePages 的情况,所以刚好进入这个分支)。 214 | 215 | `hugetlb_fault` 函数主要对进程的页表进行填充,所以我们先来回顾一下 HugePages 对应的页表结构,如下图: 216 | 217 | ![](./images/hugepages/hugepages-mapping.png) 218 | 219 | 220 | 221 | 从上图可以看出,使用 HugePages 后,`页中间目录` 直接指向物理内存页。所以,`hugetlb_fault` 函数主要就是对 `页中间目录项` 进行填充。其实现如下: 222 | 223 | ```c 224 | int hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma, 225 | unsigned long address, int write_access) 226 | { 227 | pte_t *ptep; 228 | pte_t entry; 229 | int ret; 230 | 231 | ptep = huge_pte_alloc(mm, address); // 1. 找到虚拟内存地址对应的页中间目录项 232 | ... 233 | entry = *ptep; 234 | 235 | if (pte_none(entry)) { // 如果页中间目录项还没进行映射 236 | // 2. 那么调用 hugetlb_no_page 函数进行映射操作 237 | ret = hugetlb_no_page(mm, vma, address, ptep, write_access); 238 | ... 239 | return ret; 240 | } 241 | ... 242 | } 243 | ``` 244 | 245 | 对 `hugetlb_fault` 函数进行精简后,主要完成两个工作: 246 | 247 | * 通过触发 `缺页异常` 的虚拟内存地址找到其对应的 `页中间目录项`。 248 | * 调用 `hugetlb_no_page` 函数对 `页中间目录项` 进行映射操作。 249 | 250 | 我们再来看看 `hugetlb_no_page` 函数怎么对 `页中间目录项` 进行填充: 251 | 252 | ```c 253 | static int 254 | hugetlb_no_page(struct mm_struct *mm, struct vm_area_struct *vma, 255 | unsigned long address, pte_t *ptep, int write_access) 256 | { 257 | ... 258 | page = find_lock_page(mapping, idx); 259 | if (!page) { 260 | ... 261 | // 1. 从空闲大内存页链表 hugepage_freelists 中申请一个大内存页 262 | page = alloc_huge_page(vma, address); 263 | ... 264 | } 265 | ... 266 | // 2. 通过大内存页的物理地址生成页中间目录项的值 267 | new_pte = make_huge_pte(vma, page, ((vma->vm_flags & VM_WRITE) 268 | && (vma->vm_flags & VM_SHARED))); 269 | 270 | // 3. 设置页中间目录项的值为上面生成的值 271 | set_huge_pte_at(mm, address, ptep, new_pte); 272 | ... 273 | return ret; 274 | } 275 | ``` 276 | 277 | 通过对 `hugetlb_no_page` 函数进行精简后,主要完成3个工作: 278 | 279 | * 调用 `alloc_huge_page` 函数从空闲大内存页链表 `hugepage_freelists` 中申请一个大内存页。 280 | * 通过大内存页的物理地址生成页中间目录项的值。 281 | * 设置页中间目录项的值为上面生成的值。 282 | 283 | 至此,HugePages 的映射过程已经完成。 284 | 285 | > 还有个问题,就是 CPU 怎么知道 `页中间表项` 指向的是 `页表` 还是 `大内存页` 呢? 286 | > 287 | > 这是因为 `页中间表项` 有个 `PSE` 的标志位,如果将其设置为1,那么就表明其指向 `大内存页` ,否则就指向 `页表`。 288 | 289 | ## 总结 290 | 291 | 本文介绍了 HugePages 实现的整个流程,当然本文也只是介绍了申请内存的流程,释放内存的流程并没有分析,如果有兴趣的话可以自己查阅源码。 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /images/8259A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/8259A.png -------------------------------------------------------------------------------- /images/APIC.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/APIC.gif -------------------------------------------------------------------------------- /images/APIC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/APIC.png -------------------------------------------------------------------------------- /images/aio/aio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/aio/aio.png -------------------------------------------------------------------------------- /images/aio/io_submit_once.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/aio/io_submit_once.png -------------------------------------------------------------------------------- /images/aio/kioctx-struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/aio/kioctx-struct.png -------------------------------------------------------------------------------- /images/aio/linux-native-aio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/aio/linux-native-aio.png -------------------------------------------------------------------------------- /images/aio/ring-buffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/aio/ring-buffer.png -------------------------------------------------------------------------------- /images/arp-broadcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/arp-broadcast.png -------------------------------------------------------------------------------- /images/arp-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/arp-header.png -------------------------------------------------------------------------------- /images/buffered-io-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/buffered-io-2.jpg -------------------------------------------------------------------------------- /images/buffered-io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/buffered-io.png -------------------------------------------------------------------------------- /images/cgroup-base.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-base.jpg -------------------------------------------------------------------------------- /images/cgroup-links.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-links.jpg -------------------------------------------------------------------------------- /images/cgroup-rule1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-rule1.jpeg -------------------------------------------------------------------------------- /images/cgroup-rule2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-rule2.jpeg -------------------------------------------------------------------------------- /images/cgroup-rule3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-rule3.jpeg -------------------------------------------------------------------------------- /images/cgroup-rule4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-rule4.jpeg -------------------------------------------------------------------------------- /images/cgroup-state-memory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-state-memory.jpg -------------------------------------------------------------------------------- /images/cgroup-subsys-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-subsys-state.jpg -------------------------------------------------------------------------------- /images/cgroup-task-cssset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cgroup-task-cssset.jpg -------------------------------------------------------------------------------- /images/concurrency-synchronize-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/concurrency-synchronize-1.png -------------------------------------------------------------------------------- /images/concurrency-synchronize-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/concurrency-synchronize-2.png -------------------------------------------------------------------------------- /images/concurrency-synchronize-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/concurrency-synchronize-3.jpg -------------------------------------------------------------------------------- /images/concurrency-synchronize-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/concurrency-synchronize-4.jpg -------------------------------------------------------------------------------- /images/concurrency-synchronize-semaphore.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/concurrency-synchronize-semaphore.jpg -------------------------------------------------------------------------------- /images/concurrency-synchronize-spinlock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/concurrency-synchronize-spinlock.jpg -------------------------------------------------------------------------------- /images/cpu-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/cpu-timeline.png -------------------------------------------------------------------------------- /images/csf-runqueue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/csf-runqueue.png -------------------------------------------------------------------------------- /images/device-block.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/device-block.jpg -------------------------------------------------------------------------------- /images/dr-arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/dr-arch.jpg -------------------------------------------------------------------------------- /images/dr-package.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/dr-package.jpg -------------------------------------------------------------------------------- /images/eflags-register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/eflags-register.png -------------------------------------------------------------------------------- /images/epoll-eventpoll.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/epoll-eventpoll.jpg -------------------------------------------------------------------------------- /images/epoll_principle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/epoll_principle.jpg -------------------------------------------------------------------------------- /images/ext2-filesystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ext2-filesystem.png -------------------------------------------------------------------------------- /images/ext2_filesystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ext2_filesystem.png -------------------------------------------------------------------------------- /images/hugepages/hugepages-free-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/hugepages/hugepages-free-list.png -------------------------------------------------------------------------------- /images/hugepages/hugepages-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/hugepages/hugepages-mapping.png -------------------------------------------------------------------------------- /images/hugepages/mmap-syscall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/hugepages/mmap-syscall.png -------------------------------------------------------------------------------- /images/hugepages/vmemory-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/hugepages/vmemory-mapping.png -------------------------------------------------------------------------------- /images/hugepages/vmemory-pmemory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/hugepages/vmemory-pmemory.png -------------------------------------------------------------------------------- /images/inotify2/inotify-device-handle-watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/inotify2/inotify-device-handle-watch.png -------------------------------------------------------------------------------- /images/inotify2/inotify-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/inotify2/inotify-device.png -------------------------------------------------------------------------------- /images/inotify2/inotify-events-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/inotify2/inotify-events-list.png -------------------------------------------------------------------------------- /images/inotify2/inotify-principle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/inotify2/inotify-principle.png -------------------------------------------------------------------------------- /images/inotify2/inotify-watch-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/inotify2/inotify-watch-list.png -------------------------------------------------------------------------------- /images/interrupt_hardware.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/interrupt_hardware.gif -------------------------------------------------------------------------------- /images/ip-address-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ip-address-1.png -------------------------------------------------------------------------------- /images/ip-address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ip-address.png -------------------------------------------------------------------------------- /images/ip-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ip-header.png -------------------------------------------------------------------------------- /images/ip-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ip-message.png -------------------------------------------------------------------------------- /images/ip-network-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ip-network-2.png -------------------------------------------------------------------------------- /images/ip-package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ip-package.png -------------------------------------------------------------------------------- /images/ip-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/ip-router.png -------------------------------------------------------------------------------- /images/irq_desc_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/irq_desc_t.jpg -------------------------------------------------------------------------------- /images/kprobes/kprobe-struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/kprobes/kprobe-struct.png -------------------------------------------------------------------------------- /images/kswapd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/kswapd.png -------------------------------------------------------------------------------- /images/lining-physical-mapping.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/lining-physical-mapping.jpg -------------------------------------------------------------------------------- /images/linux-filesystem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/linux-filesystem.jpg -------------------------------------------------------------------------------- /images/lvs-connection-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/lvs-connection-process.png -------------------------------------------------------------------------------- /images/lvs-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/lvs-connection.png -------------------------------------------------------------------------------- /images/lvs-hooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/lvs-hooks.png -------------------------------------------------------------------------------- /images/lvs-roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/lvs-roles.png -------------------------------------------------------------------------------- /images/lvs-scheduler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/lvs-scheduler.png -------------------------------------------------------------------------------- /images/memory-address.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory-address.jpeg -------------------------------------------------------------------------------- /images/memory-mapping/copy-on-write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory-mapping/copy-on-write.png -------------------------------------------------------------------------------- /images/memory-mapping/memory-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory-mapping/memory-mapping.png -------------------------------------------------------------------------------- /images/memory-mapping/share-memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory-mapping/share-memory.png -------------------------------------------------------------------------------- /images/memory_free_area.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_free_area.jpg -------------------------------------------------------------------------------- /images/memory_free_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_free_list.png -------------------------------------------------------------------------------- /images/memory_lru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_lru.jpg -------------------------------------------------------------------------------- /images/memory_map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_map.jpg -------------------------------------------------------------------------------- /images/memory_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_page.jpg -------------------------------------------------------------------------------- /images/memory_slab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_slab.png -------------------------------------------------------------------------------- /images/memory_slab_global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_slab_global.png -------------------------------------------------------------------------------- /images/memory_zone.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/memory_zone.gif -------------------------------------------------------------------------------- /images/minix-filesystem-read.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/minix-filesystem-read.jpg -------------------------------------------------------------------------------- /images/minix_filesystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/minix_filesystem.png -------------------------------------------------------------------------------- /images/minix_filesystem_inode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/minix_filesystem_inode.jpg -------------------------------------------------------------------------------- /images/mmap-memory-address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/mmap-memory-address.png -------------------------------------------------------------------------------- /images/nat-arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/nat-arch.jpg -------------------------------------------------------------------------------- /images/nat-package.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/nat-package.jpg -------------------------------------------------------------------------------- /images/neighbour-arp-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/neighbour-arp-queue.png -------------------------------------------------------------------------------- /images/neighbour-nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/neighbour-nodes.png -------------------------------------------------------------------------------- /images/neighbour-struct-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/neighbour-struct-new.png -------------------------------------------------------------------------------- /images/neighbour-struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/neighbour-struct.png -------------------------------------------------------------------------------- /images/net-bridge/bridge-packet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/net-bridge/bridge-packet.jpg -------------------------------------------------------------------------------- /images/net-bridge/bridge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/net-bridge/bridge.jpg -------------------------------------------------------------------------------- /images/net-bridge/docker-bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/net-bridge/docker-bridge.png -------------------------------------------------------------------------------- /images/net-bridge/net-bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/net-bridge/net-bridge.png -------------------------------------------------------------------------------- /images/net-bridge/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/net-bridge/switch.png -------------------------------------------------------------------------------- /images/netfilter-hooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/netfilter-hooks.png -------------------------------------------------------------------------------- /images/netfilter-hooks2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/netfilter-hooks2.png -------------------------------------------------------------------------------- /images/network/fib-structs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/network/fib-structs.jpg -------------------------------------------------------------------------------- /images/network/ip-route.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/network/ip-route.jpg -------------------------------------------------------------------------------- /images/overlayfs-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/overlayfs-map.png -------------------------------------------------------------------------------- /images/overlayfs-mount.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/overlayfs-mount.jpg -------------------------------------------------------------------------------- /images/overlayfs-relation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/overlayfs-relation.jpg -------------------------------------------------------------------------------- /images/pid-namespace-level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/pid-namespace-level.png -------------------------------------------------------------------------------- /images/pid-namespace-structs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/pid-namespace-structs.png -------------------------------------------------------------------------------- /images/process-schedule-o1-move.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/process-schedule-o1-move.jpg -------------------------------------------------------------------------------- /images/process-schedule-o1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/process-schedule-o1.jpg -------------------------------------------------------------------------------- /images/process_vm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/process_vm.jpg -------------------------------------------------------------------------------- /images/qrcode_linux_naxieshi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/qrcode_linux_naxieshi.jpg -------------------------------------------------------------------------------- /images/rcu-grace-period.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/rcu-grace-period.png -------------------------------------------------------------------------------- /images/read-write-system-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/read-write-system-call.png -------------------------------------------------------------------------------- /images/red-black-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/red-black-tree.png -------------------------------------------------------------------------------- /images/select-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/select-model.png -------------------------------------------------------------------------------- /images/semgent-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/semgent-selector.png -------------------------------------------------------------------------------- /images/semget-selector-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/semget-selector-table.png -------------------------------------------------------------------------------- /images/seqlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/seqlock.png -------------------------------------------------------------------------------- /images/shm-map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/shm-map.jpg -------------------------------------------------------------------------------- /images/signal-kernel-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/signal-kernel-stack.png -------------------------------------------------------------------------------- /images/signal-user-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/signal-user-stack.png -------------------------------------------------------------------------------- /images/signal1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/signal1.png -------------------------------------------------------------------------------- /images/single-trace.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/single-trace.jpg -------------------------------------------------------------------------------- /images/socket-layer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/socket-layer.jpg -------------------------------------------------------------------------------- /images/socket_interface.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/socket_interface.jpg -------------------------------------------------------------------------------- /images/socket_unix_socket_call_stack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/socket_unix_socket_call_stack.jpg -------------------------------------------------------------------------------- /images/stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/stat.png -------------------------------------------------------------------------------- /images/system_call.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/system_call.gif -------------------------------------------------------------------------------- /images/task_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/task_list.png -------------------------------------------------------------------------------- /images/task_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/task_stack.png -------------------------------------------------------------------------------- /images/task_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/task_state.png -------------------------------------------------------------------------------- /images/tcp-ip-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/tcp-ip-layer.png -------------------------------------------------------------------------------- /images/tcp/syn-flood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/tcp/syn-flood.png -------------------------------------------------------------------------------- /images/tcp/tcp-established-hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/tcp/tcp-established-hash.png -------------------------------------------------------------------------------- /images/tcp/tcp-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/tcp/tcp-header.png -------------------------------------------------------------------------------- /images/tcp/tcp-ip-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/tcp/tcp-ip-layer.png -------------------------------------------------------------------------------- /images/tcp/tcp-pseudo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/tcp/tcp-pseudo-header.png -------------------------------------------------------------------------------- /images/tcp/three-way-handshake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/tcp/three-way-handshake.png -------------------------------------------------------------------------------- /images/timer-Wheel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/timer-Wheel.jpg -------------------------------------------------------------------------------- /images/timer-heap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/timer-heap.jpg -------------------------------------------------------------------------------- /images/timer-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/timer-list.jpg -------------------------------------------------------------------------------- /images/timer-tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/timer-tree.jpg -------------------------------------------------------------------------------- /images/timer-vts-pointer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/timer-vts-pointer.jpg -------------------------------------------------------------------------------- /images/timer-vts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/timer-vts.jpg -------------------------------------------------------------------------------- /images/timer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/timer.jpg -------------------------------------------------------------------------------- /images/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/top.png -------------------------------------------------------------------------------- /images/traceme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/traceme.jpg -------------------------------------------------------------------------------- /images/udp/tcp-ip-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/udp/tcp-ip-layer.png -------------------------------------------------------------------------------- /images/udp/udp-header-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/udp/udp-header-2.png -------------------------------------------------------------------------------- /images/udp/udp-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/udp/udp-header.png -------------------------------------------------------------------------------- /images/udp/udp-recv-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/udp/udp-recv-process.png -------------------------------------------------------------------------------- /images/udp/udp-schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/udp/udp-schedule.png -------------------------------------------------------------------------------- /images/udp/udp-sendmsg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/udp/udp-sendmsg.png -------------------------------------------------------------------------------- /images/vfs-struct.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vfs-struct.jpg -------------------------------------------------------------------------------- /images/vfs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vfs.jpg -------------------------------------------------------------------------------- /images/vfs_struct.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vfs_struct.jpg -------------------------------------------------------------------------------- /images/virtaul-memory-manager/elf-file-format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/virtaul-memory-manager/elf-file-format.png -------------------------------------------------------------------------------- /images/virtaul-memory-manager/elf-sections-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/virtaul-memory-manager/elf-sections-list.png -------------------------------------------------------------------------------- /images/virtaul-memory-manager/virtual-memory-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/virtaul-memory-manager/virtual-memory-layout.png -------------------------------------------------------------------------------- /images/virtaul-memory-manager/vm-area-struct-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/virtaul-memory-manager/vm-area-struct-layout.png -------------------------------------------------------------------------------- /images/vm_address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vm_address.png -------------------------------------------------------------------------------- /images/vma-pma-maping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vma-pma-maping.png -------------------------------------------------------------------------------- /images/vmalloc-address-manager.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vmalloc-address-manager.jpg -------------------------------------------------------------------------------- /images/vmalloc-map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vmalloc-map.jpg -------------------------------------------------------------------------------- /images/vmalloc-memory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/vmalloc-memory.jpg -------------------------------------------------------------------------------- /images/waitqueue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/waitqueue.jpg -------------------------------------------------------------------------------- /images/workqueue/workqueue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/workqueue/workqueue.png -------------------------------------------------------------------------------- /images/x86-segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/x86-segment.png -------------------------------------------------------------------------------- /images/zerocopy/read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/zerocopy/read.png -------------------------------------------------------------------------------- /images/zerocopy/sendfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/zerocopy/sendfile.png -------------------------------------------------------------------------------- /images/zerocopy/sendfile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/zerocopy/sendfile2.png -------------------------------------------------------------------------------- /images/zerocopy/userspace-kernelspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/zerocopy/userspace-kernelspace.png -------------------------------------------------------------------------------- /images/zerocopy/write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/images/zerocopy/write.png -------------------------------------------------------------------------------- /in-interrupt-principle.md: -------------------------------------------------------------------------------- 1 | # in_interrupt() 原理 2 | 3 | `in_interrupt()` 宏主要用于判断当前执行上下文是否处于中断上下文中,其定义如下: 4 | 5 | ```c 6 | 7 | #define in_interrupt() irq_count() 8 | #define irq_count() (preempt_count() & (0x0FFF0000 | 0x0000FF00)) 9 | #define preempt_count() (current_thread_info()->preempt_count) 10 | 11 | ``` 12 | 13 | 所以最终展开如下: 14 | 15 | ```c 16 | #define in_interrupt() ((current_thread_info()->preempt_count) & (0x0FFFFF00)) 17 | ``` 18 | 19 | 其中 `current_thread_info()` 函数会返回一个类型为 `struct thread_info` 的指针,此指针指向当前线程的信息,`struct thread_info` 结构定义如下: 20 | 21 | ```c 22 | struct thread_info { 23 | struct task_struct *task; /* main task structure */ 24 | ... 25 | unsigned long flags; /* low level flags */ 26 | unsigned long status; /* thread-synchronous flags */ 27 | __u32 cpu; /* current CPU */ 28 | int preempt_count; /* 0 => preemptable, <0 => BUG */ 29 | ... 30 | }; 31 | ``` 32 | 33 | `struct thread_info` 结构中的 `preempt_count` 字段有 3 个用途: 34 | 35 | * 记录当前执行上下文是否处于硬中断上下文中。 36 | * 记录当前执行上下文是否处于软中断上下文中。 37 | * 记录当前执行上下文是否禁止内核抢占。 38 | 39 | 由于 `preempt_count` 字段的类型为 `整型`,所以内核将其分为3个部分,如下图所示: 40 | 41 | ```text 42 | 43 | hardirq softirq preempt 44 | / \/ \/ \ 45 | +====+============+========+========+ 46 | |....|............|........|........| 47 | +====+============+========+========+ 48 | 31 0 49 | 50 | ``` 51 | 52 | * 内核抢占计数器占用 `preempt_count` 字段的 `0 ~ 7` 位。 53 | * 软中断中断计数器占用 `preempt_count` 字段的 `8 ~ 15` 位。 54 | * 硬中断中断计数器占用 `preempt_count` 字段的 `16 ~ 27` 位。 55 | 56 | 当外部设备发生中断时,内核会调用 `irq_enter()` 宏来标记进入硬中断上下文,`irq_enter()` 宏展开后如下所示: 57 | 58 | ```c 59 | #define irq_enter() \ 60 | do { current_thread_info()->preempt_count += (1UL << 16); } while (0) 61 | ``` 62 | 63 | 从上面代码可以看出,`irq_enter()` 宏就是增加硬中断计数器的值。 64 | -------------------------------------------------------------------------------- /interrupt_hardware.md: -------------------------------------------------------------------------------- 1 | ## 什么是中断 2 | `中断` 是为了解决外部设备完成某些工作后通知CPU的一种机制(譬如硬盘完成读写操作后通过中断告知CPU已经完成)。早期没有中断机制的计算机就不得不通过轮询来查询外部设备的状态,由于轮询是试探查询的(也就是说设备不一定是就绪状态),所以往往要做很多无用的查询,从而导致效率非常低下。由于中断是由外部设备主动通知CPU的,所以不需要CPU进行轮询去查询,效率大大提升。 3 | 4 | 从物理学的角度看,中断是一种电信号,由硬件设备产生,并直接送入中断控制器(如 8259A)的输入引脚上,然后再由中断控制器向处理器发送相应的信号。处理器一经检测到该信号,便中断自己当前正在处理的工作,转而去处理中断。此后,处理器会通知 OS 已经产生中断。这样,OS 就可以对这个中断进行适当的处理。不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识,这些值通常被称为中断请求线。 5 | 6 | ## 中断控制器 7 | X86计算机的 CPU 为中断只提供了两条外接引脚:NMI 和 INTR。其中 NMI 是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。 8 | 9 | 常见的中断控制器有两种: 10 | 11 | ### 可编程中断控制器8259A 12 | 13 | 传统的 PIC(Programmable Interrupt Controller,可编程中断控制器)是由两片 8259A 风格的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达 8 个不同的 IRQ。因为从 PIC 的 INT 输出线连接到主 PIC 的 IRQ2 引脚,所以可用 IRQ 线的个数达到 15 个,如图下所示。 14 | 15 | ![8259A](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/8259A.png) 16 | 17 | ### 高级可编程中断控制器(APIC) 18 | 19 | 8259A 只适合单 CPU 的情况,为了充分挖掘 SMP 体系结构的并行性,能够把中断传递给系统中的每个 CPU 至关重要。基于此理由,Intel 引入了一种名为 I/O 高级可编程控制器的新组件,来替代老式的 8259A 可编程中断控制器。该组件包含两大组成部分:一是“本地 APIC”,主要负责传递中断信号到指定的处理器;举例来说,一台具有三个处理器的机器,则它必须相对的要有三个本地 APIC。另外一个重要的部分是 I/O APIC,主要是收集来自 I/O 装置的 Interrupt 信号且在当那些装置需要中断时发送信号到本地 APIC,系统中最多可拥有 8 个 I/O APIC。 20 | 21 | 每个本地 APIC 都有 32 位的寄存器,一个内部时钟,一个本地定时设备以及为本地中断保留的两条额外的 IRQ 线 LINT0 和 LINT1。所有本地 APIC 都连接到 I/O APIC,形成一个多级 APIC 系统,如图下所示。 22 | 23 | ![APIC](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/APIC.gif) 24 | 25 | 目前大部分单处理器系统都包含一个 I/O APIC 芯片,可以通过以下两种方式来对这种芯片进行配置: 26 | * 作为一种标准的 8259A 工作方式。本地 APIC 被禁止,外部 I/O APIC 连接到 CPU,两条 LINT0 和 LINT1 分别连接到 INTR 和 NMI 引脚。 27 | * 作为一种标准外部 I/O APIC。本地 APIC 被激活,且所有的外部中断都通过 I/O APIC 接收。 28 | 29 | 辨别一个系统是否正在使用 I/O APIC,可以在命令行输入如下命令: 30 | ```shell 31 | # cat /proc/interrupts 32 |            CPU0       33 |   0:      90504    IO-APIC-edge  timer 34 |   1:        131    IO-APIC-edge  i8042 35 |   8:          4    IO-APIC-edge  rtc 36 |   9:          0    IO-APIC-level  acpi 37 |  12:        111    IO-APIC-edge  i8042 38 |  14:       1862    IO-APIC-edge  ide0 39 |  15:         28    IO-APIC-edge  ide1 40 | 177:          9    IO-APIC-level  eth0 41 | 185:          0    IO-APIC-level  via82cxxx 42 | ... 43 | ``` 44 | 如果输出结果中列出了 IO-APIC,说明您的系统正在使用 APIC。如果看到 XT-PIC,意味着您的系统正在使用 8259A 芯片。 45 | 46 | ## 中断分类 47 | 中断可分为同步(synchronous)中断和异步(asynchronous)中断: 48 | * 同步中断是当指令执行时由 CPU 控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后 CPU 才会发出中断,而不是发生在代码指令执行期间,比如系统调用。 49 | * 异步中断是指由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能够在指令之间发生,例如键盘中断。 50 | 51 | 根据 Intel 官方资料,同步中断称为异常(exception),异步中断被称为中断(interrupt)。 52 | 53 | 中断可分为 `可屏蔽中断`(Maskable interrupt)和 `非屏蔽中断`(Nomaskable interrupt)。异常可分为 `故障`(fault)、`陷阱`(trap)、`终止`(abort)三类。 54 | 55 | 从广义上讲,中断可分为四类:`中断`、`故障`、`陷阱`、`终止`。这些类别之间的异同点请参看 表。 56 | 57 | 表:中断类别及其行为 58 | 59 | 类别|原因|异步/同步|返回行为 60 | :-: | :-: | :-: | :-: 61 | 中断|来自I/O设备的信号|异步|总是返回到下一条指令 62 | 陷阱|有意的异常|同步|总是返回到下一条指令 63 | 故障|潜在可恢复的错误|同步|返回到当前指令 64 | 终止|不可恢复的错误|同步|不会返回 65 | 66 | X86 体系结构的每个中断都被赋予一个唯一的编号或者向量(8 位无符号整数)。非屏蔽中断和异常向量是固定的,而可屏蔽中断向量可以通过对中断控制器的编程来改变。 67 | 68 | > 本文来源于:[Linux 内核中断内幕](https://www.ibm.com/developerworks/cn/linux/l-cn-linuxkernelint/) 69 | > 参考资料:[详解8259A](https://blog.csdn.net/longintchar/article/details/79439466) 70 | -------------------------------------------------------------------------------- /iowait.md: -------------------------------------------------------------------------------- 1 | # 什么是iowait 2 | 3 | 我们对系统性能进行优化时,一般会使用 `top` 命令来查看系统负载和系统中各个进程的运行情况,从而找出影响系统性能的因素。如下图所示: 4 | 5 | ![top](./images/top.png) 6 | 7 | `top` 命令会输出很多系统相关的信息,如:系统负载、系统中的进程数、CPU使用率和内存使用率等,这些信息对排查系统性能问题起着至关重要的作用。 8 | 9 | 本文主要介绍 `top` 命令中的 `iowait` 指标(如上图中红色方框所示)的含义和作用。 10 | 11 | ## 什么是iowait 12 | 13 | 什么是 `iowait`?我们来看看 Linux 的解释: 14 | 15 | > Show the percentage of time that the CPU or CPUs were idle during which the system had an outstanding disk I/O request. 16 | 17 | 中文翻译的意思就是:CPU 在等待磁盘 I/O 请求完成时,处于空闲状态的时间百分比(此时正在运行着 `idle` 进程)。 18 | 19 | 可以看出,如果系统处于 `iowait` 状态,那么必须满足以下两个条件: 20 | 21 | 1. 系统中存在等待 I/O 请求完成的进程。 22 | 2. 系统当前正处于空闲状态,也就是说没有可运行的进程。 23 | 24 | ## iowait统计原理 25 | 26 | 既然我们知道了 `iowait` 的含义,那么接下来看看 Linux 是怎么统计 `iowait` 的比率的。 27 | 28 | Linux 会把 `iowait` 占用的时间输出到 `/proc/stat` 文件中,我们可以通过一下命令来获取到 `iowait` 占用的时间: 29 | 30 | ```bash 31 | cat /proc/stat 32 | ``` 33 | 34 | 命令输出如下图所示: 35 | 36 | ![stat](./images/stat.png) 37 | 38 | 红色方框中的数据就是 `iowait` 占用的时间。 39 | 40 | 我们可以每隔一段时间读取一次 `/proc/stat` 文件,然后把两次获取到的 `iowait` 时间进行相减,得到的结果是这段时间内,CPU处于 `iowait` 状态的时间。接着再将其除以总时间,得到 `iowait` 占用总时间的比率。 41 | 42 | 现在我们来看看 `/proc/stat` 文件是怎样获取 `iowait` 的时间的。 43 | 44 | 在内核中,每个 CPU 都有一个 `cpu_usage_stat` 结构,主要用于统计 CPU 一些信息,其定义如下: 45 | 46 | ```c 47 | struct cpu_usage_stat { 48 | cputime64_t user; 49 | cputime64_t nice; 50 | cputime64_t system; 51 | cputime64_t softirq; 52 | cputime64_t irq; 53 | cputime64_t idle; 54 | cputime64_t iowait; 55 | cputime64_t steal; 56 | cputime64_t guest; 57 | cputime64_t guest_nice; 58 | }; 59 | ``` 60 | 61 | `cpu_usage_stat` 结构的 `iowait` 字段记录了 CPU 处于 `iowait` 状态的时间。 62 | 63 | 所以要获取系统处于 `iowait` 状态的总时间,只需要将所有 CPU 的 `iowait` 时间相加即可,代码如下(位于源文件 `fs/proc/stat.c`): 64 | 65 | ```c 66 | static int show_stat(struct seq_file *p, void *v) 67 | { 68 | u64 iowait; 69 | ... 70 | // 1. 遍历系统中的所有CPU 71 | for_each_possible_cpu(i) { 72 | ... 73 | // 2. 获取CPU对应的iowait时间,并相加 74 | iowait = cputime64_add(iowait, kstat_cpu(i).cpustat.iowait); 75 | ... 76 | } 77 | ... 78 | return 0; 79 | } 80 | ``` 81 | 82 | `show_stat()` 函数首先会遍历所有 CPU,然后读取其 `iowait` 时间,并且将它们相加。 83 | 84 | ## 增加iowait时间 85 | 86 | 从上面的分析可知,每个 CPU 都有一个用于统计 `iowait` 时间的计数器,那么什么时候会增加这个计数器呢? 87 | 88 | 答案是:`系统时钟中断`。 89 | 90 | 在 `系统时钟中断` 中,会调用 `account_process_tick()` 函数来更新 CPU 的时间,代码如下: 91 | 92 | ```c 93 | void account_process_tick(struct task_struct *p, int user_tick) 94 | { 95 | cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy); 96 | struct rq *rq = this_rq(); 97 | 98 | // 1. 如果当前进程处于用户态,那么增加用户态的CPU时间 99 | if (user_tick) { 100 | account_user_time(p, cputime_one_jiffy, one_jiffy_scaled); 101 | } 102 | // 2. 如果前进程处于内核态,并且不是idle进程,那么增加内核态CPU时间 103 | else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET)) { 104 | account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy, 105 | one_jiffy_scaled); 106 | } 107 | // 3. 如果当前进程是idle进程,那么调用account_idle_time()函数进行处理 108 | else { 109 | account_idle_time(cputime_one_jiffy); 110 | } 111 | } 112 | ``` 113 | 114 | 我们主要关注当前进程是 `idle` 进程的情况,这是内核会调用 `account_idle_time()` 函数进行处理,其代码如下: 115 | 116 | ```c 117 | void account_idle_time(cputime_t cputime) 118 | { 119 | struct cpu_usage_stat *cpustat = &kstat_this_cpu.cpustat; 120 | cputime64_t cputime64 = cputime_to_cputime64(cputime); 121 | struct rq *rq = this_rq(); 122 | 123 | // 1. 如果当前有进程在等待IO请求的话,那么增加iowait的时间 124 | if (atomic_read(&rq->nr_iowait) > 0) { 125 | cpustat->iowait = cputime64_add(cpustat->iowait, cputime64); 126 | } 127 | // 2. 否则增加idle的时间 128 | else { 129 | cpustat->idle = cputime64_add(cpustat->idle, cputime64); 130 | } 131 | } 132 | ``` 133 | 134 | `account_idle_time()` 函数的逻辑比较简单,主要分以下两种情况进行处理: 135 | 136 | 1. 如果当前有进程在等待 I/O 请求的话,那么增加 `iowait` 的时间。 137 | 2. 如果当前没有进程在等待 I/O 请求的话,那么增加 `idle` 的时间。 138 | 139 | 所以,从上面的分析可知,要增加 `iowait` 的时间需要满足以下两个条件: 140 | 141 | 1. 当前进程是 `idle` 进程,也就是说 CPU 处于空闲状态。 142 | 2. 有进程在等待 I/O 请求完成。 143 | 144 | 进一步说,当 CPU 处于 `iowait` 状态时,说明 CPU 处于空闲状态,并且系统中有进程因为等待 I/O 请求而阻塞,也说明了 CPU 的利用率不够充分。 145 | 146 | 这时,我们可以使用异步 I/O(如 `iouring`)来优化程序,使得进程不会被 I/O 请求阻塞。 147 | 148 | -------------------------------------------------------------------------------- /kernel-timer.md: -------------------------------------------------------------------------------- 1 | # Linux定时器实现 2 | 3 | 一般定时器实现的方式有以下几种: 4 | 5 | #### 基于排序链表方式: 6 | 通过排序链表来保存定时器,由于链表是排序好的,所以获取最小(最早到期)的定时器的时间复杂度为 `O(1)`。但插入需要遍历整个链表,所以时间复杂度为 `O(n)`。如下图: 7 | 8 | ![timer-list](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/timer-list.jpg) 9 | 10 | #### 基于最小堆方式: 11 | 通过最小堆来保存定时器,在最小堆中获取最小定时器的时间复杂度为 `O(1)`,但插入一个定时器的时间复杂度为 `O(log n)`。如下图: 12 | 13 | ![timer-heap](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/timer-heap.jpg) 14 | 15 | #### 基于平衡二叉树方式: 16 | 使用平衡二叉树(如红黑树)保存定时器,在平衡二叉树中获取最小定时器的时间复杂度为 `O(log n)`(也可以通过缓存最小值的方法来达到 `O(1)`),而插入一个定时器的时间复杂度为 `O(log n)`。如下图: 17 | 18 | ![timer-tree](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/timer-tree.jpg) 19 | 20 | ### 时间轮: 21 | 但对于Linux这种对定时器依赖性比较高(网络子模块的TCP协议使用了大量的定时器)的操作系统来说,以上的数据结构都是不能满足要求的。所以Linux使用了效率更高的定时器算法:__时间轮__。 22 | 23 | __时间轮__ 类似于日常生活的时钟,如下图: 24 | 25 | ![timer](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/timer.jpg) 26 | 27 | 日常生活的时钟,每当秒针转一圈时,分针就会走一格,而分针走一圈时,时针就会走一格。而时间轮的实现方式与时钟类似,就是把到期时间当成一个轮,然后把定时器挂在这个轮子上面,每当时间走一秒就移动时针,并且执行那个时针上的定时器,如下图: 28 | 29 | ![timer-wheel](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/timer-Wheel.jpg) 30 | 31 | 一般的定时器范围为一个32位整型的大小,也就是 `0 ~ 4294967295`,如果通过一个数组来存储的话,就需要一个元素个数为4294967296的数组,非常浪费内存。这个时候就可以通过类似于时钟的方式:通过多级数组来存储。时钟通过时分秒来进行分级,当然我们也可以这样,但对于计算机来说,时分秒的分级不太友好,所以Linux内核中,对32位整型分为5个级别,第一个等级存储`0 ~ 255秒` 的定时器,第二个等级为 `256秒 ~ 256*64秒`,第三个等级为 `256*64秒 ~ 256*64*64秒`,第四个等级为 `256*64*64秒 ~ 256*64*64*64秒`,第五个等级为 `256*64*64*64秒 ~ 256*64*64*64*64秒`。如下图: 32 | 33 | ![timer-vts](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/timer-vts.jpg) 34 | 35 | > 注意:第二级至第五级数组的第一个槽是不挂任何定时器的。 36 | 37 | 每级数组上面都有一个指针,指向当前要执行的定时器。每当时间走一秒,Linux首先会移动第一级的指针,然后执行当前位置上的定时器。当指针变为0时,会移动下一级的指针,并把该位置上的定时器重新计算一次并且插入到时间轮中,其他级如此类推。如下图所示: 38 | 39 | ![timer-vts-pointer](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/timer-vts-pointer.jpg) 40 | 41 | 当要执行到期的定时器只需要移动第一级数组上的指针并且执行该位置上的定时器列表即可,所以时间复杂度为 `O(1)`,而插入一个定时器也很简单,先计算定时器的过期时间范围在哪一级数组上,并且连接到该位置上的链表即可,时间复杂度也是 `O(1)`。 42 | 43 | ## Linux时间轮的实现 44 | 那么接下来我们看看Linux内核是怎么实现时间轮算法的。 45 | 46 | #### 定义五个等级的数组 47 | ```c 48 | #define TVN_BITS 6 49 | #define TVR_BITS 8 50 | #define TVN_SIZE (1 << TVN_BITS) // 64 51 | #define TVR_SIZE (1 << TVR_BITS) // 256 52 | #define TVN_MASK (TVN_SIZE - 1) 53 | #define TVR_MASK (TVR_SIZE - 1) 54 | 55 | struct timer_vec { 56 | int index; 57 | struct list_head vec[TVN_SIZE]; 58 | }; 59 | 60 | struct timer_vec_root { 61 | int index; 62 | struct list_head vec[TVR_SIZE]; 63 | }; 64 | 65 | static struct timer_vec tv5; 66 | static struct timer_vec tv4; 67 | static struct timer_vec tv3; 68 | static struct timer_vec tv2; 69 | static struct timer_vec_root tv1; 70 | 71 | void init_timervecs (void) 72 | { 73 | int i; 74 | 75 | for (i = 0; i < TVN_SIZE; i++) { 76 | INIT_LIST_HEAD(tv5.vec + i); 77 | INIT_LIST_HEAD(tv4.vec + i); 78 | INIT_LIST_HEAD(tv3.vec + i); 79 | INIT_LIST_HEAD(tv2.vec + i); 80 | } 81 | for (i = 0; i < TVR_SIZE; i++) 82 | INIT_LIST_HEAD(tv1.vec + i); 83 | } 84 | ``` 85 | 上面的代码定义第一级数组为 `timer_vec_root` 类型,其 `index` 成员是当前要执行的定时器指针(对应 `vec` 成员的下标),而 `vec` 成员是一个链表数组,数组元素个数为256,每个元素上保存了该秒到期的定时器列表,其他等级的数组类似。 86 | 87 | #### 插入定时器 88 | ```c 89 | static inline void internal_add_timer(struct timer_list *timer) 90 | { 91 | /* 92 | * must be cli-ed when calling this 93 | */ 94 | unsigned long expires = timer->expires; 95 | unsigned long idx = expires - timer_jiffies; 96 | struct list_head * vec; 97 | 98 | if (idx < TVR_SIZE) { // 0 ~ 255 99 | int i = expires & TVR_MASK; 100 | vec = tv1.vec + i; 101 | } else if (idx < 1 << (TVR_BITS + TVN_BITS)) { // 256 ~ 16191 102 | int i = (expires >> TVR_BITS) & TVN_MASK; 103 | vec = tv2.vec + i; 104 | } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) { 105 | int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK; 106 | vec = tv3.vec + i; 107 | } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) { 108 | int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; 109 | vec = tv4.vec + i; 110 | } else if ((signed long) idx < 0) { 111 | /* can happen if you add a timer with expires == jiffies, 112 | * or you set a timer to go off in the past 113 | */ 114 | vec = tv1.vec + tv1.index; 115 | } else if (idx <= 0xffffffffUL) { 116 | int i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; 117 | vec = tv5.vec + i; 118 | } else { 119 | /* Can only get here on architectures with 64-bit jiffies */ 120 | INIT_LIST_HEAD(&timer->list); 121 | return; 122 | } 123 | /* 124 | * 添加到链表中 125 | */ 126 | list_add(&timer->list, vec->prev); 127 | } 128 | ``` 129 | `internal_add_timer()` 函数的主要工作是计算定时器到期时间所属的等级范围,然后把定时器添加到链表中。 130 | 131 | #### 执行到期的定时器 132 | ```c 133 | static inline void cascade_timers(struct timer_vec *tv) 134 | { 135 | /* cascade all the timers from tv up one level */ 136 | struct list_head *head, *curr, *next; 137 | 138 | head = tv->vec + tv->index; 139 | curr = head->next; 140 | /* 141 | * We are removing _all_ timers from the list, so we don't have to 142 | * detach them individually, just clear the list afterwards. 143 | */ 144 | while (curr != head) { 145 | struct timer_list *tmp; 146 | 147 | tmp = list_entry(curr, struct timer_list, list); 148 | next = curr->next; 149 | list_del(curr); 150 | internal_add_timer(tmp); 151 | curr = next; 152 | } 153 | INIT_LIST_HEAD(head); 154 | tv->index = (tv->index + 1) & TVN_MASK; 155 | } 156 | 157 | static inline void run_timer_list(void) 158 | { 159 | spin_lock_irq(&timerlist_lock); 160 | while ((long)(jiffies - timer_jiffies) >= 0) { 161 | struct list_head *head, *curr; 162 | if (!tv1.index) { // 完成了一个轮回, 移动下一个单位的定时器 163 | int n = 1; 164 | do { 165 | cascade_timers(tvecs[n]); 166 | } while (tvecs[n]->index == 1 && ++n < NOOF_TVECS); 167 | } 168 | repeat: 169 | head = tv1.vec + tv1.index; 170 | curr = head->next; 171 | if (curr != head) { 172 | struct timer_list *timer; 173 | void (*fn)(unsigned long); 174 | unsigned long data; 175 | 176 | timer = list_entry(curr, struct timer_list, list); 177 | fn = timer->function; 178 | data= timer->data; 179 | 180 | detach_timer(timer); 181 | timer->list.next = timer->list.prev = NULL; 182 | timer_enter(timer); 183 | spin_unlock_irq(&timerlist_lock); 184 | fn(data); 185 | spin_lock_irq(&timerlist_lock); 186 | timer_exit(); 187 | goto repeat; 188 | } 189 | ++timer_jiffies; 190 | tv1.index = (tv1.index + 1) & TVR_MASK; 191 | } 192 | spin_unlock_irq(&timerlist_lock); 193 | } 194 | ``` 195 | 执行到期的定时器主要通过 `run_timer_list()` 函数完成,该函数首先比较当前时间与最后一次运行 `run_timer_list()` 函数时间的差值,然后循环这个差值的次数,并执行当前指针位置上的定时器。每循环一次对第一级数组指针进行加一操作,当第一级数组指针变为0(即所有定时器都执行完),那么就移动下一个等级的指针,并把该位置上的定时器重新计算插入到时间轮中,重新计算定时器通过 `cascade_timers()` 函数实现。 196 | -------------------------------------------------------------------------------- /lvs-principle-and-source-analysis-part1.md: -------------------------------------------------------------------------------- 1 | # LVS原理与实现 - 原理篇 2 | 3 | `LVS`,全称 Linux Virtual Server,是章文嵩博士发起的一个开源项目。在社区具有很大的热度,是一个基于四层、性能极高的反向代理服务器。至于什么是反向代理,这里就不作详细介绍了,如果不了解可以先去阅读反向代理相关的资料。 4 | 5 | ## LVS工作原理 6 | 7 | 下面先介绍一下 LVS 的工作原理。 8 | 9 | LVS的工作模式分为三种:`NAT模式(网络地址转换)`、`DR模式(直接路由)` 和 `TUN模式(IP隧道)`。 10 | 11 | 下面将详细介绍每种工作模式的运行原理。 12 | 13 | > 名词解析: 14 | > 15 | > 1. Director服务器:直接接收用户请求的服务器,是LVS的入口。 16 | > 2. Real-Server服务器:真实服务器,用于处理用户请求的服务器。 17 | > 3. 虚拟IP(VIP):对外网暴露的 IP 地址,客户端可以通过 VIP 访问 LVS 集群。 18 | 19 | ### 1. NAT模式 20 | 21 | `NAT模式` 的运行方式如下图: 22 | 23 | ![NAT-ARCH](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/nat-arch.jpg) 24 | 25 | __请求过程说明:__ 26 | 27 | * `client` 发送请求到 `LVS` 的 `VIP` 上,`Director` 服务器首先根据 `client` 的 IP 和端口从连接信息表中查询是否已经存在,如果存在就直接使用当前连接进行处理。否则根据负载算法选择一个 `Real-Server`(真正提供服务的服务器),并记录连接到连接信息表中,然后把 `client` 请求的目的 IP 地址修改为 `Real-Server` 的地址,将请求发给 `Real-Server`。 28 | 29 | * `Real-Server` 服务器收到请求包后,发现目的 IP 是自己的 IP,于是处理请求,然后发送回复给 `Director` 服务器。 30 | 31 | * `Director` 服务器收到回复包后,修改回复包的的源地址为VIP,发送给 `client`。 32 | 33 | > 上图中的蓝色连接线表示请求的数据流向,而红色连接线表示回复的数据流向。由于进出流量都需要经过 `Director` 服务器,所以 `Director` 服务器可能会成功瓶颈。 34 | 35 | 下面通过一幅图来说明一个请求数据包在 LVS 服务器中的地址变化情况: 36 | 37 | ![NAT-PACKAGE](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/nat-package.jpg) 38 | 39 | 下面解释一下请求数据包的地址变化过程: 40 | 41 | * client 向 LVS 集群发起请求,源IP地址和源端口为:`192.168.11.100:11021`,而目标IP地址和端口为:`192.168.10.10:80`。当 `Director` 服务器接收到 client 的请求后,会根据调度算法选择一台合适的 `Real-Server` 服务器,并且把请求数据包的目标IP地址和端口改为 `Real-Server` 服务器的IP地址和端口,并记录连接信息到连接信息表中,如上图选择的 `Real-Server` 服务器的IP地址和端口为:`192.168.1.2:80`。 42 | 43 | * 当 `Real-Server` 服务器接收到请求后,对请求进行处理,处理完后会把数据包的源IP地址和端口跟目标IP地址和端口交互,然后发送给网关 `192.168.1.1`(也就是 `Director` 服务器)。 44 | 45 | * `Director` 服务器接收到来自 `Real-Server` 服务器的回复数据,然后根据连接信息把源IP地址更改为虚拟IP地址。 46 | 47 | > 由于 `Real-Server` 服务器需要把 `Director` 服务器设置为网关,所以 `Director` 服务器与 `Real-Server` 服务器需要部署在同一个网络下。 48 | 49 | ### 2. DR模式 50 | 51 | `DR模式` 的运行方式如下图: 52 | 53 | ![DR-ARCH](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/dr-arch.jpg) 54 | 55 | __请求过程说明:__ 56 | 57 | * `client` 发送请求到 `LVS` 的 `VIP` 上,`Director` 服务器首先根据 client 的 IP 和端口从连接信息表中查询是否已经存在,如果存在就直接使用当前连接进行处理。否则根据负载算法选择一个 `Real-Server`(真正提供服务的服务器),并记录连接到连接信息表中,然后通过修改请求数据包的目标 MAC 地址为 `Real-Server` 服务器的 MAC 地址(注意:IP地址不修改),并通过局域网把数据包发送出去。 58 | 59 | * 由于 `Director` 服务器与 `Real-Server` 服务器在同一局域网中,所以通过数据包的目标 MAC 地址可以找到对应的 `Real-Server` 服务器(以太网协议),而 `Real-Server` 服务器接收到数据包后,会对数据包进行处理。 60 | 61 | * `Real-Server` 服务器处理完请求后,把处理结果直接发送给 `client`,而不会通过 `Director` 服务器。 62 | 63 | > 注意:`Real-Server` 服务器必须设置回环设备的 IP 地址为 VIP 地址,因为如果不设置 VIP,那么 `Real-Server` 服务器会认为这个数据包发送给本机的,从而丢弃这个数据包。 64 | 65 | 下面通过一幅图来说明一个请求数据包在 LVS 服务器中的地址变化情况: 66 | 67 | ![DR-PACKAGE](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/dr-package.jpg) 68 | 69 | 下面解释一下请求数据包的地址变化过程: 70 | 71 | * client 向 LVS 集群发起请求,源IP地址和源端口为:`192.168.11.100:11021`,而目标IP地址和端口为:`192.168.10.10:80`。当 `Director` 服务器接收到 client 的请求后,会根据调度算法选择一台合适的 `Real-Server` 服务器,并且把请求数据包的目标 MAC 地址改为 `Real-Server` 服务器的 MAC 地址,并记录连接信息到连接信息表中,然后通过局域网把数据包发送出去。 72 | 73 | * 当 `Real-Server` 服务器处理完请求后,会把处理结果直接发送给 `client`。如果 `Real-Server` 服务器与 `client` 在同一个局域网,那么直接通过把目标 MAC 地址修改为 `client` 的 MAC 地址。否则,通过把目标 MAC 地址修改为路由器的 MAC 地址,然后通过路由器发送出去。 74 | 75 | > 从上图可以看出,整个请求过程中,数据包只有 MAC 地址发生变化。 76 | > 另外,由于 `DR模式` 只有入口需要经过 `Director` 服务器,而出口不需要经过 `Director` 服务器,所以性能比 `NAT模式` 要高。 77 | 78 | ### 3. TUN模式 79 | 80 | `TUN模式` 比较复杂一些,并且国内使用得比较少,所以这里就不作介绍,有兴趣自己查阅相关资料。 81 | 82 | ## 调度算法 83 | 84 | 上面介绍了 LVS 的工作模式,下面介绍一下 LVS 的调度算法。 85 | 86 | 由于 LVS 需要选择合适的 `Real-Server(RS)` 服务器处理请求,所以需要根据不同的需求选择不同的调度算法来选择 `Real-Server` 服务器。LVS 的调度算法主要有以下几种: 87 | 88 | * __1. 轮询调度(Round-Robin,RR)__ 89 | 90 | 最简单的调度算法,按照顺序将请求依次转发给后端的RS。大部分情况下,RS的性能状态都是各不一致的,这种算法显然无法满足合理利用资源的要求。 91 | 92 | * __2. 带权重的轮询调度(Weighted Round-Robin,WRR)__ 93 | 94 | 在轮询算法的基础上加上权重设置,权重越高的RS被分配到的请求越多。适用于按照服务器性能高低,配置不同的权重,以达到合理的资源利用。 95 | 96 | * __3. 最小连接调度(Least-Connection, LC)__ 97 | 98 | 把新的请求分配给连接数最少的RS。连接数少说明服务器空闲。 99 | 100 | * __4. 带权重的最小连接调度(Weight Least-Connection, WLC)__ 101 | 102 | 在最小连接算法的基础上加上权重设置,这样可以人为地控制请求分配。 103 | 104 | * __5. 基于局部性的最小连接调度(Locality-Based Least Connection, LBLC)__ 105 | 106 | 针对请求报文目标IP地址的负载均衡调度。目前主要用于Cache集群系统,因为在Cache集群中客户请求报文的目标IP地址是变化的。 107 | 108 | 算法的设计目标是在服务器的负载基本平衡情况下,将相同目标IP地址的请求调度到同一台服务器,来提高各台服务器的访问局部性和主存Cache命中率,提升整个集群系统的处理能力。 109 | 110 | LBLC调度算法先根据请求的目标IP地址找出该目标IP地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器;若服务器不存在,或者该服务器超载且有服务器处于其一半的工作负载,则用“最小连接”的原则选出一个可用的服务器,将请求发送到该服务器。 111 | 112 | * __6. 带复制的基于局部性最小连接调度(Locality-Based Least Connections with Replication, LBLCR)__ 113 | 114 | 也是针对请求报文目标IP地址的负载均衡调度,与LBLC算法不同之处:LBLC维护一个目标IP到一台服务器的映射,而LBLCR则需要维护一个目标IP到一组服务器的映射。 115 | 116 | LBLCR调度算法先根据请求的目标IP地址找到对应的服务器组,按“最小连接”原则从该服务器组中选出一台服务器,若服务器没有超载,则将请求发送到该服务器;若服务器超载,则按“最小连接”原则从整个集群中选出一台服务器,将该服务器加入到服务组中,将请求发送给这台服务器。同时,当该服务器组有一段时间没有被修改,将最忙的服务器从服务器组中删除,以降低复制的程度。 117 | 118 | * __7. 目标地址散列调度(Destination Hashing, DH)__ 119 | 120 | 也是针对请求报文目标IP地址的负载均衡调度,但它是一种静态映射算法,通过一个散列(Hash)函数将一个目标IP地址映射到一台服务器。DH算法先根据请求的目标IP地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且为超载,将请求发送到该服务器,否则返回空。 121 | 122 | * __8. 源地址散列调度(Source Hashing, SH)__ 123 | 124 | 该算法正好与DH调度算法相反,它根据请求的源IP地址,作为散列键从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。算法流程与目标地址散列调度算法基本相似,只不过将请求的目标IP地址换成请求的源IP地址。 125 | 126 | ## 总结 127 | 128 | 本文主要简单的介绍了 LVS 的运行原理与调度算法,更多相关的资料可以查阅参考链接,而 LVS 的实现部分将会在另外一篇文章介绍。 129 | 130 | 参考链接: 131 | 132 | [http://www.linuxvirtualserver.org/Documents.html](http://www.linuxvirtualserver.org/Documents.html) 133 | 134 | [http://www.linuxvirtualserver.org/VS-NAT.html](http://www.linuxvirtualserver.org/VS-NAT.html) 135 | 136 | [http://www.linuxvirtualserver.org/VS-DRouting.html](http://www.linuxvirtualserver.org/VS-DRouting.html) 137 | 138 | [http://www.linuxvirtualserver.org/VS-IPTunneling.html](http://www.linuxvirtualserver.org/VS-IPTunneling.html) 139 | 140 | [https://blog.51cto.com/blief/1745134](https://blog.51cto.com/blief/1745134) 141 | -------------------------------------------------------------------------------- /memory_mmap.md: -------------------------------------------------------------------------------- 1 | ## Linux mmap完全剖析 2 | 3 | ### `mmap()` 系统调用介绍 4 | `mmap()` 系统调用能够将文件映射到内存空间,然后可以通过读写内存来读写文件。我们先来看看 `mmap()` 系统调用的用法吧,`mmap()` 函数的原型如下: 5 | 6 | ```cpp 7 | void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); 8 | ``` 9 | 10 | 参数说明: 11 | * `start`:指定要映射的内存地址,一般设置为 `NULL` 让操作系统自动选择合适的内存地址。 12 | * `length`:映射地址空间的字节数,它从被映射文件开头 `offset` 个字节开始算起。 13 | * `prot`:指定共享内存的访问权限。可取如下几个值的或:`PROT_READ`(可读), `PROT_WRITE`(可写), `PROT_EXEC`(可执行), `PROT_NONE`(不可访问)。 14 | * `flags`:由以下几个常值指定:`MAP_SHARED `(共享的) `MAP_PRIVATE`(私有的), `MAP_FIXED`(表示必须使用 `start` 参数作为开始地址,如果失败不进行修正),其中,`MAP_SHARED` , `MAP_PRIVATE`必选其一,而 `MAP_FIXED` 则不推荐使用。 15 | * `fd`:表示要映射的文件句柄。 16 | * `offset`:表示映射文件的偏移量,一般设置为 `0` 表示从文件头部开始映射。 17 | 18 | 函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。 19 | 20 | 下面通过一个例子来说明 `mmap()` 系统调用的用法: 21 | 22 | ```cpp 23 | int main() { 24 | ... 25 | fd = open(name, flag, mode); 26 | if (fd < 0) { 27 | // error process... 28 | exit(1); 29 | } 30 | 31 | addr = mmap(NULL, len, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED, fd, 0); 32 | if (addr < 0) { 33 | // error process... 34 | exit(1); 35 | } 36 | 37 | memset(addr, 0, len); 38 | ... 39 | exit(0); 40 | } 41 | ``` 42 | 43 | 上面的例子首先通过 `open()` 系统调用打开一个文件,然后通过调用 `mmap()` 把文件映射到内存空间,映射成功后就可以通过操作函数返回的内存地址来对文件进行读写操作。 44 | 45 | ### `mmap()` 底层原理 46 | 在分析 `mmap()` 系统调用原理之前,首先要知道操作系统的虚拟内存空间与物理内存空间的概念。 47 | 48 | 在 32位的 Linux 内核中,每个进程都独有 `4GB` 的虚拟内存空间,但所有进程却共用相同的物理内存空间。物理内存空间就是安装在电脑上的内存条,如果内存条只有 `1GB`,那么物理内存空间就只有 `1GB`。但虚拟内存空间是逻辑上的内存空间,虚拟内存空间必须映射到物理内存空间才能使用。 49 | 50 | 虚拟内存空间与物理内存空间映射关系如下: 51 | 52 | ![process-vm-mapping](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/process_vm.jpg) 53 | 54 | 映射是按内存页进行的,一个内存页为 `4KB` 大小。在上图中,`P1` 是进程1,`P2` 是进程2。进程1的虚拟内存页A映射到物理内存页A,进程2的虚拟内存页A映射到物理内存页B。进程1的虚拟内存页B和进程2的虚拟内存页B同时映射到物理内存页C,也就是说进程1和进程2共享了物理内存页C。 55 | 56 | #### `vm_area_struct` 结构 57 | 在Linux内核中,虚拟内存是用过结构体 `vm_area_struct` 来管理的,通过 `vm_area_struct` 结构体可以把虚拟内存划分为多个用途不相同的内存区,比如可以划分为数据段区、代码段区等等,`vm_area_struct` 结构体的定义如下: 58 | 59 | ```cpp 60 | struct vm_area_struct { 61 | struct mm_struct * vm_mm; /* The address space we belong to. */ 62 | unsigned long vm_start; /* Our start address within vm_mm. */ 63 | unsigned long vm_end; /* The first byte after our end address 64 | within vm_mm. */ 65 | 66 | /* linked list of VM areas per task, sorted by address */ 67 | struct vm_area_struct *vm_next; 68 | 69 | pgprot_t vm_page_prot; /* Access permissions of this VMA. */ 70 | unsigned long vm_flags; /* Flags, listed below. */ 71 | 72 | rb_node_t vm_rb; 73 | 74 | struct vm_area_struct *vm_next_share; 75 | struct vm_area_struct **vm_pprev_share; 76 | 77 | /* Function pointers to deal with this struct. */ 78 | struct vm_operations_struct * vm_ops; 79 | ... 80 | struct file * vm_file; /* File we map to (can be NULL). */ 81 | ... 82 | }; 83 | ``` 84 | 85 | `vm_area_struct` 结构各个字段作用: 86 | * `vm_mm`:指向进程内存空间管理对象。 87 | * `vm_start`:内存区的开始地址。 88 | * `vm_end`:内存区的结束地址。 89 | * `vm_next`:用于连接进程的所有内存区。 90 | * `vm_page_prot`:指定内存区的访问权限。 91 | * `vm_flags`:内存区的一些标志。 92 | * `vm_file`:指向映射的文件对象。 93 | * `vm_ops`:内存区的一些操作函数。 94 | 95 | `vm_area_struct` 结构与虚拟内存地址的关系如下图: 96 | 97 | ![vm_address](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/vm_address.png) 98 | 99 | 每个进程都由 `task_struct` 结构进行管理,`task_struct` 结构中的 `mm` 成员指向了每个进程的内存管理结构 `mm_struct`,而 `mm_struct` 结构的 `mmap` 成员记录了进程虚拟内存空间各个内存区的 `vm_area_struct` 结构链表。 100 | 101 | 当调用 `mmap()` 时,内核会创建一个 `vm_area_struct` 结构,并且把 `vm_start` 和 `vm_end` 指向虚拟内存空间的某个内存区,并且把 `vm_file` 字段指向映射的文件对象。然后调用文件对象的 `mmap` 接口来对 `vm_area_struct` 结构的 `vm_ops` 成员进行初始化,如 `ext2` 文件系统的文件对象会调用 `generic_file_mmap()` 函数进行初始化,代码如下: 102 | 103 | ```cpp 104 | static struct vm_operations_struct generic_file_vm_ops = { 105 | nopage: filemap_nopage, 106 | }; 107 | 108 | int generic_file_mmap(struct file * file, struct vm_area_struct * vma) 109 | { 110 | struct address_space *mapping = file->f_dentry->d_inode->i_mapping; 111 | struct inode *inode = mapping->host; 112 | 113 | if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE)) { 114 | if (!mapping->a_ops->writepage) 115 | return -EINVAL; 116 | } 117 | if (!mapping->a_ops->readpage) 118 | return -ENOEXEC; 119 | UPDATE_ATIME(inode); 120 | vma->vm_ops = &generic_file_vm_ops; 121 | return 0; 122 | } 123 | ``` 124 | 125 | `vm_operations_struct` 结构的 `nopage` 接口会在访问内存发生异常时被调用,上面指向的是 `filemap_nopage()` 函数,`filemap_nopage()` 函数的主要工作是: 126 | 1. 把映射到虚拟内存区的文件内容读入到物理内存页中。 127 | 2. 对访问发生异常的虚拟内存地址与物理内存地址进行映射。 128 | 129 | 处理过程如下图: 130 | 131 | ![vma-pma-maping](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/vma-pma-maping.png) 132 | 133 | 如上图所示,`虚拟内存页m` 映射到 `物理内存页x`,并且把映射的文件的内容读入到物理内存中,这样就把内存与文件的映射关系建立起来,对映射的内存区进行读写操作实际上就是对文件的读写操作。 134 | 135 | 一般来说,对映射的内存空间进行读写并不会实时写入到文件中,所以要对内存与文件进行同步时需要调用 `msync()` 函数来实现。 136 | 137 | #### 对文件的读写 138 | 像 `read()/write()` 这些系统调用,首先需要进行内核空间,然后把文件内容读入到缓存中,然后再对缓存进行读写操作,最后由内核定时同步到文件中。过程如下图: 139 | 140 | ![read-write-system-call](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/read-write-system-call.png) 141 | 142 | 而调用 `mmap()` 系统调用对文件进行映射后,用户对映射后的内存进行读写实际上是对文件缓存的读写,所以减少了一次系统调用,从而加速了对文件读写的效率。如下图: 143 | 144 | ![mmap-memory-address](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/mmap-memory-address.png) 145 | 146 | 147 | -------------------------------------------------------------------------------- /minix_file_system.md: -------------------------------------------------------------------------------- 1 | ## MINIX文件系统分析 2 | 3 | 硬盘是用来持久化存储数据的硬件,一般来说,硬盘分为磁盘和固态硬盘,但这里并不会介绍太多关于硬件方面的知识,我们主要详细介绍 `MINIX文件系统` 的原理和现实。 4 | 5 | ### 什么是文件系统 6 | 7 | 硬盘属于 `块设备` 硬件,也就是说对硬盘的操作都是通过块来进行的。对于一般的磁盘来说,一个块为512字节(称为扇区)。硬盘就是按照块大小来划分成多个块,每个块都有唯一的编号。 8 | 9 | 但是对于人类来说,通过块来操作硬盘是非常不方便的,譬如你不知道某个数据块是否已经被使用(当然也可以通过在数据块的第一个字节记录是否已经使用),而且你也不知道这个数据块保存了什么数据。所以这个时候就需要 `文件系统` 来帮助管理这些数据。 10 | 11 | 那么什么是 `文件系统` 呢? 12 | 13 | > 百科上的定义: `文件系统` 是操作系统用于明确磁盘或分区上的文件的方法和数据结构,即在磁盘上组织文件的方法。 14 | 15 | 前面说过,内存通过内存管理系统进行管理,所以硬盘也需要硬盘管理系统来管理,也就是 `文件系统`。`文件系统` 通过文件的方式来管理硬盘上的数据,并且提供给用户一些简单的API(如 `read()` 和 `write()` 等接口)来操作这些文件。 16 | 17 | ### 什么是MINIX文件系统 18 | 19 | `MINIX文件系统` 是从MINIX操作系统移植到Linux的,其优点是简单清晰,适合用于教学使用。但缺点是过于简单,不适合生产环境使用。不过通过对MINIX文件系统的分析,有助于理解其他复杂的文件系统。 20 | 21 | ### MINIX文件系统结构 22 | 23 | 在分析MINIX文件系统实现前,我们先来了解一下MINIX文件系统在硬盘的结构组织,如下图: 24 | 25 | ![minix_filesystem](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/minix_filesystem.png) 26 | 27 | 可以把硬盘当成一个由多个数据块组成的设备(对于MINIX文件系统一个数据块的大小为1024字节),MINIX文件系统就是组织和管理这些数据块的一种算法。下面来介绍一下上图中各个部分的作用: 28 | 29 | * `超级块`:用于记录文件系统的一些信息,比如inode的数量和inode位图占用的数据块数量等。 30 | * `inode位图`:用于记录inode的使用情况,每个位代表inode的使用情况,1表示已经使用,0代表空闲。 31 | * `逻辑块位图`:用于记录逻辑数据块的使用情况,每个位代表一个逻辑数据块的使用情况,1表示已经使用,0代表空闲。 32 | * `inode表`:用于记录所有inode的信息,每个记录代表一个inode。 33 | * `逻辑数据块`:真正保存数据的数据块。 34 | 35 | 从上面的介绍可以看到,MINIX文件系统的结构比较简单,接下来我们分析一下MINIX文件系统的实现。 36 | 37 | ### MINIX文件系统相关数据结构 38 | 39 | 在分析MINIX文件系统的实现前,先来介绍一下两个重要的数据结构:`minix_super_block` 和 `minix2_inode`,`minix_super_block` 对应MINIX文件系统的超级块,而 `minix_super_block` 则对应MINIX文件系统的inode节点。 40 | 41 | #### minix_super_block结构 42 | ```c 43 | struct minix_super_block { 44 | __u16 s_ninodes; // inode的个数 45 | __u16 s_nzones; // 逻辑数据块个数(v1版) 46 | __u16 s_imap_blocks; // inode位图占用的数据块数量 47 | __u16 s_zmap_blocks; // 数据块位图占用的数据块数量 48 | __u16 s_firstdatazone; // 第一个逻辑数据块起始号 49 | __u16 s_log_zone_size; // 使用2为底的对数表示的每个逻辑数据块包含的磁盘块数 50 | __u32 s_max_size; // 文件最大尺寸 51 | __u16 s_magic; // 魔数 52 | __u16 s_state; // 文件系统状态 53 | __u32 s_zones; // 逻辑数据块个数(v2版) 54 | }; 55 | ``` 56 | 57 | #### minix2_inode结构 58 | ```c 59 | struct minix2_inode { 60 | __u16 i_mode; // 文件模式 61 | __u16 i_nlinks; // 链接数 62 | __u16 i_uid; // 所属用户ID 63 | __u16 i_gid; // 所属组ID 64 | __u32 i_size; // 文件大小 65 | __u32 i_atime; // 访问时间 66 | __u32 i_mtime; // 修改时间 67 | __u32 i_ctime; // 创建时间 68 | __u32 i_zone[10]; // 文件数据存储的逻辑数据块编号 69 | }; 70 | ``` 71 | `minix2_inode` 的 `i_zone` 字段记录了文件数据存储在哪些逻辑数据块上,可以看到 `i_zone` 字段是一个有10个元素的数组,前7个元素是直接指向的数据块,就是数据会直接存储在这些数据块上。而第8个元素是一级间接指向,第9个元素是二级间接指向,第10个元素是三级间接指向,原理如下图: 72 | 73 | ![minix_filesystem_inode](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/minix_filesystem_inode.jpg) 74 | 75 | ### MINIX文件系统实现 76 | 77 | 要在一个硬盘或者分区安装文件系统,首先需要对硬盘或者分区进行格式化操作,Linux系统可以使用 `mkfs` 命令对硬盘或者分区进行格式化,例如可以使用下面命令对设备进行格式化: 78 | ```shell 79 | $ mkfs -t ext4 -b 4096 /dev/sdb5 80 | ``` 81 | 上面的命令把设备 `/dev/sdb5` 格式化为 `ext4` 文件系统,并且每个逻辑数据块大小为4k。格式化过程就是按照具体文件系统指定的结构来编排数据,比如格式化为MINIX文件系统,就需要写入超级块、inode位图、逻辑数据块位图、inode表等数据结构。 82 | 83 | 格式化完后可以通过 `mount` 命名将设备挂载到某个目录下,这样就可以访问这个设备了。如下: 84 | ```shell 85 | $ mount /dev/sdb5 /mnt/foo 86 | $ cd /mnt/foo 87 | ``` 88 | 挂载过程就是读入文件系统的超级块到内存,对于MINIX文件系统,读入超级块数据是通过 `minix_read_super()` 函数实现的,代码如下: 89 | ```c 90 | static struct super_block *minix_read_super( 91 | struct super_block *s, void *data, int silent) 92 | { 93 | struct minix_super_block *ms; 94 | 95 | ... 96 | if (!(bh = bread(dev,1,BLOCK_SIZE))) 97 | goto out_bad_sb; 98 | 99 | ms = (struct minix_super_block *) bh->b_data; 100 | ... 101 | 102 | /* 103 | * 申请inode位图和逻辑数据块位图的内存 104 | */ 105 | i = (s->u.minix_sb.s_imap_blocks + s->u.minix_sb.s_zmap_blocks) * sizeof(bh); 106 | map = kmalloc(i, GFP_KERNEL); 107 | if (!map) 108 | goto out_no_map; 109 | memset(map, 0, i); 110 | s->u.minix_sb.s_imap = &map[0]; 111 | s->u.minix_sb.s_zmap = &map[s->u.minix_sb.s_imap_blocks]; 112 | 113 | block=2; 114 | for (i=0 ; i < s->u.minix_sb.s_imap_blocks ; i++) { // 读取inode位图 115 | if (!(s->u.minix_sb.s_imap[i]=bread(dev,block,BLOCK_SIZE))) 116 | goto out_no_bitmap; 117 | block++; 118 | } 119 | 120 | for (i=0 ; i < s->u.minix_sb.s_zmap_blocks ; i++) { // 读取数据块位图 121 | if (!(s->u.minix_sb.s_zmap[i]=bread(dev,block,BLOCK_SIZE))) 122 | goto out_no_bitmap; 123 | block++; 124 | } 125 | 126 | // 设置第一inode和第一个逻辑数据块为已被使用(文件系统的根目录) 127 | minix_set_bit(0,s->u.minix_sb.s_imap[0]->b_data); 128 | minix_set_bit(0,s->u.minix_sb.s_zmap[0]->b_data); 129 | 130 | s->s_op = &minix_sops; 131 | root_inode = iget(s, MINIX_ROOT_INO); 132 | if (!root_inode) 133 | goto out_no_root; 134 | 135 | // 读取根目录inode节点 136 | s->s_root = d_alloc_root(root_inode); 137 | if (!s->s_root) 138 | goto out_iput; 139 | 140 | s->s_root->d_op = &minix_dentry_operations; 141 | 142 | ... 143 | return s; 144 | ... 145 | } 146 | ``` 147 | `minix_read_super()` 函数首先通过 `bread()` 读取设备的第2个数据块(第1个数据块是引导区),超级块就保存在这个数据块中。然后根据MINIX文件系统的超级块数据设置虚拟文件系统(VFS)的超级块,接着为inode位图和逻辑数据块位图申请内存,并且读入设备中的inode位图和逻辑数据块位图到内存中。最后读取根目录的inode节点数据,并保存到虚拟文件系统超级块的 `s_root` 字段中。 148 | 149 | #### 读取文件数据 150 | 读取MINIX文件系统的文件过程如下图: 151 | 152 | ![minix-filesystem-read](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/minix-filesystem-read.jpg) 153 | 154 | 读文件时,首先需要打开文件,然后通过调用 `read()` 系统调用来读取文件中的内容。`read()` 系统调用原型如下: 155 | ```cpp 156 | ssize_t read(int fd, void *buf, size_t count); 157 | ``` 158 | 参数的意义: 159 | 1. `fd`: 打开的文件句柄。 160 | 2. `buf`: 用于存放读取内容的内存地址。 161 | 3. `count`: 需要从文件中读取多少字节的数据。 162 | 163 | `read()` 系统调用会触发调用 `虚拟文件系统层(VFS)` 的 `sys_read()` 函数,而对于 MINIX 文件系统,`sys_read()` 函数会接着调用 `generic_file_read()` 函数,`generic_file_read()` 函数又接着调用 `do_generic_file_read()` 函数。 164 | 165 | `do_generic_file_read()` 函数的实现比较复杂,首先会去缓存中查找要读取的内容是否已经存在,如果存在直接返回缓存的数据即可。如果还没有缓存,那么调用 `minix_readpage()` 从磁盘中读取文件的内容到缓存中,最后调用 `file_read_actor()` 函数把数据复制都用户空间的 `buf` 参数中。 166 | -------------------------------------------------------------------------------- /multiplexing-io.md: -------------------------------------------------------------------------------- 1 | ## 什么是多路复用IO 2 | `多路复用IO (IO multiplexing)` 是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。在Linux系统中,常用的 `多路复用IO` 手段有 `select`、`poll` 和 `epoll`。 3 | 4 | `多路复用IO` 主要用于处理网络请求,例如可以把多个请求句柄添加到 `select` 中进行监听,当有请求可进行IO的时候就会告知进程,并且把就绪的请求句柄保存下来,进程只需要对这些就绪的请求进行IO操作即可。下面通过一幅图来展示 `select` 的使用方式(图片来源于网络): 5 | 6 | ![select-model](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/select-model.png) 7 | 8 | ## 多路复用IO实现原理 9 | 为了更简明的解释 `多路复用IO` 的原理,这里使用 `select` 系统调用作为分析对象。因为 `select` 的实现比较简单,而现在流行的 `epoll` 由于处于性能考虑,实现则比较复杂,不便于理解 `多路复用IO` 的原理,当然当理解了 `select` 的实现原理后,对 `epoll` 的实现就能应刃而解了。 10 | 11 | ### select系统调用的使用 12 | 要使用 `select` 来监听socket是否可以进行IO,首先需要把其添加到一个类型为 `fd_set` 的结构中,然后通过调用 `select()` 系统调用来进行监听,下面代码介绍了怎么使用 `select` 来对socket进行监听的: 13 | ```cpp 14 | int socket_can_read(int fd) 15 | { 16 | int retval; 17 | fd_set set; 18 | struct timeval tv; 19 | 20 | FD_ZERO(&set); 21 | FD_SET(fd, &set); 22 | 23 | tv.tv_sec = tv.tv_usec = 0; 24 | 25 | retval = select(fd+1, &set, NULL, NULL, &tv); 26 | if (retval < 0) { 27 | return -1; 28 | } 29 | 30 | return FD_ISSET(fd, &set) ? 1 : 0; 31 | } 32 | ``` 33 | 通过上面的函数,可以监听一个socket句柄是否可读。 34 | 35 | ### select系统调用的实现 36 | 接下来我们分析一下 `select` 系统调用的实现,用户程序通过调用 `select` 系统调用后会进入到内核态并且调用 `sys_select()` 函数,`sys_select()` 函数的实现如下: 37 | ```cpp 38 | asmlinkage long 39 | sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp) 40 | { 41 | fd_set_bits fds; 42 | char *bits; 43 | long timeout; 44 | int ret, size; 45 | 46 | timeout = MAX_SCHEDULE_TIMEOUT; 47 | if (tvp) { 48 | time_t sec, usec; 49 | ... 50 | if ((unsigned long) sec < MAX_SELECT_SECONDS) { 51 | timeout = ROUND_UP(usec, 1000000/HZ); 52 | timeout += sec * (unsigned long) HZ; 53 | } 54 | } 55 | 56 | if (n > current->files->max_fdset) 57 | n = current->files->max_fdset; 58 | 59 | ret = -ENOMEM; 60 | size = FDS_BYTES(n); 61 | bits = select_bits_alloc(size); 62 | 63 | fds.in = (unsigned long *)bits; 64 | fds.out = (unsigned long *)(bits + size); 65 | fds.ex = (unsigned long *)(bits + 2*size); 66 | fds.res_in = (unsigned long *)(bits + 3*size); 67 | fds.res_out = (unsigned long *)(bits + 4*size); 68 | fds.res_ex = (unsigned long *)(bits + 5*size); 69 | 70 | if ((ret = get_fd_set(n, inp, fds.in)) || 71 | (ret = get_fd_set(n, outp, fds.out)) || 72 | (ret = get_fd_set(n, exp, fds.ex))) 73 | goto out; 74 | zero_fd_set(n, fds.res_in); 75 | zero_fd_set(n, fds.res_out); 76 | zero_fd_set(n, fds.res_ex); 77 | 78 | ret = do_select(n, &fds, &timeout); 79 | ... 80 | set_fd_set(n, inp, fds.res_in); 81 | set_fd_set(n, outp, fds.res_out); 82 | set_fd_set(n, exp, fds.res_ex); 83 | 84 | out: 85 | select_bits_free(bits, size); 86 | out_nofds: 87 | return ret; 88 | } 89 | ``` 90 | `sys_select()` 函数主要把用户态的参数复制到内核态,然后再通过调用 `do_select()` 函数进行监听操作, `do_select()` 函数实现如下(由于实现有点复杂,所以我们分段来分析): 91 | ```cpp 92 | int do_select(int n, fd_set_bits *fds, long *timeout) 93 | { 94 | poll_table table, *wait; 95 | int retval, i, off; 96 | long __timeout = *timeout; 97 | ... 98 | poll_initwait(&table); 99 | wait = &table; 100 | if (!__timeout) 101 | wait = NULL; 102 | retval = 0; 103 | ``` 104 | 上面这段代码主要通过调用 `poll_initwait()` 函数来初始化类型为 `poll_table` 结构的变量 `table`。要理解 `poll_table` 结构的作用,我们先来看看下面的知识点: 105 | 106 | > 因为每个socket都有个等待队列,当某个进程需要对socket进行读写的时候,如果发现此socket并不能读写, 107 | > 那么就可以添加到此socket的等待队列中进行休眠,当此socket可以读写时再唤醒队列中的进程。 108 | 109 | 而 `poll_table` 结构就是为了把进程添加到socket的等待队列中而创造的,我们先跳过这部分,后面分析到socket相关的知识点再来说明。 110 | 111 | 我们接着分析 `do_select()` 函数的实现: 112 | ```cpp 113 | for (;;) { 114 | set_current_state(TASK_INTERRUPTIBLE); 115 | for (i = 0 ; i < n; i++) { 116 | ... 117 | file = fget(i); 118 | mask = POLLNVAL; 119 | if (file) { 120 | mask = DEFAULT_POLLMASK; 121 | if (file->f_op && file->f_op->poll) 122 | mask = file->f_op->poll(file, wait); 123 | fput(file); 124 | } 125 | ``` 126 | 这段代码首先通过调用文件句柄的 `poll()` 接口来检查文件是否能够进行IO操作,对于socket来说,这个 `poll()` 接口就是 `sock_poll()`,所以我们来看看 `sock_poll()` 函数的实现: 127 | ```cpp 128 | static unsigned int sock_poll(struct file *file, poll_table * wait) 129 | { 130 | struct socket *sock; 131 | 132 | sock = socki_lookup(file->f_dentry->d_inode); 133 | return sock->ops->poll(file, sock, wait); 134 | } 135 | ``` 136 | `sock_poll()` 函数的实现很简单,首先通过 `socki_lookup()` 函数来把文件句柄转换成socket结构,接着调用socket结构的 `poll()` 接口,而对应 `TCP` 类型的socket,这个接口对应的是 `tcp_poll()` 函数,实现如下: 137 | ```cpp 138 | unsigned int tcp_poll(struct file * file, struct socket *sock, poll_table *wait) 139 | { 140 | unsigned int mask; 141 | struct sock *sk = sock->sk; 142 | struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp); 143 | 144 | poll_wait(file, sk->sleep, wait); // 把文件添加到sk->sleep队列中进行等待 145 | ... 146 | return mask; 147 | } 148 | ``` 149 | `tcp_poll()` 函数通过调用 `poll_wait()` 函数把进程添加到socket的等待队列中。然后检测socket是否可读写,并通过mask返回可读写的状态。所以在 `do_select()` 函数中的 `mask = file->f_op->poll(file, wait);` 这行代码其实调用的是 `tcp_poll()` 函数。 150 | 151 | 接着分析 `do_select()` 函数: 152 | ```cpp 153 | if ((mask & POLLIN_SET) && ISSET(bit, __IN(fds,off))) { 154 | SET(bit, __RES_IN(fds,off)); 155 | retval++; 156 | wait = NULL; 157 | } 158 | if ((mask & POLLOUT_SET) && ISSET(bit, __OUT(fds,off))) { 159 | SET(bit, __RES_OUT(fds,off)); 160 | retval++; 161 | wait = NULL; 162 | } 163 | if ((mask & POLLEX_SET) && ISSET(bit, __EX(fds,off))) { 164 | SET(bit, __RES_EX(fds,off)); 165 | retval++; 166 | wait = NULL; 167 | } 168 | ``` 169 | 因为 `mask` 变量保存了socket的可读写状态,所以上面这段代码主要通过判断socket的可读写状态来把socket放置到合适的返回集合中。如果socket可读,那么就把socket放置到可读集合中,如果socket可写,那么就放置到可写集合中。 170 | 171 | ```cpp 172 | wait = NULL; 173 | if (retval || !__timeout || signal_pending(current)) 174 | break; 175 | if(table.error) { 176 | retval = table.error; 177 | break; 178 | } 179 | __timeout = schedule_timeout(__timeout); 180 | } 181 | current->state = TASK_RUNNING; 182 | 183 | poll_freewait(&table); 184 | 185 | *timeout = __timeout; 186 | return retval; 187 | } 188 | ``` 189 | 最后这段代码的作用是,如果监听的socket集合中有可读写的socket,那么就直接返回(retval不为0时)。另外,如果调用 `select()` 时超时了,或者进程接收到信号,也需要返回。 190 | 191 | 否则,通过调用 `schedule_timeout()` 来进行一次进程调度。因为前面把进程的运行状态设置成 `TASK_INTERRUPTIBLE`,所以进行进程调度时就会把当前进程从运行队列中移除,进程进入休眠状态。那么什么时候进程才会变回运行状态呢? 192 | 193 | 前面我们说过,每个socket都有个等待队列,所以当socket可读写时便会把队列中的进程唤醒。这里分析一下当socket变成可读时,怎么唤醒等待队列中的进程的。 194 | 195 | 网卡接收到数据时,会进行一系列的接收数据操作,对于TCP协议来说,接收数据的调用链是: `tcp_v4_rcv() -> tcp_data() -> tcp_data_queue() -> sock_def_readable()`,我们来看看 `sock_def_readable()` 函数的实现: 196 | ```cpp 197 | void sock_def_readable(struct sock *sk, int len) 198 | { 199 | read_lock(&sk->callback_lock); 200 | if (sk->sleep && waitqueue_active(sk->sleep)) 201 | wake_up_interruptible(sk->sleep); 202 | sk_wake_async(sk,1,POLL_IN); 203 | read_unlock(&sk->callback_lock); 204 | } 205 | ``` 206 | 可以看出 `sock_def_readable()` 函数最终会调用 `wake_up_interruptible()` 函数来把等待队列中的进程唤醒,这时调用 `select()` 的进程从休眠状态变回运行状态。 207 | -------------------------------------------------------------------------------- /namespace.md: -------------------------------------------------------------------------------- 1 | ## namespace介绍 2 | `namespace(命名空间)` 是Linux提供的一种内核级别环境隔离的方法,很多编程语言也有 namespace 这样的功能,例如C++,Java等,编程语言的 namespace 是为了解决项目中能够在不同的命名空间里使用相同的函数名或者类名。而Linux的 namespace 也是为了实现资源能够在不同的命名空间里有相同的名称,譬如在 `A命名空间` 有个pid为1的进程,而在 `B命名空间` 中也可以有一个pid为1的进程。 3 | 4 | 有了 `namespace` 就可以实现基本的容器功能,著名的 `Docker` 也是使用了 namespace 来实现资源隔离的。 5 | 6 | Linux支持6种资源的 `namespace`,分别为([文档](https://lwn.net/Articles/531114/)): 7 | 8 | |Type | Parameter |Linux Version| 9 | |------------------|-------------|-------------| 10 | | Mount namespaces | CLONE_NEWNS |Linux 2.4.19 | 11 | | UTS namespaces | CLONE_NEWUTS|Linux 2.6.19 | 12 | | IPC namespaces | CLONE_NEWIPC|Linux 2.6.19 | 13 | | PID namespaces | CLONE_NEWPID|Linux 2.6.24 | 14 | |Network namespaces| CLONE_NEWNET|Linux 2.6.24 | 15 | | User namespaces |CLONE_NEWUSER|Linux 2.6.23 | 16 | 17 | 在调用 `clone()` 系统调用时,传入以上的不同类型的参数就可以实现复制不同类型的namespace。比如传入 `CLONE_NEWPID` 参数时,就是复制 `pid命名空间`,在新的 `pid命名空间` 里可以使用与其他 `pid命名空间` 相同的pid。代码如下: 18 | ```cpp 19 | #define _GNU_SOURCE 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | char child_stack[5000]; 30 | 31 | int child(void* arg) 32 | { 33 | printf("Child - %d\n", getpid()); 34 | return 1; 35 | } 36 | 37 | int main() 38 | { 39 | printf("Parent - fork child\n"); 40 | int pid = clone(child, child_stack+5000, CLONE_NEWPID, NULL); 41 | if (pid == -1) { 42 | perror("clone:"); 43 | exit(1); 44 | } 45 | waitpid(pid, NULL, 0); 46 | printf("Parent - child(%d) exit\n", pid); 47 | return 0; 48 | } 49 | ``` 50 | 输出如下: 51 | ``` 52 | Parent - fork child 53 | Parent - child(9054) exit 54 | Child - 1 55 | ``` 56 | 从运行结果可以看出,在子进程的 `pid命名空间` 里当前进程的pid为1,但在父进程的 `pid命名空间` 中子进程的pid却是9045。 57 | 58 | ## namespace实现原理 59 | 为了让每个进程都可以从属于某一个namespace,Linux内核为进程描述符添加了一个 `struct nsproxy` 的结构,如下: 60 | ```cpp 61 | struct task_struct { 62 | ... 63 | /* namespaces */ 64 | struct nsproxy *nsproxy; 65 | ... 66 | } 67 | 68 | struct nsproxy { 69 | atomic_t count; 70 | struct uts_namespace *uts_ns; 71 | struct ipc_namespace *ipc_ns; 72 | struct mnt_namespace *mnt_ns; 73 | struct pid_namespace *pid_ns; 74 | struct user_namespace *user_ns; 75 | struct net *net_ns; 76 | }; 77 | ``` 78 | 从 `struct nsproxy` 结构的定义可以看出,Linux为每种不同类型的资源定义了不同的命名空间结构体进行管理。比如对于 `pid命名空间` 定义了 `struct pid_namespace` 结构来管理 。由于 namespace 涉及的资源种类比较多,所以本文主要以 `pid命名空间` 作为分析的对象。 79 | 80 | 我们先来看看管理 `pid命名空间` 的 `struct pid_namespace` 结构的定义: 81 | ```cpp 82 | struct pid_namespace { 83 | struct kref kref; 84 | struct pidmap pidmap[PIDMAP_ENTRIES]; 85 | int last_pid; 86 | struct task_struct *child_reaper; 87 | struct kmem_cache *pid_cachep; 88 | unsigned int level; 89 | struct pid_namespace *parent; 90 | #ifdef CONFIG_PROC_FS 91 | struct vfsmount *proc_mnt; 92 | #endif 93 | }; 94 | ``` 95 | 因为 `struct pid_namespace` 结构主要用于为当前 `pid命名空间` 分配空闲的pid,所以定义比较简单: 96 | * `kref` 成员是一个引用计数器,用于记录引用这个结构的进程数 97 | * `pidmap` 成员用于快速找到可用pid的位图 98 | * `last_pid` 成员是记录最后一个可用的pid 99 | * `level` 成员记录当前 `pid命名空间` 所在的层次 100 | * `parent` 成员记录当前 `pid命名空间` 的父命名空间 101 | 102 | 由于 `pid命名空间` 是分层的,也就是说新创建一个 `pid命名空间` 时会记录父级 `pid命名空间` 到 `parent` 字段中,所以随着 `pid命名空间` 的创建,在内核中会形成一颗 `pid命名空间` 的树,如下图([图片来源](http://www.zhongruitech.com/256011226.html)): 103 | 104 | ![pid-namespace](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/pid-namespace-level.png) 105 | 106 | 第0层的 `pid命名空间` 是 `init` 进程所在的命名空间。如果一个进程所在的 `pid命名空间` 为 `N`,那么其在 `0 ~ N 层pid命名空间` 都有一个唯一的pid号。也就是说 `高层pid命名空间` 的进程对 `低层pid命名空间` 的进程是可见的,但是 `低层pid命名空间` 的进程对 `高层pid命名空间` 的进程是不可见的。 107 | 108 | 由于在 `第N层pid命名空间` 的进程其在 `0 ~ N层pid命名空间` 都有一个唯一的pid号,所以在进程描述符中通过 `pids` 成员来记录其在每个层的pid号,代码如下: 109 | ```cpp 110 | struct task_struct { 111 | ... 112 | struct pid_link pids[PIDTYPE_MAX]; 113 | ... 114 | } 115 | 116 | enum pid_type { 117 | PIDTYPE_PID, 118 | PIDTYPE_PGID, 119 | PIDTYPE_SID, 120 | PIDTYPE_MAX 121 | }; 122 | 123 | struct upid { 124 | int nr; 125 | struct pid_namespace *ns; 126 | struct hlist_node pid_chain; 127 | }; 128 | 129 | struct pid { 130 | atomic_t count; 131 | struct hlist_head tasks[PIDTYPE_MAX]; 132 | struct rcu_head rcu; 133 | unsigned int level; 134 | struct upid numbers[1]; 135 | }; 136 | 137 | struct pid_link { 138 | struct hlist_node node; 139 | struct pid *pid; 140 | }; 141 | ``` 142 | 这几个结构的关系如下图: 143 | 144 | ![pid-namespace-structs](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/pid-namespace-structs.png) 145 | 146 | 我们主要关注 `struct pid` 这个结构,`struct pid` 有个类型为 `struct upid` 的成员 `numbers`,其定义为只有一个元素的数组,但是其实是一个动态的数据,它的元素个数与 `level` 的值一致,也就是说当 `level` 的值为5时,那么 `numbers` 成员就是一个拥有5个元素的数组。而每个元素记录了其在每层 `pid命名空间` 的pid号,而 `struct upid` 结构的 `nr` 成员就是用于记录进程在不同层级 `pid命名空间` 的pid号。 147 | 148 | 我们通过代码来看看怎么为进程分配pid号的,在内核中是用过 `alloc_pid()` 函数分配pid号的,代码如下: 149 | ```cpp 150 | struct pid *alloc_pid(struct pid_namespace *ns) 151 | { 152 | struct pid *pid; 153 | enum pid_type type; 154 | int i, nr; 155 | struct pid_namespace *tmp; 156 | struct upid *upid; 157 | 158 | pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); 159 | if (!pid) 160 | goto out; 161 | 162 | tmp = ns; 163 | for (i = ns->level; i >= 0; i--) { 164 | nr = alloc_pidmap(tmp); // 为当前进程所在的不同层级pid命名空间分配一个pid 165 | if (nr < 0) 166 | goto out_free; 167 | 168 | pid->numbers[i].nr = nr; // 对应i层namespace中的pid数字 169 | pid->numbers[i].ns = tmp; // 对应i层namespace的实体 170 | tmp = tmp->parent; 171 | } 172 | 173 | get_pid_ns(ns); 174 | pid->level = ns->level; 175 | atomic_set(&pid->count, 1); 176 | for (type = 0; type < PIDTYPE_MAX; ++type) 177 | INIT_HLIST_HEAD(&pid->tasks[type]); 178 | 179 | spin_lock_irq(&pidmap_lock); 180 | for (i = ns->level; i >= 0; i--) { 181 | upid = &pid->numbers[i]; 182 | // 把upid连接到全局pid中, 用于快速查找pid 183 | hlist_add_head_rcu(&upid->pid_chain, 184 | &pid_hash[pid_hashfn(upid->nr, upid->ns)]); 185 | } 186 | spin_unlock_irq(&pidmap_lock); 187 | 188 | out: 189 | return pid; 190 | 191 | ... 192 | } 193 | ``` 194 | 上面的代码中,那个 `for (i = ns->level; i >= 0; i--)` 就是通过 `parent` 成员不断向上检索为不同层级的 `pid命名空间` 分配一个唯一的pid号,并且保存到对应的 `nr` 字段中。另外,还会把进程所在各个层级的pid号添加到全局pid哈希表中,这样做是为了通过pid号快速找到进程。 195 | 196 | 现在我们来看看怎么通过pid号快速找到一个进程,在内核中 `find_get_pid()` 函数用来通过pid号查找对应的 `struct pid` 结构,代码如下(find_get_pid() -> find_vpid() -> find_pid_ns()): 197 | ```cpp 198 | struct pid *find_get_pid(pid_t nr) 199 | { 200 | struct pid *pid; 201 | 202 | rcu_read_lock(); 203 | pid = get_pid(find_vpid(nr)); 204 | rcu_read_unlock(); 205 | 206 | return pid; 207 | } 208 | 209 | struct pid *find_vpid(int nr) 210 | { 211 | return find_pid_ns(nr, current->nsproxy->pid_ns); 212 | } 213 | 214 | struct pid *find_pid_ns(int nr, struct pid_namespace *ns) 215 | { 216 | struct hlist_node *elem; 217 | struct upid *pnr; 218 | 219 | hlist_for_each_entry_rcu(pnr, elem, 220 | &pid_hash[pid_hashfn(nr, ns)], pid_chain) 221 | if (pnr->nr == nr && pnr->ns == ns) 222 | return container_of(pnr, struct pid, 223 | numbers[ns->level]); 224 | 225 | return NULL; 226 | } 227 | ``` 228 | 通过pid号查找 `struct pid` 结构时,首先会把进程pid号和当前进程的 `pid命名空间` 传入到 `find_pid_ns()` 函数,而在 `find_pid_ns()` 函数中通过全局pid哈希表来快速查找对应的 `struct pid` 结构。获取到 `struct pid` 结构后就可以很容易地获取到进程对应的进程描述符,例如可以通过 `pid_task()` 函数来获取 `struct pid` 结构对应进程描述符,由于代码比较简单,这里就不再分析了。 229 | -------------------------------------------------------------------------------- /physical-memory-buddy-system.md: -------------------------------------------------------------------------------- 1 | ### 伙伴系统分配算法 2 | 在上一节, 我们介绍了Linux内核怎么管理系统中的物理内存. 但有时候内核需要分配一些物理内存地址也连续的内存页, 所以Linux使用了 `伙伴系统分配算法` 来管理系统中的物理内存页. 3 | 4 | 上一节说过, 内核使用 `alloc_pages()` 函数来分配内存页, 而 `alloc_pages()` 函数最后会调用 `rmqueue()` 函数来分配内存页, `rmqueue()` 函数原型如下: 5 | ```cpp 6 | static struct page * rmqueue(zone_t *zone, unsigned long order); 7 | ``` 8 | 参数 `zone` 是内存管理区, 而 `order` 是要分配 2order 个内存页. 由于 `rmqueue()` 函数使用了伙伴系统算法, 所以下面先来介绍一下伙伴系统算法的原理. 9 | 10 | 伙伴系统算法的核心是 `伙伴`, 那什么是伙伴呢? 在Linux内核中, 把两个物理地址相邻的内存页当作成伙伴, 因为Linux是以页面号来管理内存页的, 所以就是说两个相邻页面号的页面是伙伴关系. 但是并不是所有相邻页面号的页面都是伙伴关系, 例如0号和1号页面是伙伴关系, 但是1号和2号就不是了. 为什么呢? 这是因为如果把1号页面和2号页面当成伙伴关系, 那么0号页面就没有伙伴从而变成孤岛了. 11 | 12 | 那么给定一个 `i` 号内存页, 怎么找到他的伙伴内存页呢? 通过观察我们可以发现, 如果页面号是复数的, 那么他的伙伴内存页要加1, 如果页面号是单数的, 那么他的伙伴内存页要减1. 所以对于给定一个页面号为 `i` 的内存页, 他的伙伴内存页号可以使用以下的代码获得: 13 | ```cpp 14 | if (i & 1) { 15 | buddy = i - 1 16 | } else { 17 | buddy = i + 1 18 | } 19 | ``` 20 | 那么知道一个内存页的伙伴页面有什么用呢? 答案是为了合并为更大的内存页, 例如把两个单位为1的伙伴内存页合并成为一个单位为2的内存页(这时应该称为内存块), 把两个单位为2的伙伴内存块合并为单位为4的内存块, 以此类推. 21 | 22 | 所以, 使用伙伴系统算法只能分配 2order (order为0,1,2,3...)个页面. 那么order是不是无限大呢? 当然不是, 在Linux内核中, order的最大值是 `10`. 也就是说在内核中, 最大能够申请到一个 29 个页面的内存块. 23 | 24 | 上一节我们介绍过内存管理区数据结构 `struct zone_struct`, 在内存管理区数据结构中有个名为 `free_area` 类型为 `free_area_t` 的字段, 他的作用就是用来管理内存管理区内的空闲物理内存页. 定义如下: 25 | ```cpp 26 | #define MAX_ORDER 10 27 | 28 | typedef struct free_area_struct { 29 | struct list_head free_list; 30 | unsigned int *map; 31 | } free_area_t; 32 | 33 | typedef struct zone_struct { 34 | ... 35 | free_area_t free_area[MAX_ORDER]; // 用于伙伴分配算法 36 | ... 37 | } zone_t; 38 | ``` 39 | `free_area` 是伙伴系统算法的核心, 可以看到 `free_area` 有10个元素, 每个元素都是一个类型为 `free_area_t` 的结构体, `free_area_t` 结构的 `free_list` 字段用于连接有相同页面个数的内存块. `map` 字段是一个位图, 用于记录伙伴内存块的使用情况. 40 | 41 | Linux内核使用 `free_area[i]` 管理 2i 个内存页面大小的内存块列表. 例如 `free_area[0]` 就是管理1个内存页面大小的内存块(20等于1), 而 `free_area[1]` 则管理2个内存页面大小的内存块(21等于2). 如下图所示: 42 | ![](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/memory_free_list.png) 43 | 44 | 上一节我们说过, 在管理物理内存页的 `struct page` 结构中有个 `list` 的字段, 内核就是通过这个字段把有着相同个数页面的内存块连成一个链表的: 45 | ```cpp 46 | typedef struct page { 47 | struct list_head list; 48 | ... 49 | } mem_map_t; 50 | ``` 51 | 前面我们说过, 在 `free_area_t` 结构中有个名为 `map` 的字段, `map` 字段是一个位图, 每个位记录着一对伙伴内存块的使用情况. 举个例子, 如果一对伙伴内存块中的某一个内存块在使用, 那么对应的位就为1, 如果两个伙伴内存块都是空闲或者使用, 那么对应的位就为0. 如下图: 52 | ![](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/memory_free_area.jpg) 53 | 54 | 使用位图来标识伙伴内存块使用情况的原因是: 当释放内存块时, 如果对应的位是1的话, 那么说明另外一个伙伴内存块是空闲状态的, 所以释放当前内存块可以跟其伙伴内存块合并成一个更大的内存块了. 55 | 56 | 我们来看看内核在初始化内存管理区时怎么初始化空闲内存块链表的, 代码如下: 57 | ```cpp 58 | void __init free_area_init_core(int nid, pg_data_t *pgdat, struct page **gmap, 59 | unsigned long *zones_size, unsigned long zone_start_paddr, 60 | unsigned long *zholes_size, struct page *lmem_map) 61 | { 62 | ... 63 | for (j = 0; j < MAX_NR_ZONES; j++) { 64 | zone_t *zone = pgdat->node_zones + j; 65 | unsigned long mask; 66 | unsigned long size, realsize; 67 | 68 | ... 69 | 70 | mask = -1; // 32位系统这个值等于0xffffffff 71 | for (i = 0; i < MAX_ORDER; i++) { // 初始化free_area 72 | unsigned long bitmap_size; 73 | 74 | memlist_init(&zone->free_area[i].free_list); // 初始化空闲链表 75 | mask += mask; // 这里等于: mask = mask << 1; 76 | size = (size + ~mask) & mask; // 用于向上对齐 77 | bitmap_size = size >> i; // 内存块个数 78 | bitmap_size = (bitmap_size + 7) >> 3; // 因为一个字节有8个位, 所以要除以8 79 | bitmap_size = LONG_ALIGN(bitmap_size); 80 | // 申请位图内存 81 | zone->free_area[i].map = 82 | (unsigned int *) alloc_bootmem_node(pgdat, bitmap_size); 83 | } 84 | 85 | ... 86 | } 87 | ... 88 | } 89 | ``` 90 | 上面的代码首先为每个管理不同大小空闲内存块的 `free_area_t` 结构初始化其 `free_list` 字段, 然后根据其管理内存块的大小来计算需要多少个位来记录伙伴内存块的关系, 并保存到 `map` 字段中. 91 | > 说明一下, 这里计算位图的大小时为每个内存块申请了一个位, 但事实上每个位记录的是一对伙伴内存块的关系, 所以需要除以2, 而现在明显浪费了一半的内存. 在后面的Linux版本中改进了这个问题. 92 | 93 | 现在再回头看看物理内存分配 `rmqueue()` 函数的实现: 94 | ```cpp 95 | static struct page * rmqueue(zone_t *zone, unsigned long order) 96 | { 97 | free_area_t * area = zone->free_area + order; // 获取申请对应大小内存块的空闲列表 98 | unsigned long curr_order = order; 99 | struct list_head *head, *curr; 100 | unsigned long flags; 101 | struct page *page; 102 | 103 | spin_lock_irqsave(&zone->lock, flags); 104 | do { 105 | head = &area->free_list; // 空闲内存块链表 106 | curr = memlist_next(head); 107 | 108 | if (curr != head) { // 如果链表不为空 109 | unsigned int index; 110 | 111 | page = memlist_entry(curr, struct page, list); // 当前内存块 112 | if (BAD_RANGE(zone,page)) 113 | BUG(); 114 | memlist_del(curr); 115 | index = (page - mem_map) - zone->offset; // 内存块所在内存管理区的索引 116 | MARK_USED(index, curr_order, area); // 标记伙伴标志位为已用 117 | zone->free_pages -= 1 << order; // 减去内存块所占用的内存页数 118 | 119 | // 把更大的内存块分裂为申请大小的内存块 120 | page = expand(zone, page, index, order, curr_order, area); 121 | spin_unlock_irqrestore(&zone->lock, flags); 122 | 123 | set_page_count(page, 1); 124 | if (BAD_RANGE(zone,page)) 125 | BUG(); 126 | DEBUG_ADD_PAGE 127 | return page; 128 | } 129 | // 如果在当前空闲链表中没有空闲的内存块, 那么向空间更大的的空闲内存块链表中申请 130 | curr_order++; 131 | area++; 132 | } while (curr_order < MAX_ORDER); 133 | spin_unlock_irqrestore(&zone->lock, flags); 134 | 135 | return NULL; 136 | } 137 | ``` 138 | 申请内存块时, 首先会在大小一致的空闲链表中申请, 如果大小一致的空闲链表没有空闲的内存块, 那么只能向空间更大的空闲内存块链表中申请. 如果申请到的内存块比要申请的大小大, 那么需要调用 `expand()` 函数来把内存块分裂成指定大小的内存块. 139 | 140 | 大内存块分裂为小内存块的过程也很简单, 举个例子: 如果我们要申请order为2的内存块(也就是大小为4个内存页的内存块), 但是order为2的空闲链表没有空闲的内存, 那么只能向order为3的空闲内存块链表中申请, 如果order为3的空闲链表有空闲内存块, 那么就从order为3的链表中申请一块空闲内存块, 并且把此内存块分裂为2块order为2的内存块, 一块添加到order为2的空闲链表中, 另外一块分配给用户. 如果order为3的空闲链表也没有空闲内存块, 那么只能向order为4的空闲链表中申请, 如此类推. `expand()` 函数的源码如下: 141 | ```cpp 142 | static inline struct page * expand (zone_t *zone, struct page *page, 143 | unsigned long index, int low, int high, free_area_t * area) 144 | { 145 | unsigned long size = 1 << high; 146 | 147 | while (high > low) { 148 | if (BAD_RANGE(zone,page)) 149 | BUG(); 150 | area--; 151 | high--; 152 | size >>= 1; 153 | memlist_add_head(&(page)->list, &(area)->free_list); // 把分裂出来的一块内存块添加到下一级空闲链表中 154 | MARK_USED(index, high, area); // 标记伙伴标志位为已用 155 | index += size; 156 | page += size; 157 | } 158 | if (BAD_RANGE(zone,page)) 159 | BUG(); 160 | return page; 161 | } 162 | ``` 163 | 可以对照上面的思路来分析 `expand()` 函数. 164 | 165 | 我们接着来分析内存块的释放, 内存块的释放是通过 `free_pages()` 函数来实现的, 而 `free_pages()` 函数最终会调用 `__free_pages_ok()` 函数, `__free_pages_ok()` 函数代码如下: 166 | ```cpp 167 | static void __free_pages_ok (struct page *page, unsigned long order) 168 | { 169 | unsigned long index, page_idx, mask, flags; 170 | free_area_t *area; 171 | struct page *base; 172 | zone_t *zone; 173 | ... 174 | zone = page->zone; 175 | 176 | mask = (~0UL) << order; // 获取一个后order个位为0的长整型数字 177 | base = mem_map + zone->offset; // 获取内存管理区管理的开始内存页 178 | page_idx = page - base; // 当前页面在内存管理区的索引 179 | if (page_idx & ~mask) 180 | BUG(); 181 | index = page_idx >> (1 + order); // 伙伴标记位索引 182 | 183 | area = zone->free_area + order; // 内存块所在的空闲链表 184 | 185 | spin_lock_irqsave(&zone->lock, flags); 186 | 187 | zone->free_pages -= mask; // 添加释放的内存块所占用的内存页数 188 | 189 | while (mask + (1 << (MAX_ORDER-1))) { // 遍历(MAX_ORDER-order-1, MAX_ORDER等于10)次, 也就是说最多循环9次 190 | struct page *buddy1, *buddy2; 191 | 192 | if (area >= zone->free_area + MAX_ORDER) 193 | BUG(); 194 | if (!test_and_change_bit(index, area->map)) // 如果伙伴内存块在使用状态, 那么退出循环 195 | break; 196 | 197 | buddy1 = base + (page_idx ^ -mask); // 伙伴内存块(-mask 等于 1<list); // 把伙伴内存块从空闲链表中删除(因为要合并为更大的内存块, 所以要从当前的空闲链表中删除) 205 | mask <<= 1; 206 | area++; // 向更大的空闲链表进行合并操作 207 | index >>= 1; 208 | page_idx &= mask; 209 | } 210 | memlist_add_head(&(base + page_idx)->list, &area->free_list); 211 | 212 | spin_unlock_irqrestore(&zone->lock, flags); 213 | 214 | if (memory_pressure > NR_CPUS) 215 | memory_pressure--; 216 | } 217 | ``` 218 | 释放过程和分配过程是一对互逆的过程, 释放内存块时首先看看伙伴内存块的状态, 如果伙伴内存块是空闲状态, 那么就与伙伴内存块合并为更大的内存块, 并且一直尝试合并为更大的内存块, 直到伙伴内存块不是空闲状态或者达到内存块的最大限制(order为9)停止合并过程, 根据上面代码的注释可以慢慢理解. 219 | -------------------------------------------------------------------------------- /process-management.md: -------------------------------------------------------------------------------- 1 | # 进程管理 2 | ## 进程 3 | 程序是指储存在外部存储(如硬盘)的一个可执行文件, 而进程是指处于执行期间的程序, 进程包括 `代码段(text section)` 和 `数据段(data section)`, 除了代码段和数据段外, 进程一般还包含打开的文件, 要处理的信号和CPU上下文等等. 4 | 5 | ### 进程描述符 6 | Linux进程使用 `struct task_struct` 来描述(include/linux/sched.h), 如下: 7 | ```c 8 | struct task_struct { 9 | /* 10 | * offsets of these are hardcoded elsewhere - touch with care 11 | */ 12 | volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 13 | unsigned long flags; /* per process flags, defined below */ 14 | int sigpending; 15 | mm_segment_t addr_limit; /* thread address space: 16 | 0-0xBFFFFFFF for user-thead 17 | 0-0xFFFFFFFF for kernel-thread 18 | */ 19 | struct exec_domain *exec_domain; 20 | volatile long need_resched; 21 | unsigned long ptrace; 22 | 23 | int lock_depth; /* Lock depth */ 24 | 25 | /* 26 | * offset 32 begins here on 32-bit platforms. We keep 27 | * all fields in a single cacheline that are needed for 28 | * the goodness() loop in schedule(). 29 | */ 30 | long counter; 31 | long nice; 32 | unsigned long policy; 33 | struct mm_struct *mm; 34 | int processor; 35 | ... 36 | } 37 | ``` 38 | Linux把所有的进程使用双向链表连接起来, 如下图(来源): 39 | 40 | ![](https://github.com/liexusong/myblog/blob/master/images/task_list.png) 41 | 42 | Linux内核为了加快获取当前进程的的task_struct结构, 使用了一个技巧, 就是把task_struct放置在内核栈的栈底, 这样就可以通过 `esp寄存器` 快速获取到当前运行进程的task_struct结构. 如下图: 43 | 44 | ![](https://raw.githubusercontent.com/liexusong/myblog/master/images/task_stack.png) 45 | 46 | 获取当前运行进程的task_struct代码如下: 47 | ```c 48 | static inline struct task_struct * get_current(void) 49 | { 50 | struct task_struct *current; 51 | __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL)); 52 | return current; 53 | } 54 | ``` 55 | 56 | ### 进程状态 57 | 进程描述符的state字段用于保存进程的当前状态, 进程的状态有以下几种: 58 | * `TASK_RUNNING (运行)` -- 进程处于可执行状态, 在这个状态下的进程要么正在被CPU执行, 要么在等待执行(CPU被其他进程占用的情况下). 59 | * `TASK_INTERRUPTIBLE (可中断等待)` -- 进程处于等待状态, 其在等待某些条件成立或者接收到某些信号, 进程会被唤醒变为运行状态. 60 | * `TASK_UNINTERRUPTIBLE (不可中断等待)` -- 进程处于等待状态, 其在等待某些条件成立, 进程会被唤醒变为运行状态, 但不能被信号唤醒. 61 | * `TASK_TRACED (被追踪)` -- 进程处于被追踪状态, 例如通过ptrace命令对进程进行调试. 62 | * `TASK_STOPPED (停止)` -- 进程处于停止状态, 进程不能被执行. 一般接收到SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU信号进程会变成TASK_STOPPED状态. 63 | 64 | 各种状态间的转换如下图: 65 | 66 | ![](https://raw.githubusercontent.com/liexusong/myblog/master/images/task_state.png) 67 | 68 | ### 进程的创建 69 | 在Linux系统中,进程的创建使用fork()系统调用,fork()调用会创建一个与父进程一样的子进程,唯一不同就是fork()的返回值,父进程返回的是子进程的进程ID,而子进程返回的是0。 70 | 71 | Linux创建子进程时使用了`写时复制(Copy On Write)`,也就是创建子进程时使用的是父进程的内存空间,当子进程或者父进程修改数据时才会复制相应的内存页。 72 | 73 | 当调用fork()系统调用时会陷入内核空间并且调用sys_fork()函数,sys_fork()函数会调用do_fork()函数,代码如下(arch/i386/kernel/process.c): 74 | ```c 75 | asmlinkage int sys_fork(struct pt_regs regs) 76 | { 77 | return do_fork(SIGCHLD, regs.esp, ®s, 0); 78 | } 79 | ``` 80 | do_fork()主要的工作是申请一个进程描述符, 然后初始化进程描述符的各个字段, 包括调用 copy_files() 函数复制打开的文件, 调用 copy_sighand() 函数复制信号处理函数, 调用 copy_mm() 函数复制进程虚拟内存空间, 调用 copy_namespace() 函数复制命名空间. 代码如下: 81 | ```c 82 | int do_fork(unsigned long clone_flags, unsigned long stack_start, 83 | struct pt_regs *regs, unsigned long stack_size) 84 | { 85 | ... 86 | p = alloc_task_struct(); // 申请进程描述符 87 | ... 88 | if (copy_files(clone_flags, p)) 89 | goto bad_fork_cleanup; 90 | if (copy_fs(clone_flags, p)) 91 | goto bad_fork_cleanup_files; 92 | if (copy_sighand(clone_flags, p)) 93 | goto bad_fork_cleanup_fs; 94 | if (copy_mm(clone_flags, p)) 95 | goto bad_fork_cleanup_sighand; 96 | if (copy_namespace(clone_flags, p)) 97 | goto bad_fork_cleanup_mm; 98 | retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); 99 | ... 100 | wake_up_process(p); 101 | ... 102 | } 103 | ``` 104 | 值得注意的是do_fork() 还调用了 copy_thread() 这个函数, copy_thread()这个函数主要用于设置进程的CPU执行上下文 `struct thread_struct` 结构. 代码如下: 105 | ```cpp 106 | int copy_thread(int nr, unsigned long clone_flags, unsigned long esp, 107 | unsigned long unused, 108 | struct task_struct * p, struct pt_regs * regs) 109 | { 110 | struct pt_regs * childregs; 111 | 112 | // 指向栈顶(见图2) 113 | childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1; 114 | struct_cpy(childregs, regs); // 复制父进程的栈信息 115 | childregs->eax = 0; // 这个是子进程调用fork()之后的返回值, 也就是0 116 | childregs->esp = esp; // 设置新的栈空间 117 | 118 | p->thread.esp = (unsigned long) childregs; // 子进程当前的栈地址, 调用switch_to()的时候esp设置为这个地址 119 | p->thread.esp0 = (unsigned long) (childregs+1); // 子进程内核空间栈地址 120 | 121 | p->thread.eip = (unsigned long) ret_from_fork; // 子进程将要执行的代码地址 122 | 123 | savesegment(fs,p->thread.fs); 124 | savesegment(gs,p->thread.gs); 125 | 126 | unlazy_fpu(current); 127 | struct_cpy(&p->thread.i387, ¤t->thread.i387); 128 | 129 | return 0; 130 | } 131 | ``` 132 | do_fork() 函数最后调用 wake_up_process() 函数唤醒子进程, 让子进程进入运行状态. 133 | 134 | ### 内核线程 135 | Linux内核有很多任务需要去做, 例如定时把缓冲中的数据刷到硬盘, 当内存不足的时候进行内存的回收等, 这些工作都需要通过内核线程来完成. 内核线程与普通进程的主要区别就是: 内核线程没有自己的 `虚拟空间结构(struct mm)`, 每次内核线程执行的时候都是借助当前运行进程的虚拟内存空间结构来运行, 因为内核线程只会运行在内核态, 而每个进程的内核态空间都是一样的, 所以借助其他进程的虚拟内存空间结构来运行是完全可行的. 136 | 137 | 内核线程使用 `kernel_thread()` 函数来创建, 代码如下: 138 | ```cpp 139 | int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) 140 | { 141 | long retval, d0; 142 | 143 | __asm__ __volatile__( 144 | "movl %%esp,%%esi\n\t" 145 | "int $0x80\n\t" /* Linux/i386 system call */ 146 | "cmpl %%esp,%%esi\n\t" /* child or parent? */ 147 | "je 1f\n\t" /* parent - jump */ 148 | /* Load the argument into eax, and push it. That way, it does 149 | * not matter whether the called function is compiled with 150 | * -mregparm or not. */ 151 | "movl %4,%%eax\n\t" 152 | "pushl %%eax\n\t" 153 | "call *%5\n\t" /* call fn */ 154 | "movl %3,%0\n\t" /* exit */ 155 | "int $0x80\n" 156 | "1:\t" 157 | :"=&a" (retval), "=&S" (d0) 158 | :"0" (__NR_clone), "i" (__NR_exit), 159 | "r" (arg), "r" (fn), 160 | "b" (flags | CLONE_VM) 161 | : "memory"); 162 | return retval; 163 | } 164 | ``` 165 | 因为这个函数式使用嵌入汇编来实现的, 所以有点难懂, 不过主要过程就是通过调用 `_clone()`函数来创建一个新的进程, 而创建进程是通过传入 `CLONE_VM` 标志来指定进程借用其他进程的虚拟内存空间结构. 166 | 167 | > 特别说明一下:`d0` 局部变量的作用是为了在创建内核线程时保证 `struct pt_regs` 结构的完整, 168 | > 169 | > 这是因为创建内核线程是在内核态进行的,所以在内核态调用系统调用是不会压入 `ss` 和 `esp` 寄存器的, 170 | > 171 | > 这样就会导致系统调用的 `struct pt_regs` 参数信息不完整, 172 | > 173 | > 所以 `kernel_thread()` 函数定义了一个 `d0` 局部变量是为了补充没压栈的 `ss` 和 `esp` 的。 174 | -------------------------------------------------------------------------------- /process-schedule-o1.md: -------------------------------------------------------------------------------- 1 | ## O(1)调度算法 2 | 3 | Linux是一个支持多任务的操作系统,而多个任务之间的切换是通过 `调度器` 来完成,`调度器` 使用不同的调度算法会有不同的效果。 4 | 5 | Linux2.4版本使用的调度算法的时间复杂度为O(n),其主要原理是通过轮询所有可运行任务列表,然后挑选一个最合适的任务运行,所以其时间复杂度与可运行任务队列的长度成正比。 6 | 7 | 而Linux2.6开始替换成名为 `O(1)调度算法`,顾名思义,其时间复杂度为O(1)。虽然在后面的版本开始使用 `CFS调度算法(完全公平调度算法)`,但了解 `O(1)调度算法` 对学习Linux调度器还是有很大帮助的,所以本文主要介绍 `O(1)调度算法` 的原理与实现。 8 | 9 | 由于在 Linux 内核中,任务和进程是相同的概念,所以在本文混用了任务和进程这两个名词。 10 | 11 | ### O(1)调度算法原理 12 | 13 | #### `prio_array` 结构 14 | 15 | `O(1)调度算法` 通过优先级来对任务进行分组,可分为140个优先级(0 ~ 139,数值越小优先级越高),每个优先级的任务由一个队列来维护。`prio_array` 结构就是用来维护这些任务队列,如下代码: 16 | ```cpp 17 | #define MAX_USER_RT_PRIO 100 18 | #define MAX_RT_PRIO MAX_USER_RT_PRIO 19 | #define MAX_PRIO (MAX_RT_PRIO + 40) 20 | 21 | #define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long)) 22 | 23 | struct prio_array { 24 | int nr_active; 25 | unsigned long bitmap[BITMAP_SIZE]; 26 | struct list_head queue[MAX_PRIO]; 27 | }; 28 | ``` 29 | 下面介绍 `prio_array` 结构各个字段的作用: 30 | 1. `nr_active`: 所有优先级队列中的总任务数。 31 | 2. `bitmap`: 位图,每个位对应一个优先级的任务队列,用于记录哪个任务队列不为空,能通过 `bitmap` 够快速找到不为空的任务队列。 32 | 3. `queue`: 优先级队列数组,每个元素维护一个优先级队列,比如索引为0的元素维护着优先级为0的任务队列。 33 | 34 | 下图更直观地展示了 `prio_array` 结构各个字段的关系: 35 | 36 | ![prio_array](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/process-schedule-o1.jpg) 37 | 38 | 如上图所述,`bitmap` 的第2位和第6位为1(红色代表为1,白色代表为0),表示优先级为2和6的任务队列不为空,也就是说 `queue` 数组的第2个元素和第6个元素的队列不为空。 39 | 40 | #### `runqueue` 结构 41 | 42 | 另外,为了减少多核CPU之间的竞争,所以每个CPU都需要维护一份本地的优先队列。因为如果使用全局的优先队列,那么多核CPU就需要对全局优先队列进行上锁,从而导致性能下降。 43 | 44 | 每个CPU都需要维护一个 `runqueue` 结构,`runqueue` 结构主要维护任务调度相关的信息,比如优先队列、调度次数、CPU负载信息等。其定义如下: 45 | ```cpp 46 | struct runqueue { 47 | spinlock_t lock; 48 | unsigned long nr_running, 49 | nr_switches, 50 | expired_timestamp, 51 | nr_uninterruptible; 52 | task_t *curr, *idle; 53 | struct mm_struct *prev_mm; 54 | prio_array_t *active, *expired, arrays[2]; 55 | int prev_cpu_load[NR_CPUS]; 56 | task_t *migration_thread; 57 | struct list_head migration_queue; 58 | atomic_t nr_iowait; 59 | }; 60 | ``` 61 | `runqueue` 结构有两个重要的字段:`active` 和 `expired`,这两个字段在 `O(1)调度算法` 中起着至关重要的作用。我们先来了解一下 `O(1)调度算法` 的大概原理。 62 | 63 | 我们注意到 `active` 和 `expired` 字段的类型为 `prio_array`,指向任务优先队列。`active` 代表可以调度的任务队列,而 `expired` 字段代表时间片已经用完的任务队列。`active` 和 `expired` 会进行以下两个过程: 64 | 65 | 1. 当 `active` 中的任务时间片用完,那么就会被移动到 `expired` 中。 66 | 2. 当 `active` 中已经没有任务可以运行,就把 `expired` 与 `active` 交换,从而 `expired` 中的任务可以重新被调度。 67 | 68 | 如下图所示: 69 | 70 | ![process-schedule-o1-move](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/process-schedule-o1-move.jpg) 71 | 72 | `O(1)调度算法` 把140个优先级的前100个(0 ~ 99)作为 `实时进程优先级`,而后40个(100 ~ 139)作为 `普通进程优先级`。实时进程被放置到实时进程优先级的队列中,而普通进程放置到普通进程优先级的队列中。 73 | 74 | #### 实时进程调度 75 | 76 | 实时进程分为 `FIFO(先进先出)` 和 `RR(时间轮询)` 两种,其调度算法比较简单,如下: 77 | 1. `先进先出的实时进程调度`:如果调度器在执行某个先进先出的实时进程,那么调度器会一直运行这个进程,直至其主动放弃运行权(退出进程或者sleep等)。 78 | 2. `时间轮询的实时进程调度`:如果调度器在执行某个时间轮询的实时进程,那么调度器会判断当前进程的时间片是否用完,如果用完的话,那么重新分配时间片给它,并且重新放置回 `active` 队列中,然后调度到其他同优先级或者优先级更高的实时进程进行运行。 79 | 80 | #### 普通进程调度 81 | 82 | 每个进程都要一个动态优先级和静态优先级,静态优先级不会变化(进程创建时被设置),而动态优先级会随着进程的睡眠时间而发生变化。动态优先级可以通过以下公式进行计算: 83 | ``` 84 | 动态优先级 = max(100, min(静态优先级 – bonus + 5), 139)) 85 | ``` 86 | 上面公式的 `bonus(奖励或惩罚)` 是通过进程的睡眠时间计算出来,进程的睡眠时间越大,`bonus` 的值就越大,那么动态优先级就越高(前面说过优先级的值越小,优先级越高)。 87 | 88 | > 另外要说明一下,实时进程的动态优先级与静态优先级相同。 89 | 90 | 当一个普通进程被添加到运行队列时,会先计算其动态优先级,然后按照动态优先级的值来添加到对应优先级的队列中。而调度器调度进程时,会先选择优先级最高的任务队列中的进程进行调度运行。 91 | 92 | #### 运行时间片计算 93 | 94 | 当进程的时间用完后,就需要重新进行计算。进程的运行时间片与静态优先级有关,可以通过以下公式进行计算: 95 | ``` 96 | 静态优先级 < 120,运行时间片 = max((140-静态优先级)*20, MIN_TIMESLICE) 97 | 静态优先级 >= 120,运行时间片 = max((140-静态优先级)*5, MIN_TIMESLICE) 98 | ``` 99 | ### O(1)调度算法实现 100 | 101 | 接下来我们分析一下 `O(1)调度算法` 在内核中的实现。 102 | 103 | #### 时钟中断 104 | 105 | 时钟中断是由硬件触发的,可以通过编程来设置其频率,Linux内核一般设置为每秒产生100 ~ 1000次。时钟中断会触发调用 `scheduler_tick()` 内核函数,其主要工作是:减少进程的可运行时间片,如果时间片用完,那么把进程从 `active` 队列移动到 `expired` 队列中。代码如下: 106 | ```cpp 107 | void scheduler_tick(int user_ticks, int sys_ticks) 108 | { 109 | runqueue_t *rq = this_rq(); 110 | task_t *p = current; 111 | 112 | ... 113 | 114 | // 处理普通进程 115 | if (!--p->time_slice) { // 减少时间片, 如果时间片用完 116 | dequeue_task(p, rq->active); // 把进程从运行队列中删除 117 | set_tsk_need_resched(p); // 设置要重新调度标志 118 | p->prio = effective_prio(p); // 重新计算动态优先级 119 | p->time_slice = task_timeslice(p); // 重新计算时间片 120 | p->first_time_slice = 0; 121 | 122 | if (!rq->expired_timestamp) 123 | rq->expired_timestamp = jiffies; 124 | 125 | // 如果不是交互进程或者没有处于饥饿状态 126 | if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) { 127 | enqueue_task(p, rq->expired); // 移动到expired队列 128 | } else 129 | enqueue_task(p, rq->active); // 重新放置到active队列 130 | } 131 | ... 132 | } 133 | ``` 134 | 上面代码主要完成以下几个工作: 135 | 1. 减少进程的时间片,并且判断时间片是否已经使用完。 136 | 2. 如果时间片使用完,那么把进程从 `active` 队列中删除。 137 | 3. 调用 `set_tsk_need_resched()` 函数设 `TIF_NEED_RESCHED` 标志,表示当前进程需要重新调度。 138 | 4. 调用 `effective_prio()` 函数重新计算进程的动态优先级。 139 | 5. 调用 `task_timeslice()` 函数重新计算进程的可运行时间片。 140 | 6. 如果当前进程是交互进程并且没有处于饥饿状态,那么重新加入到 `active` 队列。 141 | 7. 否则把进程移动到 `expired` 队列。 142 | 143 | #### 任务调度 144 | 145 | 如果进程设置了 `TIF_NEED_RESCHED` 标志,那么当从时钟中断返回到用户空间时,会调用 `schedule()` 函数进行任务调度。`schedule()` 函数代码如下: 146 | ```cpp 147 | void schedule(void) 148 | { 149 | ... 150 | prev = current; // 当前需要被调度的进程 151 | rq = this_rq(); // 获取当前CPU的runqueue 152 | 153 | array = rq->active; // active队列 154 | 155 | // 如果active队列中没有进程, 那么替换成expired队列 156 | if (unlikely(!array->nr_active)) { 157 | rq->active = rq->expired; 158 | rq->expired = array; 159 | array = rq->active; 160 | rq->expired_timestamp = 0; 161 | } 162 | 163 | idx = sched_find_first_bit(array->bitmap); // 找到最高优先级的任务队列 164 | queue = array->queue + idx; 165 | next = list_entry(queue->next, task_t, run_list); // 获取到下一个将要运行的进程 166 | ... 167 | 168 | if (likely(prev != next)) { 169 | ... 170 | prev = context_switch(rq, prev, next); // 切换到next进程进行运行 171 | ... 172 | } 173 | ... 174 | } 175 | ``` 176 | 上面代码主要完成以下几个步骤: 177 | 1. 如果当前 `runqueue` 的 `active` 队列为空,那么把 `active` 队列与 `expired` 队列进行交换。 178 | 2. 调用 `sched_find_first_bit()` 函数在 `bitmap` 中找到优先级最高并且不为空的任务队列索引。 179 | 3. 调用 `context_switch()` 函数切换到next进程进行运行。 180 | 181 | -------------------------------------------------------------------------------- /process-schedule.md: -------------------------------------------------------------------------------- 1 | ## 进程调度 2 | 在Linux内核中通常有几十或者上百个进程在运行, 但个人电脑的CPU一般也只有双核或者四核, CPU的一个核在某一时刻只能运行一个进程, 所以有四个核的CPU只能同时运行4个进程, 那么Linux内核怎么可以运行比CPU核数多的进程呢? 这里就涉及到一个叫 `进程运行时间片` 的概念. 3 | 4 | `进程运行时间片` 是让每个进程在CPU中运行一段指定的时间(时间片), 当某一个进程的时间片用完后, 由Linux内核切换到其他时间片还没用完的进程运行. 进程管理结构 `task_struct` 中有个保存着时间片的字段 `counter`, 如下: 5 | ```cpp 6 | struct task_struct { 7 | ... 8 | volatile long need_resched; 9 | ... 10 | long counter; 11 | ... 12 | }; 13 | ``` 14 | 15 | ### 时钟中断 16 | 有了时间片的概念后, 进程就不能为所欲为的占用CPU了. 但这里有个问题, 就是进程的时间片不会自己减少的, 那么应该由谁来将进程的时间片减少呢? 答案就是 `时钟中断` 程序(`中断处理` 在后面会介绍, 所以这里不会对 `中断处理` 作详细的介绍). 17 | 18 | `时钟中断` 是指每隔一段相同的时间, 都会发出一个中断信号(称为一个tick, 由8253芯片触发), CPU接受到中断信号后触发内核中相应的中断处理程序. 当 `时钟中断` 发生时会调用 `timer_interrupt()` 函数来处理中断, `timer_interrupt()` 函数源码如下: 19 | ```cpp 20 | static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) 21 | { 22 | int count; 23 | 24 | write_lock(&xtime_lock); 25 | 26 | ... 27 | do_timer_interrupt(irq, NULL, regs); 28 | 29 | write_unlock(&xtime_lock); 30 | } 31 | 32 | static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) 33 | { 34 | ... 35 | do_timer(regs); 36 | ... 37 | if ((time_status & STA_UNSYNC) == 0 && 38 | xtime.tv_sec > last_rtc_update + 660 && 39 | xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 && 40 | xtime.tv_usec <= 500000 + ((unsigned) tick) / 2) { 41 | if (set_rtc_mmss(xtime.tv_sec) == 0) 42 | last_rtc_update = xtime.tv_sec; 43 | else 44 | last_rtc_update = xtime.tv_sec - 600; 45 | } 46 | ... 47 | } 48 | ``` 49 | 从上面的代码可以看到, `timer_interrupt()` 函数会调用 `do_timer_interrupt()` 函数, 而 `do_timer_interrupt()` 函数最终会调用 `do_timer()`, `do_timer()` 函数是时钟中断处理的主要逻辑, 源码如下: 50 | ```cpp 51 | void do_timer(struct pt_regs *regs) 52 | { 53 | (*(unsigned long *)&jiffies)++; 54 | #ifndef CONFIG_SMP 55 | /* SMP process accounting uses the local APIC timer */ 56 | update_process_times(user_mode(regs)); 57 | #endif 58 | mark_bh(TIMER_BH); 59 | if (TQ_ACTIVE(tq_timer)) 60 | mark_bh(TQUEUE_BH); 61 | } 62 | ``` 63 | `do_timer()` 函数主要调用 `update_process_times()` 函数更新进程的时间片, 代码如下: 64 | ```cpp 65 | void update_process_times(int user_tick) 66 | { 67 | struct task_struct *p = current; 68 | ... 69 | if (p->pid) { 70 | if (--p->counter <= 0) { 71 | p->counter = 0; 72 | p->need_resched = 1; 73 | } 74 | ... 75 | } 76 | ... 77 | } 78 | ``` 79 | 从上面的代码可以看出, 每次时钟中断发生都会将当前进程的时间片减一, 当时间片用完后会设置进程的 `need_resched` 字段为1(表示需要调用当前进程). 80 | 81 | 这里有个问题, 就是时钟中断只是把进程的 `need_resched` 字段设置为1而已, 并没有对进程进行调度啊, 那么什么时候才会对进程进行调度呢? 答案是从内核态返回到用户态的时候. 82 | 83 | 从内核态返回到用户态有几个时机: 84 | 1. 中断处理完成后返回. 85 | 2. 异常处理完成后返回. 86 | 3. 系统调用完成后返回. 87 | 88 | 譬如, 当用户进程调用系统调用返回时, 会调用以下的汇编代码: 89 | ```asm 90 | ENTRY(ret_from_sys_call) 91 | ... 92 | ret_with_reschedule: 93 | cmpl $0,need_resched(%ebx) // 判断当前进程的 need_resched 字段是否为1 94 | jne reschedule // 如果是, 就跳到reschedule处执行 95 | cmpl $0,sigpending(%ebx) 96 | jne signal_return 97 | restore_all: 98 | RESTORE_ALL // 返回到用户空间 99 | 100 | reschedule: 101 | call SYMBOL_NAME(schedule) // 调用 schedule() 函数进行进程的调度 102 | jmp ret_from_sys_call 103 | ``` 104 | 由于是汇编写的, 所以有点难懂, 所以这里我大概说说这段代码的流程: 105 | 1. 首先判断当前进程的 `need_resched` 字段是否为1. 106 | 2. 如果进程的 `need_resched` 为1, 那么就调用 `schedule()` 函数进行进程的调度. 107 | 3. 调用完 `schedule()` 函数后, 继续返回到 `ret_from_sys_call` 处执行. 108 | 109 | ### schedule()函数 110 | 现在我们来分析一下 `schedule()` 这个函数, 由于这个函数比较长, 所以我们分段来分析这个函数: 111 | ```cpp 112 | asmlinkage void schedule(void) 113 | { 114 | struct schedule_data * sched_data; 115 | struct task_struct *prev, *next, *p; 116 | struct list_head *tmp; 117 | int this_cpu, c; 118 | 119 | ... 120 | prev = current; 121 | ... 122 | 123 | spin_lock_irq(&runqueue_lock); 124 | 125 | if (prev->policy == SCHED_RR) 126 | goto move_rr_last; 127 | move_rr_back: 128 | 129 | switch (prev->state) { 130 | case TASK_INTERRUPTIBLE: 131 | if (signal_pending(prev)) { 132 | prev->state = TASK_RUNNING; 133 | break; 134 | } 135 | default: 136 | del_from_runqueue(prev); 137 | case TASK_RUNNING: 138 | } 139 | prev->need_resched = 0; 140 | ``` 141 | 上面的代码首先判断当前进程是否可中断休眠状态, 并且接受到信号, 如果是就唤醒当前进程. 如果当前进程是休眠状态, 那么就把当前进程从运行队列中删除. 接着把当前进程的 `need_resched` 字段设置为0. 142 | 143 | ```cpp 144 | repeat_schedule: 145 | next = idle_task(this_cpu); 146 | c = -1000; 147 | if (prev->state == TASK_RUNNING) 148 | goto still_running; 149 | 150 | still_running_back: 151 | list_for_each(tmp, &runqueue_head) { 152 | p = list_entry(tmp, struct task_struct, run_list); 153 | if (can_schedule(p, this_cpu)) { 154 | int weight = goodness(p, this_cpu, prev->active_mm); 155 | if (weight > c) 156 | c = weight, next = p; 157 | } 158 | } 159 | ``` 160 | 这段代码是便利运行队列中的所有进程, 然后通过调用 `goodness()` 函数来计算每个进程的运行优先级, 值越大就越先被运行, 找到的进程会被保存到 `next` 变量中. 我们来看看 `goodness()` 的计算过程: 161 | ```cpp 162 | static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm) 163 | { 164 | int weight; 165 | 166 | weight = -1; 167 | if (p->policy & SCHED_YIELD) 168 | goto out; 169 | 170 | if (p->policy == SCHED_OTHER) { // 普通进程 171 | weight = p->counter; 172 | if (!weight) 173 | goto out; 174 | 175 | if (p->mm == this_mm || !p->mm) 176 | weight += 1; 177 | weight += 20 - p->nice; 178 | goto out; 179 | } 180 | 181 | weight = 1000 + p->rt_priority; 182 | out: 183 | return weight; 184 | } 185 | ``` 186 | 计算过程很简单, 首先进程在Linux内核中分为实时进程和普通进程, 普通进程的计算方法就是: 187 | 188 | 进程时间片 + 20 - 进程的友好值 189 | 190 | 而实时进程的计算方法是: 191 | 192 | 1000 + 实时进程的优先级 193 | 194 | 我们继续来分析 `schedule()` 函数的余下部分: 195 | ```cpp 196 | prepare_to_switch(); 197 | { 198 | // 切换进程的内存空间 199 | struct mm_struct *mm = next->mm; 200 | struct mm_struct *oldmm = prev->active_mm; 201 | if (!mm) { 202 | if (next->active_mm) BUG(); 203 | next->active_mm = oldmm; 204 | atomic_inc(&oldmm->mm_count); 205 | enter_lazy_tlb(oldmm, next, this_cpu); 206 | } else { 207 | if (next->active_mm != mm) BUG(); 208 | switch_mm(oldmm, mm, next, this_cpu); 209 | } 210 | 211 | if (!prev->mm) { 212 | prev->active_mm = NULL; 213 | mmdrop(oldmm); 214 | } 215 | } 216 | 217 | switch_to(prev, next, prev); 218 | __schedule_tail(prev); 219 | ``` 220 | 找到合适的进程后, 接下来就是进行调度工作了. 调度工作首先调用 `switch_mm()` 函数来把旧进程的内存空间切换到新进程的内存空间, 切换内存空间主要是通过把 `cr3` 寄存器的值设置为新进程页目录的地址. 接着调用 `switch_to()` 函数进行进程的切换, 我们来看看 `switch_to()` 函数的实现: 221 | ```cpp 222 | #define switch_to(prev,next,last) do { \ 223 | asm volatile("pushl %%esi\n\t" \ 224 | "pushl %%edi\n\t" \ 225 | "pushl %%ebp\n\t" \ 226 | "movl %%esp,%0\n\t" /* save ESP */ \ 227 | "movl %3,%%esp\n\t" /* restore ESP */ \ 228 | "movl $1f,%1\n\t" /* save EIP */ \ 229 | "pushl %4\n\t" /* restore EIP */ \ 230 | "jmp __switch_to\n" \ 231 | "1:\t" \ 232 | "popl %%ebp\n\t" \ 233 | "popl %%edi\n\t" \ 234 | "popl %%esi\n\t" \ 235 | :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \ 236 | "=b" (last) \ 237 | :"m" (next->thread.esp),"m" (next->thread.eip), \ 238 | "a" (prev), "d" (next), \ 239 | "b" (prev)); \ 240 | } while (0) 241 | ``` 242 | 又是一段难懂的汇编, 而且是比汇编更难懂的GCC嵌入汇编. 为了让大家不陷入痛苦之中, 这里主要介绍一下这段代码的作用. 在 `进程管理` 一节中, 我们介绍过进程管理结构 `task_struct` 是放置在内核栈的底部的, 所以要切换进程只需要切换内核栈即可. 这里正是通过这个方法来切换进程的, 我们看到的 `movl %3, %%esp` 这行代码就是切换到新进程的内核栈. 243 | 244 | 当调用完 `schedule()` 函数后, 现在通过 `get_current()` 函数获取到的当前进程就是我们刚才切换的新进程了, 至此进程切换完成. 245 | -------------------------------------------------------------------------------- /rcu.md: -------------------------------------------------------------------------------- 1 | ## RCU原理与实现 2 | 3 | Linux内核有多种锁机制,比如 `自旋锁`、`信号量` 和 `读写锁` 等。不同的场景使用不同的锁,如在读多写少的场景可以使用读写锁,而在锁粒度比较小的场景可以使用自旋锁。 4 | 5 | 本文主要介绍一种比较有趣的锁,名为:`RCU`,`RCU` 是 `Read Copy Update` 这几个单词的缩写,中文翻译是 `读 复制 更新`,顾名思义这个锁只需要三个步骤就能完成:__1) 读__、 __2) 复制__、 __3) 更新__。但是往往现实并不是那么美好的,这个锁机制要比这个名字复杂很多。 6 | 7 | 我们先来介绍一下 `RCU` 的使用场景,`RCU` 的特点是:多个 `reader(读者)` 可以同时读取共享的数据,而 `updater(更新者)` 更新共享的数据时需要复制一份,然后对副本进行修改,修改完把原来的共享数据替换成新的副本,而对旧数据的销毁(释放)等待到所有读者都不再引用旧数据时进行。 8 | 9 | `RCU` 与 `读写锁` 一样可以支持多个读者同时访问临界区,并且比 `读写锁` 更为轻量,性能更好。 10 | 11 | ### RCU 原理 12 | 13 | 分析下面代码存在的问题(例子参考:《深入理解并行编程》): 14 | ```cpp 15 | struct foo { 16 | int a; 17 | char b; 18 | long c; 19 | }; 20 | 21 | DEFINE_SPINLOCK(foo_mutex); 22 | 23 | struct foo *gbl_foo; 24 | 25 | void foo_read(void) 26 | { 27 | foo *fp = gbl_foo; 28 | if (fp != NULL) 29 | dosomething(fp->a, fp->b, fp->c); 30 | } 31 | 32 | void foo_update(foo* new_fp) 33 | { 34 | spin_lock(&foo_mutex); 35 | foo *old_fp = gbl_foo; 36 | gbl_foo = new_fp; 37 | spin_unlock(&foo_mutex); 38 | free(old_fp); 39 | } 40 | ``` 41 | 假如有线程A和线程B同时执行 `foo_read()`,而另线程C执行 `foo_update()`,那么会出现以下情况: 42 | 1) 线程A和线程B同时读取到旧的 `gbl_foo` 的指针。 43 | 2) 线程A和线程B同时读取到新的 `gbl_foo` 的指针。 44 | 3) 线程A和线程B有一个读取到新的 `gbl_foo` 的指针,另外一个读取到旧的 `gbl_foo` 的指针。 45 | 46 | 如果线程A或线程B在读取旧的 `gbl_foo` 数据还没完成时,线程C释放了旧的 `gbl_foo` 指针,那么将会导致程序奔溃。 47 | 48 | 为了解决这个问题,`RCU` 提出 `宽限期` 的概念。 49 | 50 | `宽限期` 是指线程引用旧数据结束前的一段时间,如下图(图片来源:[RCU原理分析](https://www.cnblogs.com/chaozhu/p/6265740.html)): 51 | 52 | ![rcu-grace-period](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/rcu-grace-period.png) 53 | 54 | 如上图所示,线程1、线程2和线程5在删除(替换)旧数据前已经在使用旧数据,所以必须等待它们不再引用旧数据时才能对旧数据进行销毁,这个等待的时间就是 `宽限期`。由于线程3、线程4和线程6使用的是新数据(已经被替换成新的指针),所以不需要等到它们。 55 | 56 | > 由于 `RCU` 的读者需要禁止抢占,所以对于 `RCU` 来说,`宽限期` 是所有CPU都进行一次用户态调度的时间。 57 | 58 | 上面的这段话是什么意思? 59 | 60 | 禁止抢占代表CPU不能调度到其他线程,CPU只能等待当前线程离开临界区(不再引用旧数据)才能进行调度。也就是说,如果CPU进行了一次调度,说明线程已经不再引用旧数据。如果所有CPU都进行了一次调度,就说明已经没有线程引用旧数据,那么就可以对旧数据进行销毁。 61 | 62 | ### RCU 使用 63 | 64 | > 本文使用的是Linux2.6.0版本的内核。 65 | 66 | #### RCU 读者 67 | 68 | 要做Linux内核中使用 `RCU`,读者需要使用 `rcu_read_lock()` 来对临界区进行 “上锁”,本质上 `rcu_read_lock()` 就是禁止CPU进行抢占,如下代码: 69 | ```cpp 70 | #define rcu_read_lock() preempt_disable() // 禁止抢占 71 | ``` 72 | 73 | 当不再引用数据时,需要使用 `rcu_read_unlock()` 对临界区进行 “解锁”,本质上 `rcu_read_unlock()` 就是开启抢占,如下代码: 74 | ```cpp 75 | #define rcu_read_unlock() preempt_enable() // 开启抢占 76 | ``` 77 | 78 | 例子如下: 79 | ```cpp 80 | void foo_read(void) 81 | { 82 | rcu_read_lock(); // 上锁 83 | 84 | foo *fp = gbl_foo; 85 | if (fp != NULL) 86 | dosomething(fp->a, fp->b, fp->c); 87 | 88 | rcu_read_unlock(); // 解锁 89 | } 90 | ``` 91 | 92 | #### RCU 更新者 93 | 94 | 对于更新者,有两种方式: 95 | 1. 调用 `call_rcu()` 异步销毁,由内核自动触发,非阻塞。 96 | 2. 调用 `synchronize_kernel()` 同步销毁,等待宽限期过后,阻塞。 97 | 98 | 例子如下: 99 | ```cpp 100 | void foo_update(foo* new_fp) 101 | { 102 | spin_lock(&foo_mutex); 103 | foo *old_fp = gbl_foo; 104 | gbl_foo = new_fp; 105 | spin_unlock(&foo_mutex); 106 | 107 | synchronize_kernel(); // 等待宽限期过后 108 | 109 | free(old_fp); 110 | } 111 | ``` 112 | 113 | ### RCU 实现 114 | 115 | 在介绍 `RCU` 实现前,先要介绍两个重要的数据结构:`rcu_ctrlblk` 和 `rcu_data`。`rcu_ctrlblk` 结构用于记录当前系统宽限期批次信息,而 `rcu_data` 结构用于记录每个CPU的调度次数与需要延迟执行的函数列表。 116 | 117 | #### rcu_ctrlblk 结构 118 | ```cpp 119 | struct rcu_ctrlblk { 120 | spinlock_t mutex; 121 | long curbatch; 122 | long maxbatch; 123 | cpumask_t rcu_cpu_mask; 124 | }; 125 | ``` 126 | rcu_ctrlblk 结构各个字段的作用: 127 | 1. `mutex`:由于 `rcu_ctrlblk` 结构是全局变量,所需通过这个锁来进行同步。 128 | 2. `curbatch`:当前批次数(`RCU` 的实现把每个宽限期当成是一个批次)。 129 | 3. `maxbatch`:系统最大批次数,如果 `maxbatch` 大于 `curbatch` 说明还有没有完成的批次。 130 | 4. `rcu_cpu_mask`:当前批次还没有进行调度的CPU列表,因为前面说过,必须所有CPU进行一次调度宽限期才能算结束。 131 | 132 | #### rcu_data 结构 133 | ```cpp 134 | struct rcu_data { 135 | long qsctr; /* User-mode/idle loop etc. */ 136 | long last_qsctr; /* value of qsctr at beginning */ 137 | /* of rcu grace period */ 138 | long batch; /* Batch # for current RCU batch */ 139 | struct list_head nxtlist; 140 | struct list_head curlist; 141 | }; 142 | ``` 143 | 每个CPU都有一个 `rcu_data` 结构,其各个字段的作用如下: 144 | 1. `qsctr`:当前CPU调度的次数。 145 | 2. `last_qsctr`:用于记录宽限期开始时的调度次数,如果 `qsctr` 比它大,说明当前CPU的宽限期已经结束。 146 | 3. `batch`:用于记录当前CPU的批次数。 147 | 4. `nxtlist`:下一次批次要执行的函数列表。 148 | 5. `curlist`:当前批次要执行的函数列表。 149 | 150 | #### 时钟中断 151 | 152 | 每次时钟中断都会触发调用 `scheduler_tick()` 函数,而 `scheduler_tick()` 函数会调用 `rcu_check_callbacks()` 函数,调用链:`scheduler_tick() -> rcu_check_callbacks()`。`rcu_check_callbacks()` 函数实现如下: 153 | ```cpp 154 | void rcu_check_callbacks(int cpu, int user) 155 | { 156 | if (user || (idle_cpu(cpu) && !in_softirq() && hardirq_count() <= (1 << HARDIRQ_SHIFT))) { 157 | RCU_qsctr(cpu)++; 158 | } 159 | tasklet_schedule(&RCU_tasklet(cpu)); // 这里会调用 rcu_process_callbacks() 函数 160 | } 161 | ``` 162 | 这个函数主要做两件事情: 163 | 1. 判断当前进程是否处于用户态,如果是就对当前CPU的 `rcu_data` 结构的 `qsctr` 字段进行加一操作。前面说过,如果进程处于用户态,代表内核已经推出了临界区,也就是说不再引用旧数据。 164 | 2. 调用 `rcu_process_callbacks()` 函数继续进行其他工作。 165 | 166 | 我们解析来分析 `rcu_process_callbacks()` 函数: 167 | ```cpp 168 | static void rcu_process_callbacks(unsigned long unused) 169 | { 170 | int cpu = smp_processor_id(); // 获取CPU ID 171 | LIST_HEAD(list); 172 | 173 | // 1. 如果CPU的当前批次有要执行的函数列表, 并且全局批次数大于CPU的当前批次数, 174 | // 那么把当前批次要执行的函数列表移动到list列表中, 并且清空当前批次要执行的函数列表。 175 | if (!list_empty(&RCU_curlist(cpu)) && 176 | rcu_batch_after(rcu_ctrlblk.curbatch, RCU_batch(cpu))) { 177 | list_splice(&RCU_curlist(cpu), &list); 178 | INIT_LIST_HEAD(&RCU_curlist(cpu)); 179 | } 180 | 181 | // 2. 如果CPU下一个批次要执行的函数列表不为空, 并且当前批次要执行的函数列表为空, 182 | // 那么把下一个批次要执行的函数列表移动到当前批次要执行的函数列表, 并且清空下一个批次要执行的函数列表 183 | local_irq_disable(); 184 | if (!list_empty(&RCU_nxtlist(cpu)) && list_empty(&RCU_curlist(cpu))) { 185 | list_splice(&RCU_nxtlist(cpu), &RCU_curlist(cpu)); 186 | INIT_LIST_HEAD(&RCU_nxtlist(cpu)); 187 | local_irq_enable(); 188 | 189 | /* 190 | * start the next batch of callbacks 191 | */ 192 | spin_lock(&rcu_ctrlblk.mutex); 193 | RCU_batch(cpu) = rcu_ctrlblk.curbatch + 1; // 把CPU当前批次设置为全局批次数加一 194 | rcu_start_batch(RCU_batch(cpu)); // 开始新的批次周期(宽限期) 195 | spin_unlock(&rcu_ctrlblk.mutex); 196 | } else { 197 | local_irq_enable(); 198 | } 199 | 200 | // 调用 rcu_check_quiescent_state() 函数检测所有CPU是否都退出临界区(宽限期结束),如果是则对全局批次数进行加一操作。 201 | rcu_check_quiescent_state(); 202 | 203 | // 执行CPU当前批次函数列表的函数 204 | if (!list_empty(&list)) 205 | rcu_do_batch(&list); 206 | } 207 | ``` 208 | `rcu_process_callbacks()` 函数主要完成4件事情: 209 | 1. 如果CPU的当前批次(`rcu_data` 结构的 `curlist` 字段)有要执行的函数列表(一般都是销毁旧数据的函数),并且全局批次数大于CPU的当前批次数。那么把列表移动到 `list` 中,并且清空当前批次的函数列表。 210 | 2. 如果CPU的下一次批次(`rcu_data` 结构的 `nxtlist` 字段)有要执行的函数列表,并且当前批次要执行的函数列表为空。那么把其移动到当前批次的函数列表,并清空下一次批次的函数列表。接着把CPU的当前批次数设置为全局批次数加一,然后调用 `rcu_start_batch()` 函数开始一个新的批次周期(宽限期)。 211 | 3. 调用 `rcu_check_quiescent_state()` 函数检测所有CPU是否都退出临界区(宽限期结束),如果是则对全局批次数进行加一操作。 212 | 4. 如果CPU当前批次执行的函数列表不为空,那么就执行函数列表中的函数。 213 | 214 | 从上面的代码可知,每个CPU的当前批次要执行的函数列表必须等待全局批次数大于当前CPU的批次数才能被执行。全局批次数由 `rcu_check_quiescent_state()` 函数推动,我们来看看 `rcu_check_quiescent_state()` 函数的实现: 215 | ```cpp 216 | static void rcu_check_quiescent_state(void) 217 | { 218 | int cpu = smp_processor_id(); 219 | 220 | // 如果当前CPU不在rcu_cpu_mask列表中,表示当前CPU已经经历了一次调用,所有不需要再执行下去 221 | if (!cpu_isset(cpu, rcu_ctrlblk.rcu_cpu_mask)) 222 | return; 223 | 224 | if (RCU_last_qsctr(cpu) == RCU_QSCTR_INVALID) { // 宽限期开始记录点 225 | RCU_last_qsctr(cpu) = RCU_qsctr(cpu); 226 | return; 227 | } 228 | if (RCU_qsctr(cpu) == RCU_last_qsctr(cpu)) // 还没有被调度过,直接返回 229 | return; 230 | 231 | spin_lock(&rcu_ctrlblk.mutex); 232 | if (!cpu_isset(cpu, rcu_ctrlblk.rcu_cpu_mask)) 233 | goto out_unlock; 234 | 235 | // 当前CPU已经被调度了,清空其所在rcu_cpu_mask列表的标志位 236 | cpu_clear(cpu, rcu_ctrlblk.rcu_cpu_mask); 237 | RCU_last_qsctr(cpu) = RCU_QSCTR_INVALID; 238 | 239 | // 如果还有CPU没有经历一次调度,直接返回 240 | if (!cpus_empty(rcu_ctrlblk.rcu_cpu_mask)) 241 | goto out_unlock; 242 | 243 | // 执行到这里表示所有CPU都执行了一次调度 244 | rcu_ctrlblk.curbatch++; // 全局批次数加一 245 | rcu_start_batch(rcu_ctrlblk.maxbatch); // 开始下一轮批次 246 | 247 | out_unlock: 248 | spin_unlock(&rcu_ctrlblk.mutex); 249 | } 250 | ``` 251 | `rcu_check_quiescent_state()` 函数主要做以下几件事情: 252 | 1. 判断当前CPU是否已经经历一次调度,如果是,就把其从 `rcu_ctrlblk.rcu_cpu_mask` 列表中清除。 253 | 2. 如果所有CPU都经历了一次调度,那么就对全局批次数进行加一操作。 254 | 3. 开始下一轮的批次周期。 255 | 256 | 从上面的分析可以看出,推动 `RCU` 对旧数据进行销毁的动力是时钟中断。 257 | 258 | `call_rcu()` 函数会把销毁函数添加到当前CPU的 `nxtlist` 函数列表中,代码如下: 259 | ```cpp 260 | void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg) 261 | { 262 | int cpu; 263 | unsigned long flags; 264 | 265 | head->func = func; 266 | head->arg = arg; 267 | local_irq_save(flags); 268 | cpu = smp_processor_id(); 269 | list_add_tail(&head->list, &RCU_nxtlist(cpu)); // 添加到CPU的nxtlist列表中 270 | local_irq_restore(flags); 271 | } 272 | ``` 273 | 随着时钟中断的推动,`nxtlist` 函数列表会移动到 `curlist` 函数列表中,然后会在适当的时机执行 `curlist` 函数列表中的函数。 274 | -------------------------------------------------------------------------------- /seqlock.md: -------------------------------------------------------------------------------- 1 | Linux 内核有非常多的锁机制,如:自旋锁、读写锁、信号量和 RCU 锁等。本文介绍一种和读写锁比较相似的锁机制:顺序锁(seqlock)。 2 | 3 | 顺序锁与读写锁一样,都是针对多读少写且快速处理的锁机制。而顺序锁和读写锁的区别就在于:读写锁的读锁会阻塞写锁,而顺序锁的读锁不会阻塞写锁。 4 | 5 | ## 读锁原理 6 | 7 | 为了让读锁不阻塞写锁,读锁并不会真正进行上锁操作。那么读锁是如何避免在读取临界区数据时,数据被其他进程修改了? 8 | 9 | 为了解决这个问题,顺序锁使用了一种类似于版本号的机制:`序号`。序号是一个只增不减的计数器,可以从顺序锁对象的定义看出,如下代码所示: 10 | 11 | ```c 12 | typedef struct { 13 | struct seqcount seqcount; // 序号 14 | spinlock_t lock; // 自旋锁,写锁上锁时使用 15 | } seqlock_t; 16 | ``` 17 | 18 | 在读取临界区数据前,首先需要调用 `read_seqbegin()` 函数来获取读锁,`read_seqbegin()` 函数的核心逻辑是读取顺序锁的序号。代码如下所示: 19 | 20 | ```c 21 | static inline unsigned read_seqbegin(const seqlock_t *sl) 22 | { 23 | unsigned ret; 24 | 25 | repeat: 26 | // 读取顺序锁的序号 27 | ret = sl->sequence; 28 | 29 | // 如果序号是单数,需要重新获取 30 | if (unlikely(ret & 1)) { 31 | ... 32 | goto repeat; 33 | } 34 | ... 35 | return ret; 36 | } 37 | ``` 38 | 39 | 从上面的代码可以看出,`read_seqbegin()` 函数只获取顺序锁的序号,并不会进行上锁操作,所以读锁并不会阻塞写锁。 40 | 41 | > 注意:序号是单数时需要重新获取的原因,会在分析写锁实现原理时说明。 42 | 43 | 既然读锁并不会进行上锁操作,如果在读取临界区数据时,数据被修改了怎么办呢?答案就是:在退出临界区时,比较一下当前顺序锁的序号跟之前读取的序号是否一致。如果一致表示数据没有被修改,否则说明数据已经被修改。如果数据被修改了,那么需要重新读取临界区的数据。 44 | 45 | 比较序号是否一致可以使用 `read_seqretry()` 函数,所以读锁的正确用法如下代码所示: 46 | 47 | ```c 48 | do { 49 | // 获取顺序锁序号 50 | unsigned seq = read_seqbegin(&seqlock); 51 | // 读取临界区数据 52 | ... 53 | } while (read_seqretry(&seqlock, seq)); // 对比序号是否一致? 54 | ``` 55 | 56 | `read_seqretry()` 函数的实现非常简单,如下所示: 57 | 58 | ```c 59 | static inline unsigned 60 | read_seqretry(const seqlock_t *sl, unsigned start) 61 | { 62 | ... 63 | return sl->sequence != start; 64 | } 65 | ``` 66 | 67 | 从上面代码可以看出,`read_seqretry()` 函数只是简单比较当前序号与之前读取到的序号是否一致。 68 | 69 | ## 写锁原理 70 | 71 | 从上面的分析可知,读锁是通过对比前后序号是否一致来判断数据是否被修改的。那么序号在什么时候被修改呢?答案就是:获取写锁时。 72 | 73 | 获取写锁是通过 `write_seqlock()` 函数来实现的,其实现也比较简单,代码如下所示: 74 | 75 | ```c 76 | static inline void write_seqlock(seqlock_t *sl) 77 | { 78 | spin_lock(&sl->lock); 79 | 80 | sl->sequence++; 81 | ... 82 | } 83 | ``` 84 | 85 | `write_seqlock()` 函数首先会获取自旋锁(所以写锁与写锁之间是互斥的),然后对序号进行加一操作。所以,在修改临界区数据前,写锁先会增加序号的值,这样就会导致读锁前后两次获取的序号不一致。我们可以用下图来说明这种情况: 86 | 87 | ![image-20230903130923123](./images/seqlock.png) 88 | 89 | 可以看出,当在读临界区前后获取的序号值不一致时,就表示数据已经被修改,这时就需要重新读取被修改后的数据。 90 | 91 | 写锁解锁也很简单,代码如下: 92 | 93 | ```c 94 | static inline void write_sequnlock(seqlock_t *sl) 95 | { 96 | ... 97 | s->sequence++; 98 | spin_unlock(&sl->lock); 99 | } 100 | ``` 101 | 102 | 解锁也需要对序号进行加一操作,然后释放自旋锁。 103 | 104 | 由于 `write_seqlock()` 函数与 `write_sequnlock()` 函数都会对序号进行加一操作,所以解锁后,序号的值必定为双数。 105 | 106 | 我们在分析读锁时看到,如果序号是单数时会重新获取序号,直到序号为双数为止。这是因为序号单数时,表示正在更新数据。此时读取临界区的值是没有意义的,所以需要等到更新完毕再读取。 107 | 108 | -------------------------------------------------------------------------------- /seqlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/339f80468128b2bbaed1160571337aa3c82ae01e/seqlock.png -------------------------------------------------------------------------------- /syscall.md: -------------------------------------------------------------------------------- 1 | # 系统调用 2 | 3 | ## 一、什么是系统调用 4 | 5 | `系统调用` 跟用户自定义函数一样也是一个函数,不同的是 `系统调用` 运行在内核态,而用户自定义函数运行在用户态。由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运行在内核态,所以操作系统必须提供一种能够进入内核态的方式,`系统调用` 就是这样的一种机制。 6 | 7 | `系统调用` 是 Linux 内核提供的一段代码(函数),其实现了一些特定的功能,用户可以通过 `int 0x80` 中断(x86 CPU)或者 `syscall` 指令(x64 CPU)来调用 `系统调用`。 8 | 9 | ## 二、进入系统调用 10 | 11 | > 本文主要介绍的是 x86 CPU 进入系统调用的方式 12 | 13 | Linux 提供了 `int 0x80` 中断来让用户程序进入 `系统调用`,我们来看看 Linux 对 `int 0x80` 中断的处理初始化过程: 14 | 15 | ```c 16 | void __init trap_init(void) 17 | { 18 | ... 19 | set_system_gate(SYSCALL_VECTOR, &system_call); 20 | ... 21 | } 22 | ``` 23 | 24 | 系统初始化时,会在 `trap_init()` 函数中对 `int 0x80` 中断处理进行初始化,设置其中断处理过程入口为 `system_call`。`system_call` 是一段由汇编语言编写的代码,我们看看关键部分,如下: 25 | ```asm 26 | ENTRY(system_call) 27 | ... 28 | call *SYMBOL_NAME(sys_call_table)(,%eax,4) 29 | movl %eax,EAX(%esp) # save the return value 30 | ... 31 | ``` 32 | 33 | 我们把上面的汇编改写成 C 代码如下: 34 | 35 | ```c 36 | void system_call() 37 | { 38 | ... 39 | // 变量 eax 代表 eax 寄存器的值 40 | syscall = sys_call_table[eax]; 41 | eax = syscall(); 42 | ... 43 | } 44 | ``` 45 | 46 | `sys_call_table` 变量是一个数组,数组的每一个元素代表一个 `系统调用` 的入口,其定义如下(在文件 arch/i386/kernel/entry.S 中): 47 | 48 | ```asm 49 | .data 50 | ENTRY(sys_call_table) 51 | .long SYMBOL_NAME(sys_ni_syscall) 52 | .long SYMBOL_NAME(sys_exit) 53 | .long SYMBOL_NAME(sys_fork) 54 | .long SYMBOL_NAME(sys_read) 55 | .long SYMBOL_NAME(sys_write) 56 | .long SYMBOL_NAME(sys_open) 57 | .long SYMBOL_NAME(sys_close) 58 | ... 59 | ``` 60 | 61 | 翻译成 C 代码如下: 62 | 63 | ```c 64 | long sys_call_table[] = { 65 | sys_ni_syscall, 66 | sys_exit, 67 | sys_fork, 68 | sys_read, 69 | sys_write, 70 | sys_open, 71 | sys_close, 72 | ... 73 | }; 74 | ``` 75 | 76 | 用户调用 `系统调用` 时,通过向 `eax` 寄存器写入要调用的 `系统调用` 编号,这个编号就是 `sys_call_table` 数组的下标。 `system_call` 过程获取 `eax` 寄存器的值,然后通过 `eax` 寄存器的值找到要调用的 `系统调用` 入口,并且进行调用。调用完成后,`系统调用` 会把返回值保存到 `eax` 寄存器中。 77 | 78 | 原理如下图(图片来源 https://developer.ibm.com/zh/technologies/linux/tutorials/l-system-calls/ ): 79 | 80 | ![system_call](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/system_call.gif) 81 | 82 | ## 三、系统调用实现 83 | 84 | 当用户要调用 `系统调用` 时,需要通过向 `eax` 寄存器写入要调用的 `系统调用` 编号。因为 `用户态` 和 `内核态` 使用的栈不同,而调用 `系统调用` 是在用户态调用的,而进入 `系统调用` 后会变成内核态,所以参数就不能通过栈来传递。Linux 使用寄存器来传递参数,参数与寄存器的关系如下: 85 | 86 | * 第1个参数放置在 `ebx` 寄存器。 87 | * 第2个参数放置在 `ecx` 寄存器。 88 | * 第3个参数放置在 `edx` 寄存器。 89 | * 第4个参数放置在 `esi` 寄存器。 90 | * 第5个参数放置在 `edi` 寄存器。 91 | * 第6个参数放置在 `ebp` 寄存器。 92 | 93 | 而 Linux 进入中断处理程序时,会把这些寄存器的值保存到内核栈中,这样 `系统调用` 就能通过内核栈来获取到参数。 94 | 95 | 下面我们通过 `sys_open()` 系统调用来说明一下 `系统调用` 的运作方式,`sys_open()` 实现如下: 96 | 97 | ```c 98 | asmlinkage long sys_open(const char *filename, int flags, int mode) 99 | { 100 | ... 101 | } 102 | ``` 103 | 104 | 一般 `系统调用` 都需要使用 `asmlinkage` 编译选项,`asmlinkage` 编译选项是告诉编译器从栈中读取参数,其实际是封装了 GCC 的编译选项,如下: 105 | 106 | ```c 107 | #define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0))) 108 | ``` 109 | 110 | `__attribute__((regparm(0)))` 就是告诉 GCC 所有参数都从栈中读取,而 Linux 进入中断处理上下文时,会把 `ebx`、`ecx`、`edx`、`esi`、`edi`、`ebp` 寄存器的值保存到内核栈中,那么 `系统调用` 就可以从内核栈获取到参数的值。 111 | 112 | 但由于寄存器只能传递 32 位的整型值(x86 CPU),所以参数一般只能传递指针或者整型的数值,如果要获取指针对应结构的数据,就必须通过从用户空间复制到内核空间,如 `sys_open()` 系统调用获取要打开的文件路径: 113 | 114 | ```c 115 | asmlinkage long sys_open(const char *filename, int flags, int mode) 116 | { 117 | char * tmp; 118 | ... 119 | tmp = getname(filename); 120 | ... 121 | } 122 | ``` 123 | 124 | `getname()` 函数就是用于从用户空间复制数据到内核空间。 125 | -------------------------------------------------------------------------------- /tun-tap-principle.md: -------------------------------------------------------------------------------- 1 | # TUN/TAP设备原理与实现 2 | 3 | `TUN/TAP设备` 是Linux下的一种虚拟设备,相对虚拟设备来说,一般计算机所使用的网卡就是物理设备。物理设备有发送和接收网络数据包的能力,而虚拟设备的作用就是模拟物理设备,提供特殊的发送和接收功能。 4 | 5 | 由于 `TUN/TAP设备` 是虚拟的设备,所以不能像网卡一样从网络中接收数据包(因为虚拟设备没有接口),那么 `TUN/TAP设备` 从哪里读取数据,又把数据发送到哪里去呢?下面通过一张图来阐述 `TUN/TAP设备` 的工作方式 (图片来源于https://www.ibm.com/developerworks/cn/linux/l-tuntap/index.html ): 6 | 7 | ![TUN/TAP](https://www.ibm.com/developerworks/cn/linux/l-tuntap/images/image002.jpg) 8 | 9 | 上图的右下角是物理网卡,它能从 `物理链路` 读取和发送数据。 10 | 11 | 而左下角是 `TUN/TAP设备`,可以看出 `TUN/TAP设备` 连接着 `字符驱动` 和 `TCP/IP协议栈`,这就是说可以通过 `read()` 和 `write()` 系统调用来对 `TUN/TAP设备` 进行数据的读写操作,而读写的数据会经过 `TCP/IP协议栈`。 12 | 13 | 其实 `TUN/TAP设备` 并没有对外网发送数据这个功能,如果需要对外网发送数据,必须通过路由转发才能实现。也就是通过构建发送到外网的数据包,然后通过路由转发来发送出去。 14 | 15 | ## TUN/TAP设备使用 16 | 17 | 在使用 TUN/TAP 设备前,先确保已经装载 TUN/TAP 模块并建立设备文件: 18 | 19 | ```shell 20 | root@vagrant]# modprobe tun 21 | root@vagrant]# mknod /dev/net/tun c 10 200 22 | ``` 23 | 24 | 参数 `c` 表示是字符设备, 10 和 200 分别是主设备号和次设备号。 25 | 26 | 这样,我们就可以在程序中使用该驱动了。 27 | 28 | 下面例子主要介绍怎么创建和启动一个 TUN/TAP 设备(摘自openvpn开源项目 [http://openvpn.sourceforge.net](http://openvpn.sourceforge.net/),tun.c文件): 29 | 30 | ```c 31 | int open_tun(const char *dev, char *actual, int size) 32 | { 33 | struct ifreq ifr; 34 | int fd; 35 | char *device = "/dev/net/tun"; 36 | 37 | if ((fd = open(device, O_RDWR)) < 0) // 创建描述符 38 | msg(M_ERR, "Cannot open TUN/TAP dev %s", device); 39 | 40 | memset(&ifr, 0, sizeof (ifr)); 41 | 42 | ifr.ifr_flags = IFF_NO_PI; 43 | if (!strncmp(dev, "tun", 3)) { 44 | ifr.ifr_flags |= IFF_TUN; 45 | } else if (!strncmp(dev, "tap", 3)) { 46 | ifr.ifr_flags |= IFF_TAP; 47 | } else { 48 | msg(M_FATAL, "I don't recognize device %s as a TUN or TAP device", dev); 49 | } 50 | 51 | if (strlen(dev) > 3) 52 | strncpy(ifr.ifr_name, dev, IFNAMSIZ); 53 | 54 | if (ioctl(fd, TUNSETIFF, (void *) &ifr) < 0) // 打开虚拟网卡设备 55 | msg(M_ERR, "Cannot ioctl TUNSETIFF %s", dev); 56 | 57 | set_nonblock(fd); 58 | 59 | msg(M_INFO, "TUN/TAP device %s opened", ifr.ifr_name); 60 | 61 | strncpynt(actual, ifr.ifr_name, size); 62 | 63 | return fd; 64 | } 65 | ``` 66 | 67 | `open_tun()` 函数会创建并启动一个 TUN/TAP 设备文件句柄,然后就可以通过对这个文件句柄进行 `read()` 和 `write()` 系统调用来读写此 TUN/TAP 设备。 68 | 69 | ## TUN/TAP 设备实现 70 | 71 | 上面主要介绍了 TUN/TAP 设备的原理与使用方式,接下来主要分析 TUN/TAP 设备的实现过程。 72 | 73 | ### 1. 打开 TUN/TAP 设备 74 | 75 | 当调用 `open()` 系统调用打开一个 TUN/TAP 设备时,会触发调用内核态的 `tun_chr_open()` 函数,其实现如下: 76 | 77 | ```c 78 | static int tun_chr_open(struct inode *inode, struct file *file) 79 | { 80 | struct tun_struct *tun = NULL; 81 | 82 | // 申请一个 TUN 对象结构 83 | tun = kmalloc(sizeof(struct tun_struct), GFP_KERNEL); 84 | if (tun == NULL) 85 | return -ENOMEM; 86 | 87 | memset(tun, 0, sizeof(struct tun_struct)); 88 | file->private_data = tun; // 将 TUN 对象结构与文件描述符绑定 89 | 90 | skb_queue_head_init(&tun->txq); // 初始化txq队列 91 | init_waitqueue_head(&tun->read_wait); // 初始化等待队列 92 | 93 | sprintf(tun->name, "tunX"); // 设置设备的名称 94 | 95 | tun->dev.init = tun_net_init; // 设置启动TUN设备的初始化函数 96 | tun->dev.priv = tun; 97 | 98 | return 0; 99 | } 100 | ``` 101 | 102 | `tun_chr_open()` 函数主要完成以下几个步骤: 103 | 104 | * 调用 `kmalloc()` 函数创建一个 `tun` 对象结构。 105 | * 将 `TUN` 对象结构与文件描述符绑定。 106 | * 初始化 `TUN` 对象的 `txq` 队列和等待队列。 107 | * 设置 `TUN` 设备的名称。 108 | * 设置启动 `TUN` 设备的初始化函数。 109 | 110 | 我们先来看看 `TUN` 对象结构的定义: 111 | 112 | ```c 113 | struct tun_struct { 114 | char name[8]; // TUN设备的名字 115 | unsigned long flags; // 设备类型: TUN或者TAP 116 | struct fasync_struct *fasync; 117 | wait_queue_head_t read_wait; // 等待此设备可读的进程队列 118 | struct net_device dev; // TUN设备关联的网络设备对象 119 | struct sk_buff_head txq; // 数据队列(接收到的数据包会保存到这里) 120 | ... 121 | }; 122 | ``` 123 | 124 | -------------------------------------------------------------------------------- /unix-domain-sockets.md: -------------------------------------------------------------------------------- 1 | ## Unix Domain Sockets使用 2 | 上一章介绍了Socket接口层的实现,接下来我们将会介绍具体的协议层实现,这一章将会介绍用于进程间通信的 `Unix Doamin Sockets` 的实现。要使用 `Unix Domain Sockets` 需要在创建socket时为 `family` 参数传入 `AF_UNIX`,如下代码: 3 | ```cpp 4 | fd = socket(AF_UNIX, SOCK_STREAM, 0); 5 | ``` 6 | 这样就可以创建一个类型为 `Unix Domain Sockets` 的socket描述符,如果我们编写的是服务端程序,那就需要在调用 `bind()` 函数时为其指定一个唯一的文件路径,客户端就可以通过这个文件路径来连接服务端程序。唯一路径是通过结构体 `struct sockaddr_un` 来设置的,如下代码: 7 | 8 | 服务端: 9 | ```cpp 10 | ... 11 | int fd; 12 | struct sockaddr_un addr; 13 | 14 | fd = socket(AF_UNIX, SOCK_STREAM, 0); 15 | 16 | memset(&addr, 0, sizeof(addr)); 17 | 18 | addr.sun_family = AF_UNIX; 19 | strcpy(addr.sun_path, "/tmp/server.sock"); 20 | 21 | bind(fd, (struct sockaddr *)&addr, sizeof(addr)); 22 | ... 23 | ``` 24 | 客户端: 25 | ```cpp 26 | ... 27 | int fd; 28 | struct sockaddr_un addr; 29 | 30 | fd = socket(AF_UNIX, SOCK_STREAM, 0); 31 | 32 | memset(&addr, 0, sizeof(addr)); 33 | 34 | addr.sun_family = AF_UNIX; 35 | strcpy(addr.sun_path, "/tmp/server.sock"); 36 | 37 | connect(fd, (struct sockaddr *)&addr, sizeof(addr)); 38 | ... 39 | ``` 40 | 41 | ## Unix Domain Sockets实现 42 | ### socket() 函数实现 43 | 上一章介绍过,在应用程序中调用 `socket()` 函数时候,最终会调用 `sys_socket()` 函数,`sys_socket()` 接着通过调用 `sock_create()` 函数创建socket对象,我们来看看 `sock_create()` 函数的实现: 44 | ```cpp 45 | int sock_create(int family, int type, int protocol, struct socket **res) 46 | { 47 | int i; 48 | struct socket *sock; 49 | 50 | ... 51 | 52 | if (!(sock = sock_alloc())) { 53 | printk(KERN_WARNING "socket: no more sockets\n"); 54 | i = -ENFILE; 55 | goto out; 56 | } 57 | 58 | sock->type = type; 59 | 60 | if ((i = net_families[family]->create(sock, protocol)) < 0) { 61 | sock_release(sock); 62 | goto out; 63 | } 64 | 65 | *res = sock; 66 | 67 | out: 68 | net_family_read_unlock(); 69 | return i; 70 | } 71 | ``` 72 | 因为创建 `Unix Domain Sockets` 时需要为 `family` 参数传入 `AF_UNIX`,所以代码 `net_families[family]->create()` 就是调用 `AF_UNIX` 类型的 `create()` 函数。上一章也介绍过,需要通过调用 `sock_register()` 函数向 `net_families` 注册具体协议的创建函数,对于 `Unix Domain Sockets` 在系统初始化时会在 `af_unix_init()` 函数中注册其创建函数,代码如下: 73 | ```cpp 74 | struct net_proto_family unix_family_ops = { 75 | PF_UNIX, 76 | unix_create 77 | }; 78 | 79 | static int __init af_unix_init(void) 80 | { 81 | ... 82 | sock_register(&unix_family_ops); 83 | ... 84 | return 0; 85 | } 86 | ``` 87 | 所以对于代码 `net_families[AF_UNIX]->create()` 实际调用的就是 `unix_create()` 函数,`unix_create()` 函数实现如下: 88 | ```cpp 89 | static int unix_create(struct socket *sock, int protocol) 90 | { 91 | ... 92 | sock->state = SS_UNCONNECTED; 93 | 94 | switch (sock->type) { 95 | case SOCK_STREAM: 96 | sock->ops = &unix_stream_ops; 97 | break; 98 | case SOCK_RAW: 99 | sock->type=SOCK_DGRAM; 100 | case SOCK_DGRAM: 101 | sock->ops = &unix_dgram_ops; 102 | break; 103 | default: 104 | return -ESOCKTNOSUPPORT; 105 | } 106 | 107 | return unix_create1(sock) ? 0 : -ENOMEM; 108 | } 109 | ``` 110 | 从 `unix_create()` 函数的实现可知,当向参数 `protocol` 传入 `SOCK_STREAM` 时,就把 sock 的 `ops` 字段设置为 `unix_stream_ops`,如果传入的是 `SOCK_RAW`,那么就把 sock 的 `ops` 字段设置为 `unix_dgram_ops`。这两个结构的定义如下: 111 | ```cpp 112 | struct proto_ops unix_stream_ops = { 113 | family: PF_UNIX, 114 | 115 | release: unix_release, 116 | bind: unix_bind, 117 | connect: unix_stream_connect, 118 | socketpair: unix_socketpair, 119 | accept: unix_accept, 120 | getname: unix_getname, 121 | poll: unix_poll, 122 | ioctl: unix_ioctl, 123 | listen: unix_listen, 124 | shutdown: unix_shutdown, 125 | setsockopt: sock_no_setsockopt, 126 | getsockopt: sock_no_getsockopt, 127 | sendmsg: unix_stream_sendmsg, 128 | recvmsg: unix_stream_recvmsg, 129 | mmap: sock_no_mmap, 130 | }; 131 | 132 | struct proto_ops unix_dgram_ops = { 133 | family: PF_UNIX, 134 | 135 | release: unix_release, 136 | bind: unix_bind, 137 | connect: unix_dgram_connect, 138 | socketpair: unix_socketpair, 139 | accept: sock_no_accept, 140 | getname: unix_getname, 141 | poll: datagram_poll, 142 | ioctl: unix_ioctl, 143 | listen: sock_no_listen, 144 | shutdown: unix_shutdown, 145 | setsockopt: sock_no_setsockopt, 146 | getsockopt: sock_no_getsockopt, 147 | sendmsg: unix_dgram_sendmsg, 148 | recvmsg: unix_dgram_recvmsg, 149 | mmap: sock_no_mmap, 150 | }; 151 | ``` 152 | `unix_create()` 函数接着会调用 `unix_create1()` 来进行下一步创建工作,代码如下: 153 | ```cpp 154 | static struct sock * unix_create1(struct socket *sock) 155 | { 156 | struct sock *sk; 157 | 158 | if (atomic_read(&unix_nr_socks) >= 2*files_stat.max_files) 159 | return NULL; 160 | 161 | MOD_INC_USE_COUNT; 162 | sk = sk_alloc(PF_UNIX, GFP_KERNEL, 1); 163 | if (!sk) { 164 | MOD_DEC_USE_COUNT; 165 | return NULL; 166 | } 167 | 168 | atomic_inc(&unix_nr_socks); 169 | 170 | sock_init_data(sock,sk); 171 | 172 | sk->write_space = unix_write_space; 173 | sk->max_ack_backlog = sysctl_unix_max_dgram_qlen; 174 | sk->destruct = unix_sock_destructor; 175 | 176 | sk->protinfo.af_unix.dentry = NULL; 177 | sk->protinfo.af_unix.mnt = NULL; 178 | sk->protinfo.af_unix.lock = RW_LOCK_UNLOCKED; 179 | 180 | atomic_set(&sk->protinfo.af_unix.inflight, 0); 181 | init_MUTEX(&sk->protinfo.af_unix.readsem); 182 | init_waitqueue_head(&sk->protinfo.af_unix.peer_wait); 183 | sk->protinfo.af_unix.list=NULL; 184 | unix_insert_socket(&unix_sockets_unbound, sk); 185 | 186 | return sk; 187 | } 188 | ``` 189 | `unix_create1()` 函数主要是创建并初始化一个 `struct sock` 结构,然后保存到 `socket` 对象的 `sk` 字段中。这个 `struct sock` 结构就是 `Unix Domain Sockets` 的操作实体,也就是说所有对socket的操作最终都是作用于这个 `struct sock` 结构上。`struct sock` 结构的定义非常复杂,所以这里就不把这个结构列出来,在分析的过程中涉及到这个结构的时候再加以说明。 190 | 191 | ### bind() 函数实现 192 | `bind()` 系统调用最终会调用 `sys_bind()` 函数,而 `sys_bind()` 函数继而调用 `unix_bind()` 函数,调用链为: `bind() -> sys_bind() -> unix_bind()`,我们来看看 `unix_bind()` 函数的实现: 193 | ```cpp 194 | static int unix_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len) 195 | { 196 | ... 197 | write_lock(&unix_table_lock); 198 | 199 | if (!sunaddr->sun_path[0]) { 200 | err = -EADDRINUSE; 201 | if (__unix_find_socket_byname(sunaddr, addr_len, 202 | sk->type, hash)) { 203 | unix_release_addr(addr); 204 | goto out_unlock; 205 | } 206 | 207 | list = &unix_socket_table[addr->hash]; 208 | } else { 209 | // 根据文件路径查找一个inode对象, 然后通过这个inode的编号查找到合适的哈希链表 210 | list = &unix_socket_table[dentry->d_inode->i_ino & (UNIX_HASH_SIZE-1)]; 211 | sk->protinfo.af_unix.dentry = nd.dentry; 212 | sk->protinfo.af_unix.mnt = nd.mnt; 213 | } 214 | 215 | err = 0; 216 | __unix_remove_socket(sk); 217 | sk->protinfo.af_unix.addr = addr; 218 | __unix_insert_socket(list, sk); // 把socket添加到全局哈希表中 219 | 220 | out_unlock: 221 | write_unlock(&unix_table_lock); 222 | out_up: 223 | up(&sk->protinfo.af_unix.readsem); 224 | out: 225 | return err; 226 | ... 227 | } 228 | ``` 229 | `unix_socket()` 函数有一大部分代码是基于文件系统的,主要就是根据 socket 绑定的文件路径创建一个 `inode` 对象,然后将这个 `inode` 的编号作为哈希值找到 `Unix Domain Sockets` 全局哈希表对应的哈希链表,最后把这个 socket 添加到哈希链表中。通过这个操作,就可以把 socket 与一个文件路径关联上。 230 | 231 | ### listen() 函数 232 | `listen()` 函数用于把 socket 设置为监听状态,`listen()` 函数首先会触发 `sys_listen()` 函数,然后 `sys_listen()` 函数会调用 `unix_listen()` 函数把 socket 设置为监听状态,调用链为:`listen() -> sys_listen() -> unix_listen()`,`unix_listen()` 函数的实现如下: 233 | ```cpp 234 | static int unix_listen(struct socket *sock, int backlog) 235 | { 236 | ... 237 | sk->max_ack_backlog = backlog; 238 | sk->state = TCP_LISTEN; 239 | ... 240 | return err; 241 | } 242 | ``` 243 | `unix_socket()` 函数的实现很简单,主要是把 socket 的 `state` 字段设置为 `TCP_LISTEN`,表示当前 socket 处于监听状态。同时还设置了 socket 的 `max_ack_backlog` 字段,表示当前 socket 能够接收最大用户连接的队列长度。 244 | -------------------------------------------------------------------------------- /virtual-memory-managemen.md: -------------------------------------------------------------------------------- 1 | ## X86 CPU分段机制 2 | 在介绍Linux虚拟内存管理之前,我们必须介绍 `X86 CPU` 的分段和分页机制(比较枯燥的知识点),因为Linux虚拟内存管理是建立在分段和分页机制的基础上的。 3 | 4 | 分段的本意是按不同的功能来把内存划分成不同的段进行管理,例如:一个进程按不同的功能,可以划分为数据段、代码段和堆栈段等等。数据段和堆栈段可以进行读写操作但不能执行,而代码段能够执行但不能写(因为如果代码段能进行写操作,那么一些恶意的程序就可以随意改动要执行的代码进行一些非法的操作)。 5 | 6 | 从本意来看,分段是一个不错的内存管理方案。但是,分段机制有个致命的问题,就是当内存不足时,需要把整个段交换到磁盘中。如果段占用的空间很大,那么交换的代价就非常大,以致后来开发出分页机制来弥补这个问题。有了分页机制后,分段机制本应可以去掉的,但是 Intel 公司为了兼容旧版的CPU,保留了分段机制,所以新版的CPU也一直保留着分段机制。 7 | 8 | 在安装有 `X86 CPU` 的电脑开机后,首先会进入 `实模式`,所谓的 `实模式` 是指代码中访问的内存地址都是物理地址,而且内存地址通过 `段寄存器:偏移量` 这种分段方式访问的,代表的实际内存地址是: 9 | ``` 10 | 物理地址 = (段寄存器 << 4) + 偏移量 11 | ``` 12 | 因为在实模式下段寄存器和偏移量都是16位的,所以下只能访问 `1MB` 的内存地址。`X86 CPU` 的段寄存器有6个,分别为: `cs`、`ds`、`fs`、`gs`、`ss` 和 `es`。`cs` 是代码段寄存器,`ds` 是数据段寄存器,`ss` 是堆栈段寄存器,而 `fs`、`gs` 和 `es` 是辅助段寄存器。 13 | 14 | 由于在实模式下只能访问 `1MB` 的内存地址,这对于现代操作系统来说是远远不够的,所以 Intel 公司开发出支持 `保护模式` 的 CPU,在 `保护模式` 下,还是通过 `段寄存器:偏移量` 的模式进行内存访问,但 `段寄存器` 不再是内存地址的一部分,而是指向一个内存基地址的描述符:`段描述符(也叫段选择器)`,而偏移量也从原来的 16 位变为 32 位。如下图: 15 | 16 | ![x86-segment](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/x86-segment.png) 17 | 18 | 运行在 `保护模式` 下的操作系统需要提供一个 `段描述符表` 的数组让CPU能够通过段寄存器找到对应的 `段描述符`,`段描述符表` 分为 `全局描述符表GDT` 和 `局部描述符表LDT`,如下图: 19 | 20 | ![semget-selector-table](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/semget-selector-table.png) 21 | 22 | 为什么会有 `全局描述符表(GDT)` 和 `局部描述符表(LDT)` 这两种表?这是因为 Intel 当初希望操作系统开发者能够通过 `全局描述符表` 来访问内核的数据,而通过 `局部描述符表` 来访问进程的数据。但 Linux 基本不会用到 `局部描述符表`,所以我们基本可以忽略。 23 | 24 | `段描述符` 是一个占用64字节大小的数据结构,结构如下图: 25 | 26 | ![semgent-selector](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/semgent-selector.png) 27 | 28 | 说实话,有了分页机制后,分段机制就变成了鸡肋了(保留只是为了兼容罢了,而且Linux也只是应付式的使用)。所以对于 `段描述符` 这个结构有兴趣的可以自己翻阅一些 `X86 CPU` 相关的书籍或文字了,这里就不作详细介绍了。 29 | 30 | ## X86 CPU分页机制 31 | -------------------------------------------------------------------------------- /virtual-physical-address-mapping.md: -------------------------------------------------------------------------------- 1 | ## 虚拟地址与物理地址映射 2 | -------------------------------------------------------------------------------- /virtual_memory_address_manager.md: -------------------------------------------------------------------------------- 1 | ## Linux虚拟内存管理 2 | 3 | Linux的内存管理分为 `虚拟内存管理` 和 `物理内存管理`,本文主要介绍 `虚拟内存管理` 的原理和实现。在介绍 `虚拟内存管理` 前,首先介绍一下 `x86 CPU` 内存寻址的具体过程。 4 | 5 | ### x86 内存寻址 6 | 7 | `Intel x86 CPU` 把内存地址分为3种:`逻辑地址`、`线性地址` 和 `物理地址`。 8 | * `逻辑地址:` 也称为 `虚拟地址`,由 `段寄存器:偏移量` 组成(`段寄存器` 为16位,`偏移量` 为32位),`偏移量` 是应用程序能够直接操作的地址,比如在C语言中使用 `&` 操作符取得的变量地址就是 `逻辑地址`。 9 | * `线性地址`:是通过 CPU 的分段单元把 `段寄存器:偏移量` 转换成一个32位的无符号整数,范围从 `0x00000000 ~ 0xFFFFFFFFF`。分段机制的原理是,段寄存器指向一个段描述符,段描述符里面包含了段的基地址(开始地址),然后通过基地址加上偏移量就是线性地址。 10 | * `物理地址`:内存中的每个字节都由一个32位的整数编号表示,而这个整数编号就是内存的 `物理地址`。比如在安装了4GB内存条的计算机中,能够寻址的物理地址范围为 `0x00000000 ~ 0xFFFFFFFFF`。在开启了分页机制的情况下,`线性地址` 要经过分页单元转换才能得到 `物理地址`。 11 | 12 | 下图展示了 `逻辑地址`、`线性地址` 和 `物理地址` 三者的关系: 13 | 14 | ![memory-address](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/memory-address.jpeg) 15 | 16 | ### x86 分页机制 17 | 18 | 前面介绍过,应用程序中的逻辑地址需要通过分段机制和分页机制转换后才能得到真正的物理地址。由于Linux把代码段和数据段的基地址都设置为0,所以逻辑地址中的偏移量就等价于线性地址。所以这里就不介绍分段机制了,有兴趣可以查阅相关的文章或者书籍。 19 | 20 | 由于Linux主要使用分页机制,所以下面重点介绍一下分页机制的原理。 21 | 22 | 由于CPU只能对 `物理地址` 进行寻址,所以 `线性地址` 需要映射到 `物理地址` 才能使用,而映射是按 `页(Page)` 作为单位进行的,一个页的大小为4KB,所以32位的线性地址可以划分为 `2^32 / 2^12 = 2^20 (1048576)` 个页。 23 | 24 | 映射是通过 `页表` 作为媒介的。页表是一个类型为整型的数组,数组的每个元素保存了线性地址页对应的物理地址页的起始地址。由于32位的线性地址可以划分为 `2^20` 个页,而每个线性地址页需要一个整型来映射到物理地址页,所以页表的大小为 `4 * 2^20 (4MB)`。由于并不是所有的线性地址都会映射到物理地址,所以为了节省空间,引入了一个二级管理模式的机器来组织分页单元。如下图: 25 | 26 | ![lining-physical-mapping](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/lining-physical-mapping.jpg) 27 | 28 | 从上图可以看出,线性地址被划分为3部分:`面目录索引(10位)`、`页表索引(10位)` 和 `偏移量(12位)`。而 `cr3寄存器` 保存了 `页目录` 的物理地址,这样就可以通过 `cr3寄存器` 来找到 `页目录`。`页目录项` 指向页表地址,`页表项` 指向映射的物理内存页地址,而 `偏移量` 指定了在物理内存页的偏移量。 29 | 30 | ### 虚拟内存地址管理 31 | 32 | 应用程序使用 `malloc()` 函数向Linux内核申请内存时,Linux内核会返回可用的虚拟内存地址给应用程序。我们可以通过以下程序来验证: 33 | ```cpp 34 | #include 35 | #include 36 | 37 | int main() 38 | { 39 | void *ptr; 40 | 41 | ptr = malloc(1024); 42 | 43 | printf("%p\n", ptr); 44 | 45 | return 0; 46 | } 47 | ``` 48 | 49 | 运行程序后输出: 50 | 51 | ```plain 52 | # 0x7fffeefe6260 53 | ``` 54 | 55 | 内核返回的是虚拟内存地址,但虚拟内存地址映射到物理内存地址不会在申请内存时进行,只有在应用程序读写申请的内存时才会进行映射。 56 | 57 | 每个进程都可以使用4GB的虚拟内存地址,所以Linux内核需要为每个进程管理这4GB的虚拟内存地址。例如记录哪些虚拟内存地址是空闲的可以分配的,哪些虚拟内存地址已经被占用了。而Linux内核使用 `vm_area_struct` 结构进行管理,定义如下: 58 | ```cpp 59 | struct vm_area_struct { 60 | struct mm_struct * vm_mm; /* VM area parameters */ 61 | unsigned long vm_start; 62 | unsigned long vm_end; 63 | 64 | /* linked list of VM areas per task, sorted by address */ 65 | struct vm_area_struct *vm_next; 66 | 67 | pgprot_t vm_page_prot; 68 | unsigned long vm_flags; 69 | 70 | /* AVL tree of VM areas per task, sorted by address */ 71 | short vm_avl_height; 72 | struct vm_area_struct * vm_avl_left; 73 | struct vm_area_struct * vm_avl_right; 74 | 75 | struct vm_area_struct *vm_next_share; 76 | struct vm_area_struct **vm_pprev_share; 77 | 78 | struct vm_operations_struct * vm_ops; 79 | unsigned long vm_pgoff; 80 | struct file * vm_file; 81 | ... 82 | }; 83 | ``` 84 | `vm_area_struct` 结构各个字段作用: 85 | * `vm_mm`:指向进程内存空间管理对象。 86 | * `vm_start`:内存区的开始地址。 87 | * `vm_end`:内存区的结束地址。 88 | * `vm_next`:用于连接进程的所有内存区。 89 | * `vm_page_prot`:指定内存区的访问权限。 90 | * `vm_flags`:内存区的一些标志。 91 | * `vm_file`:指向映射的文件对象。 92 | * `vm_ops`:内存区的一些操作函数。 93 | 94 | `vm_area_struct` 结构通过以下方式对虚拟内存地址进行管理,如下图: 95 | 96 | ![vm_address](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/vm_address.png) 97 | 98 | 从上图可以看出,通过 `vm_area_struct` 结构体可以把虚拟内存地址划分为多个用途不相同的内存区,比如可以划分为数据区、代码区、堆区和栈区等等。每个进程描述符(内核用于管理进程的结构)都有一个类型为 `mm_struct` 结构的字段,这个结构的 `mmap` 字段保存了已经被使用的虚拟内存地址。 99 | 100 | 当应用程序通过 `malloc()` 函数向内核申请内存时,会触发系统调用 `sys_brk()`,`sys_brk()` 实现如下: 101 | ```cpp 102 | asmlinkage unsigned long sys_brk(unsigned long brk) 103 | { 104 | unsigned long rlim, retval; 105 | unsigned long newbrk, oldbrk; 106 | struct mm_struct *mm = current->mm; 107 | 108 | down(&mm->mmap_sem); 109 | 110 | if (brk < mm->end_code) 111 | goto out; 112 | newbrk = PAGE_ALIGN(brk); 113 | oldbrk = PAGE_ALIGN(mm->brk); 114 | if (oldbrk == newbrk) 115 | goto set_brk; 116 | 117 | if (brk <= mm->brk) { // 缩小堆空间 118 | if (!do_munmap(mm, newbrk, oldbrk-newbrk)) 119 | goto set_brk; 120 | goto out; 121 | } 122 | 123 | // 检测是否超过限制 124 | rlim = current->rlim[RLIMIT_DATA].rlim_cur; 125 | if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim) 126 | goto out; 127 | 128 | // 如果已经存在, 那么直接返回 129 | if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) 130 | goto out; 131 | 132 | // 是否有足够的内存页? 133 | if (!vm_enough_memory((newbrk-oldbrk) >> PAGE_SHIFT)) 134 | goto out; 135 | 136 | // 所有判断都成功, 现在调用do_brk()进行扩展堆空间 137 | if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) 138 | goto out; 139 | set_brk: 140 | mm->brk = brk; 141 | out: 142 | retval = mm->brk; 143 | up(&mm->mmap_sem); 144 | return retval; 145 | } 146 | ``` 147 | `sys_brk()` 系统调用的 `brk` 参数指定了堆区的新指针,`sys_brk()` 首先会进行一些检测,然后调用 `do_brk()` 函数进行虚拟内存地址的申请,`do_brk()` 函数实现如下: 148 | ```cpp 149 | unsigned long do_brk(unsigned long addr, unsigned long len) 150 | { 151 | struct mm_struct * mm = current->mm; 152 | struct vm_area_struct * vma; 153 | unsigned long flags, retval; 154 | 155 | ... 156 | 157 | // 如果新申请的内存空间与之前的内存空间相连并且特性一样, 那么就合并内存空间 158 | if (addr) { 159 | struct vm_area_struct * vma = find_vma(mm, addr-1); 160 | if (vma && vma->vm_end == addr && 161 | !vma->vm_file && 162 | vma->vm_flags == flags) 163 | { 164 | vma->vm_end = addr + len; 165 | goto out; 166 | } 167 | } 168 | 169 | // 新申请一个 vm_area_struct 结构 170 | vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); 171 | if (!vma) 172 | return -ENOMEM; 173 | 174 | vma->vm_mm = mm; 175 | vma->vm_start = addr; 176 | vma->vm_end = addr + len; 177 | vma->vm_flags = flags; 178 | vma->vm_page_prot = protection_map[flags & 0x0f]; 179 | vma->vm_ops = NULL; 180 | vma->vm_pgoff = 0; 181 | vma->vm_file = NULL; 182 | vma->vm_private_data = NULL; 183 | 184 | insert_vm_struct(mm, vma); // 添加到虚拟内存管理器中 185 | 186 | out: 187 | ... 188 | return addr; 189 | } 190 | ``` 191 | `do_brk()` 函数主要通过调用 `kmem_cache_alloc()` 申请一个 `vm_area_struct` 结构,然后对这个结构的各个字段进行初始。最后通过调用 `insert_vm_struct()` 函数把这个结构添加到进程的虚拟内存地址链表中。 192 | 193 | > 为了加速查找虚拟内存区,Linux内核还为 `vm_area_struct` 结构构建了一个 `AVL树(新版本为红黑树)`,有兴趣的可以查阅源码或相关资料。 194 | 195 | ### 虚拟地址与物理地址映射 196 | 197 | 前面说过,虚拟地址必须要与物理地址进行映射才能使用,如果访问了没有被映射的虚拟地址,CPU会触发内存访问异常,并且调用异常处理例程对没被映射的虚拟地址进行映射操作。Linux的内存访问异常处理例程是 `do_page_fault()`,代码如下: 198 | ```cpp 199 | asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) 200 | { 201 | ... 202 | 203 | // 获取发生错误的虚拟地址 204 | __asm__("movl %%cr2,%0":"=r" (address)); 205 | 206 | ... 207 | 208 | down(&mm->mmap_sem); 209 | 210 | // 查找第一个结束地址比address大的vma 211 | vma = find_vma(mm, address); 212 | if (!vma) 213 | goto bad_area; 214 | 215 | ... 216 | 217 | // 这里是进行物理内存映射的地方 218 | switch (handle_mm_fault(mm, vma, address, write)) { 219 | case 1: 220 | tsk->min_flt++; 221 | break; 222 | case 2: 223 | tsk->maj_flt++; 224 | break; 225 | case 0: 226 | goto do_sigbus; 227 | default: 228 | goto out_of_memory; 229 | } 230 | 231 | ... 232 | 233 | bad_area: 234 | up(&mm->mmap_sem); 235 | 236 | bad_area_nosemaphore: 237 | // 用户空间触发的虚拟内存地址越界访问, 发送SIGSEGV信息(段错误) 238 | if (error_code & 4) { 239 | tsk->thread.cr2 = address; 240 | tsk->thread.error_code = error_code; 241 | tsk->thread.trap_no = 14; 242 | info.si_signo = SIGSEGV; 243 | info.si_errno = 0; 244 | info.si_addr = (void *)address; 245 | force_sig_info(SIGSEGV, &info, tsk); 246 | return; 247 | } 248 | } 249 | ``` 250 | 当异常发生时,CPU会把触发异常的虚拟内存地址保存到 `cr2寄存器` 中,`do_page_fault()` 函数首先通过读取 `cr2寄存器` 获取到触发异常的虚拟内存地址,然后调用 `find_vma()` 函数获取虚拟内存地址对应的 `vm_area_struct` 结构,如果找不到说明这个虚拟内存地址是不合法的(没有进行申请),所以内核会发送 `SIGSEGV` 信号(传说中的段错误)给进程。如果虚拟地址是合法的,那么就调用 `handle_mm_fault()` 函数对虚拟地址进行映射。 251 | -------------------------------------------------------------------------------- /vmalloc-memory-implements.md: -------------------------------------------------------------------------------- 1 | # vmalloc原理与实现 2 | 3 | 在 Linux 系统中的每个进程都有独立 4GB 内存空间,而 Linux 把这 4GB 内存空间划分为用户内存空间(`0 ~ 3GB`)和内核内存空间(`3GB ~ 4GB`),而内核内存空间由划分为直接内存映射区和动态内存映射区(vmalloc区)。 4 | 5 | 直接内存映射区从 `3GB` 开始到 `3GB+896MB` 处结束,直接内存映射区的特点就是物理地址与虚拟地址的关系为:`虚拟地址 = 物理地址 + 3GB`。而动态内存映射区不能通过这种简单的关系关联,而是需要访问动态内存映射区时,由内核动态申请物理内存并且映射到动态内存映射区中。下图是动态内存映射区在内存空间的位置: 6 | 7 | ![vmalloc-memory](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/vmalloc-memory.jpg) 8 | 9 | ## 为什么需要vmalloc区 10 | 11 | 由于直接内存映射区(`3GB ~ 3GB+896MB`)是直接映射到物理地址(`0 ~ 896MB`)的,所以内核不能通过直接内存映射区使用到超过 896MB 之外的物理内存。这时候就需要提供一个机制能够让内核使用 896MB 之外的物理内存,所以 Linux 就实现了一个 vmalloc 机制。vmalloc 机制的目的是在内核内存空间提供一个内存区,能够让这个内存区映射到 896MB 之外的物理内存。如下图: 12 | 13 | ![vmalloc-map](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/vmalloc-map.jpg) 14 | 15 | 那么什么时候使用 vmalloc 呢?一般来说,如果要申请大块的内存就可以用vmalloc。 16 | 17 | ## vmalloc实现 18 | 19 | 可以通过 `vmalloc()` 函数向内核申请一块内存,其原型如下: 20 | 21 | ```c 22 | void * vmalloc(unsigned long size); 23 | ``` 24 | 25 | 参数 `size` 表示要申请的内存块大小。 26 | 27 | 我们看看看 `vmalloc()` 函数的实现,代码如下: 28 | ```c 29 | static inline void * vmalloc(unsigned long size) 30 | { 31 | return __vmalloc(size, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL); 32 | } 33 | ``` 34 | 35 | 从上面代码可以看出,`vmalloc()` 函数直接调用了 `__vmalloc()` 函数,而 `__vmalloc()` 函数的实现如下: 36 | ```c 37 | void * __vmalloc(unsigned long size, int gfp_mask, pgprot_t prot) 38 | { 39 | void * addr; 40 | struct vm_struct *area; 41 | 42 | size = PAGE_ALIGN(size); // 内存对齐 43 | if (!size || (size >> PAGE_SHIFT) > num_physpages) { 44 | BUG(); 45 | return NULL; 46 | } 47 | 48 | area = get_vm_area(size, VM_ALLOC); // 申请一个合法的虚拟地址 49 | if (!area) 50 | return NULL; 51 | 52 | addr = area->addr; 53 | // 映射物理内存地址 54 | if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) { 55 | vfree(addr); 56 | return NULL; 57 | } 58 | 59 | return addr; 60 | } 61 | ``` 62 | 63 | `__vmalloc()` 函数主要工作有两点: 64 | * 调用 `get_vm_area()` 函数申请一个合法的虚拟内存地址。 65 | * 调用 `vmalloc_area_pages()` 函数把虚拟内存地址映射到物理内存地址。 66 | 67 | 接下来,我们看看 `get_vm_area()` 函数的实现,代码如下: 68 | ```c 69 | struct vm_struct * get_vm_area(unsigned long size, unsigned long flags) 70 | { 71 | unsigned long addr; 72 | struct vm_struct **p, *tmp, *area; 73 | 74 | area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL); 75 | if (!area) 76 | return NULL; 77 | size += PAGE_SIZE; 78 | addr = VMALLOC_START; 79 | write_lock(&vmlist_lock); 80 | for (p = &vmlist; (tmp = *p) ; p = &tmp->next) { 81 | if ((size + addr) < addr) 82 | goto out; 83 | if (size + addr <= (unsigned long) tmp->addr) 84 | break; 85 | addr = tmp->size + (unsigned long) tmp->addr; 86 | if (addr > VMALLOC_END-size) 87 | goto out; 88 | } 89 | area->flags = flags; 90 | area->addr = (void *)addr; 91 | area->size = size; 92 | area->next = *p; 93 | *p = area; 94 | write_unlock(&vmlist_lock); 95 | return area; 96 | 97 | out: 98 | write_unlock(&vmlist_lock); 99 | kfree(area); 100 | return NULL; 101 | } 102 | ``` 103 | 104 | `get_vm_area()` 函数比较简单,首先申请一个类型为 `vm_struct` 的结构 `area` 用于保存申请到的虚拟内存地址。然后查找可用的虚拟内存地址,如果找到,就把虚拟内存到虚拟内存地址保存到 `area` 变量中。最后把 `area` 连接到 `vmalloc` 虚拟内存地址管理链表 `vmlist` 中。`vmlist` 链表最终结果如下图: 105 | 106 | ![vmalloc-address-manager](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/vmalloc-address-manager.jpg) 107 | 108 | 申请到虚拟内存地址后,`__vmalloc()` 函数会调用 `vmalloc_area_pages()` 函数来对虚拟内存地址与物理内存地址进行映射。 109 | 110 | 我们知道,映射过程就是对进程的 `页表` 进行映射。但每个进程都有一个独立 `页表`(内核线程除外),并且我们知道内核空间是所有进程共享的,那么就有个问题:如果只映射当前进程 `页表` 的内核空间,那么怎么同步到其他进程的内核空间呢? 111 | 112 | 为了解决内核空间同步问题,Linux 并不是直接对当前进程的内核空间映射的,而是对 `init` 进程的内核空间(`init_mm`)进行映射,我们来看看 `vmalloc_area_pages()` 函数的实现: 113 | ```c 114 | inline int vmalloc_area_pages (unsigned long address, unsigned long size, 115 | int gfp_mask, pgprot_t prot) 116 | { 117 | pgd_t * dir; 118 | unsigned long end = address + size; 119 | int ret; 120 | 121 | dir = pgd_offset_k(address); // 获取 address 地址在 init 进程对应的页目录项 122 | spin_lock(&init_mm.page_table_lock); // 对 init_mm 上锁 123 | do { 124 | pmd_t *pmd; 125 | 126 | pmd = pmd_alloc(&init_mm, dir, address); 127 | ret = -ENOMEM; 128 | if (!pmd) 129 | break; 130 | 131 | ret = -ENOMEM; 132 | if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot)) // 对页目录项进行映射 133 | break; 134 | 135 | address = (address + PGDIR_SIZE) & PGDIR_MASK; 136 | dir++; 137 | 138 | ret = 0; 139 | } while (address && (address < end)); 140 | spin_unlock(&init_mm.page_table_lock); 141 | return ret; 142 | } 143 | ``` 144 | 从上面代码可以看出,`vmalloc_area_pages()` 函数映射的主体是 `init` 进程的内存空间。因为映射的 `init` 进程的内存空间,所以当前进程访问 `vmalloc()` 函数申请的内存时,由于没有对虚拟内存进行映射,所以会发生 `缺页异常` 而触发内核调用 `do_page_fault()` 函数来修复。我们看看 `do_page_fault()` 函数对 `vmalloc()` 申请的内存异常处理: 145 | ```c 146 | void do_page_fault(struct pt_regs *regs, unsigned long error_code) 147 | { 148 | ... 149 | __asm__("movl %%cr2,%0":"=r" (address)); // 获取出错的虚拟地址 150 | ... 151 | 152 | if (address >= TASK_SIZE && !(error_code & 5)) 153 | goto vmalloc_fault; 154 | 155 | ... 156 | 157 | vmalloc_fault: 158 | { 159 | int offset = __pgd_offset(address); 160 | pgd_t *pgd, *pgd_k; 161 | pmd_t *pmd, *pmd_k; 162 | pte_t *pte_k; 163 | 164 | asm("movl %%cr3,%0":"=r" (pgd)); 165 | pgd = offset + (pgd_t *)__va(pgd); 166 | pgd_k = init_mm.pgd + offset; 167 | 168 | if (!pgd_present(*pgd_k)) 169 | goto no_context; 170 | set_pgd(pgd, *pgd_k); 171 | 172 | pmd = pmd_offset(pgd, address); 173 | pmd_k = pmd_offset(pgd_k, address); 174 | if (!pmd_present(*pmd_k)) 175 | goto no_context; 176 | set_pmd(pmd, *pmd_k); 177 | 178 | pte_k = pte_offset(pmd_k, address); 179 | if (!pte_present(*pte_k)) 180 | goto no_context; 181 | return; 182 | } 183 | } 184 | ``` 185 | 186 | 上面的代码就是当进程访问 `vmalloc()` 函数申请到的内存时,发生 `缺页异常` 而进行的异常修复,主要的修复过程就是把 `init` 进程的 `页表项` 复制到当前进程的 `页表项` 中,这样就可以实现所有进程的内核内存地址空间同步。 187 | -------------------------------------------------------------------------------- /waitqueue.md: -------------------------------------------------------------------------------- 1 | ## 等待队列原理与实现 2 | 3 | 当进程要获取某些资源(例如从网卡读取数据)的时候,但资源并没有准备好(例如网卡还没接收到数据),这时候内核必须切换到其他进程运行,直到资源准备好再唤醒进程。 4 | 5 | `waitqueue (等待队列)` 就是内核用于管理等待资源的进程,当某个进程获取的资源没有准备好的时候,可以通过调用 `add_wait_queue()` 函数把进程添加到 `waitqueue` 中,然后切换到其他进程继续执行。当资源准备好,由资源提供方通过调用 `wake_up()` 函数来唤醒等待的进程。 6 | 7 | ### 等待队列初始化 8 | 9 | 要使用 `waitqueue` 首先需要声明一个 `wait_queue_head_t` 结构的变量,`wait_queue_head_t` 结构定义如下: 10 | ```cpp 11 | struct __wait_queue_head { 12 | spinlock_t lock; 13 | struct list_head task_list; 14 | }; 15 | ``` 16 | `waitqueue` 本质上是一个链表,而 `wait_queue_head_t` 结构是 `waitqueue` 的头部,`lock` 字段用于保护等待队列在多核环境下数据被破坏,而 `task_list` 字段用于保存等待资源的进程列表。 17 | 18 | 可以通过调用 `init_waitqueue_head()` 函数来初始化 `wait_queue_head_t` 结构,其实现如下: 19 | ```cpp 20 | void init_waitqueue_head(wait_queue_head_t *q) 21 | { 22 | spin_lock_init(&q->lock); 23 | INIT_LIST_HEAD(&q->task_list); 24 | } 25 | ``` 26 | 初始化过程很简单,首先调用 `spin_lock_init()` 来初始化自旋锁 `lock`,然后调用 `INIT_LIST_HEAD()` 来初始化进程链表。 27 | 28 | ### 向等待队列添加等待进程 29 | 30 | 要向 `waitqueue` 添加等待进程,首先要声明一个 `wait_queue_t` 结构的变量,`wait_queue_t` 结构定义如下: 31 | ```cpp 32 | typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int sync, void *key); 33 | 34 | struct __wait_queue { 35 | unsigned int flags; 36 | void *private; 37 | wait_queue_func_t func; 38 | struct list_head task_list; 39 | }; 40 | ``` 41 | 下面说明一下各个成员的作用: 42 | 1. `flags`: 可以设置为 `WQ_FLAG_EXCLUSIVE`,表示等待的进程应该独占资源(解决惊群现象)。 43 | 2. `private`: 一般用于保存等待进程的进程描述符 `task_struct`。 44 | 3. `func`: 唤醒函数,一般设置为 `default_wake_function()` 函数,当然也可以设置为自定义的唤醒函数。 45 | 4. `task_list`: 用于连接其他等待资源的进程。 46 | 47 | 可以通过调用 `init_waitqueue_entry()` 函数来初始化 `wait_queue_t` 结构变量,其实现如下: 48 | ```cpp 49 | static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p) 50 | { 51 | q->flags = 0; 52 | q->private = p; 53 | q->func = default_wake_function; 54 | } 55 | ``` 56 | 57 | 也可以通过调用 `init_waitqueue_func_entry()` 函数来初始化为自定义的唤醒函数: 58 | ```cpp 59 | static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func) 60 | { 61 | q->flags = 0; 62 | q->private = NULL; 63 | q->func = func; 64 | } 65 | ``` 66 | 67 | 初始化完 `wait_queue_t` 结构变量后,可以通过调用 `add_wait_queue()` 函数把等待进程添加到等待队列,其实现如下: 68 | ```cpp 69 | void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) 70 | { 71 | unsigned long flags; 72 | 73 | wait->flags &= ~WQ_FLAG_EXCLUSIVE; 74 | spin_lock_irqsave(&q->lock, flags); 75 | __add_wait_queue(q, wait); 76 | spin_unlock_irqrestore(&q->lock, flags); 77 | } 78 | 79 | static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new) 80 | { 81 | list_add(&new->task_list, &head->task_list); 82 | } 83 | ``` 84 | `add_wait_queue()` 函数的实现很简单,首先通过调用 `spin_lock_irqsave()` 上锁,然后调用 `list_add()` 函数把节点添加到等待队列即可。 85 | 86 | `wait_queue_head_t` 结构与 `wait_queue_t` 结构之间的关系如下图: 87 | 88 | ![waitqueue](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/waitqueue.jpg) 89 | 90 | ### 休眠等待进程 91 | 92 | 当把进程添加到等待队列后,就可以休眠当前进程,让出CPU给其他进程运行,要休眠进程可以通过一下方式: 93 | ```cpp 94 | set_current_state(TASK_INTERRUPTIBLE); 95 | schedule(); 96 | ``` 97 | 代码 `set_current_state(TASK_INTERRUPTIBLE)` 可以把当前进程运行状态设置为 `可中断休眠` 状态,调用 `schedule()` 函数可以使当前进程让出CPU,切换到其他进程执行。 98 | 99 | ### 唤醒等待队列 100 | 101 | 当资源准备好后,就可以唤醒等待队列中的进程,可以通过 `wake_up()` 函数来唤醒等待队列中的进程。`wake_up()` 最终会调用 `__wake_up_common()`,其实现如下: 102 | ```cpp 103 | static void __wake_up_common(wait_queue_head_t *q, 104 | unsigned int mode, int nr_exclusive, int sync, void *key) 105 | { 106 | wait_queue_t *curr, *next; 107 | 108 | list_for_each_entry_safe(curr, next, &q->task_list, task_list) { 109 | unsigned flags = curr->flags; 110 | 111 | if (curr->func(curr, mode, sync, key) && 112 | (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) 113 | break; 114 | } 115 | } 116 | ``` 117 | 可以看出,唤醒等待队列就是变量等待队列的等待进程,然后调用唤醒函数来唤醒它们。 118 | -------------------------------------------------------------------------------- /workqueue.md: -------------------------------------------------------------------------------- 1 | # 内核工作队列 2 | 3 | 在某些情景下内核会将一些耗时的、可延迟的工作放到 `工作队列` 中,内核会在适当的时机处理 `工作队列` 中的工作,就像应用层开发的 `消息队列` 一样。 4 | 5 | ## 一、工作队列对象 6 | 7 | 内核使用 `workqueue_struct` 对象来存储要延迟执行的工作,其定义如下: 8 | 9 | ```c 10 | struct workqueue_struct { 11 | struct cpu_workqueue_struct *cpu_wq; // 真正存储延迟执行工作的地方, 每个拥有CPU一个 12 | struct list_head list; // 用于把内核中所有的工作队列连接起来 13 | const char *name; // 工作队列的名字 14 | int singlethread; // 是否只启动一个工作线程 15 | ... 16 | }; 17 | ``` 18 | 19 | 在 `workqueue_struct` 对象中,`cpu_wq` 字段才是真正存储延迟执行工作的地方,其类型为 `cpu_workqueue_struct`,我们来看看 `cpu_workqueue_struct` 的定义: 20 | 21 | ```c 22 | struct cpu_workqueue_struct { 23 | spinlock_t lock; // 用于锁定工作队列 24 | struct list_head worklist; // 存储延迟执行工作的队列 25 | wait_queue_head_t more_work; // 用于唤醒工作队列线程 26 | struct work_struct *current_work; // 当前正在执行的工作 27 | struct workqueue_struct *wq; // 指向工作队列的指针 28 | struct task_struct *thread; // 执行工作队列的进程 29 | ... 30 | } ____cacheline_aligned; 31 | ``` 32 | 33 | `workqueue_struct` 与 `cpu_workqueue_struct` 之间的关系如下图所示: 34 | 35 | ![](https://raw.githubusercontent.com/liexusong/linux-source-code-analyze/master/images/workqueue/workqueue.png) 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /zero-copy.md: -------------------------------------------------------------------------------- 1 | # 零拷贝技术 2 | 3 | `零拷贝技术` 是编写高性能服务器的一个关键技术,在介绍 `零拷贝技术` 前先说明一下 `用户空间` 与 `内核空间`。 4 | 5 | ### 用户空间 6 | 7 | 通俗的说,`用户空间` 就是运行着用户编写的应用程序的虚拟内存空间。在32位的操作系统中,每个进程都有 4GB 独立的虚拟内存空间,而 0 ~ 3GB 的虚拟内存空间就是用户空间 。 8 | 9 | ### 内核空间 10 | 11 | `内核空间` 就是运行着操作系统代码的虚拟内存空间,而 3GB ~ 4GB 的虚拟内存空间就是内核空间。 12 | 13 | 图 1 展示了 `用户空间` 与 `内核空间` 在进程虚拟内存空间所在的位置: 14 | 15 | ![](./images/zerocopy/userspace-kernelspace.png) 16 | 17 | ### 发送文件 18 | 19 | 为什么要介绍 `用户空间` 和 `内核空间` 呢? 20 | 21 | 我们先来回忆一下,服务端发送一个文件给客户端一般需要进行什么操作。一般来说,服务端发送一个文件给客户端的步骤如下: 22 | 23 | * 首先需要调用 `read` 读取文件的数据到用户空间缓冲区中。 24 | 25 | * 然后再调用 `write` 把缓冲区的数据发送给客户端 Socket。 26 | 27 | 伪代码如下: 28 | 29 | ```c 30 | while ((n = read(file, buf, 4069)) > 0) { 31 | write(sock, buf , n); 32 | } 33 | ``` 34 | 35 | 在上面的过程中,调用了 `read` 和 `write` 两个系统调用。`read` 系统调用是从文件中读取数据到用户空间的缓冲区中,所以调用 `read` 时需要从内核空间复制数据到用户空间,如图 2 所示: 36 | 37 | ![](./images/zerocopy/read.png) 38 | 39 | 图2 就是数据的复制过程,首先会从文件中读取数据到内核的 `页缓存(page cache)`,然后再从页缓存中复制到用户空间的缓冲区中。 40 | 41 | 而当调用 `write` 系统调用把用户空间缓冲区中的数据发送到客户端 Socket 时,首先会把缓冲区的数据复制到内核的 Socket 缓冲区中,网卡驱动会把 Socket 缓冲区的数据发送出去,如图 3 所示: 42 | 43 | ![](./images/zerocopy/write.png) 44 | 45 | 46 | 47 | 从上图可以看出,服务端发送文件给客户端的过程中需要进行两次数据复制,第一次是从内核空间的页缓存复制到用户空间的缓冲区,第二次是从用户空间的缓冲区复制到内核空间的 Socket 缓冲区。 48 | 49 | 仔细观察我们可以发现,上图中的页缓存其实可以直接复制到 Socket 缓冲区,而不需要复制到用户空间缓冲区的。如图 4 所示: 50 | 51 | ![](./images/zerocopy/sendfile.png) 52 | 53 | 54 | 55 | 如上图所示,不需要用户空间作为数据中转的技术叫 `零拷贝技术`。那么,我们可以通过哪个系统调用来实现上图中的技术呢?答案就是 `sendfile`,我们来看看 `sendfile` 系统调用的原型: 56 | 57 | ```c 58 | #include 59 | 60 | ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 61 | ``` 62 | 63 | 下面介绍一下 `sendfile` 各个参数的作用: 64 | 65 | * `out_fd`:数据接收方文件句柄(一般为 Socket 句柄)。 66 | * `in_fd`:数据提供方文件句柄(一般为文件句柄)。 67 | * `offset`:如果 offset 不为 NULL,表示从哪里开始发送数据的偏移量。 68 | * `count`:表示需要发送多少字节的数据。 69 | 70 | `sendfile` 发送数据的过程如图 5 所示: 71 | 72 | ![](./images/zerocopy/sendfile2.png) 73 | 74 | 75 | 76 | 对比图 5 与 图 3,我们发现使用 `sendfile` 可以减少一次系统调用,并且减少一次数据拷贝过程。 77 | 78 | ### 总结 79 | 80 | 本文主要通过 `sendfile` 系统调用来介绍 `零拷贝技术`,但 `零拷贝技术` 不单只有 `sendfile`,如 `mmap`、`splice` 和 `直接I/O` 等都是 `零拷贝技术` 的实现,有兴趣的可以参考 Linux 官方文档或相关资料。 81 | 82 | --------------------------------------------------------------------------------