一、硬件初始化阶段
1、电源自检(POST)
按下电源键,主板 BIOS 固件启动,依次检测 CPU、内存、硬盘控制器、显卡、外设等核心硬件,故障则通过蜂鸣 / 屏幕提示终止启动,无异常则进入下一步。
2、BIOS 固件初始化,定位启动设备并加载GRUB第一扇区
BIOS 加载自身固化驱动,初始化已检测通过的硬件,建立基础硬件运行环境,识别本地存储设备(硬盘/SSD)。BIOS 按 CMOS 中预设的启动顺序(硬盘/U盘/光驱),找到目标启动硬盘,读取硬盘主引导记录(MBR,磁盘首个 512 字节),加载其中的 GRUB 第一阶段(Stage1)引导程序,将控制权移交 GRUB。
Category Archives: OS
【温故知新】Linux系统启动流程(UEFI+GRUB模式)
一、硬件初始化阶段
1、电源自检(Power-On Self-Test, POST)
按下电源键后,主板 UEFI 固件执行核心硬件检测(内存、CPU、硬盘控制器、显卡等),硬件故障则终止启动并抛出提示,无异常则进入下一阶段。
2、固件初始化
UEFI 固件加载自身驱动,识别本地存储设备(硬盘 / SSD 等),初始化硬件运行环境,完成启动前基础准备。
3、启动管理器加载
UEFI 按预设启动项顺序,从EFI 系统分区(ESP) 读取并加载 GRUB 引导程序核心文件(grubx64.efi)。
Windows进程间通讯全解析:10大核心方式+场景选型,搞定进程协同

Windows进程间通讯全解析:10大核心方式+场景选型,搞定进程协同
在 Windows 系统中,多个进程想要协同工作(比如浏览器调用下载工具、办公软件同步数据),就离不开 “进程间通讯(IPC)” 技术。不同场景下,有的需要高速传输大数据,有的需要实时同步状态,有的要跨网络通讯 —— 选对 IPC 方式,能让程序协作更高效、更稳定。今天就拆解 Windows 系统中常见的进程间通讯方式,帮你理清适用场景和核心逻辑。
一、内核对象同步:轻量级状态协同,保障进程有序执行
核心逻辑:利用 Windows 内核对象的信号状态,实现进程间的同步与互斥(比如避免多个进程同时操作同一资源),适用于简单状态通知。
代表技术:互斥量(Mutex,保证同一时刻只有一个进程访问资源)、信号量(Semaphore,控制同时访问资源的进程数量)、Event(事件对象,通过信号触发进程执行);
优势:轻量级、效率高、系统原生支持,无需复杂配置;
适用场景:进程间同步(如多个进程读写同一文件时的锁机制)、简单状态通知(如 “任务完成” 信号触发下一个进程执行)。
二、共享内存类:高速传输大数据,性能优先之选
核心逻辑:多个进程共享同一块物理内存,直接读写内存实现数据传输,无需拷贝,是速度最快的 IPC 方式。
代表技术:共享内存(直接创建共享内存区域,进程间直接访问)、文件映射(将文件映射到内存,多个进程通过内存共享文件数据)、DLL 共享段(通过 DLL 的共享数据段,实现进程间数据共享);
优势:传输速度极快(内存级读写)、无数据拷贝损耗、支持大容量数据传输;
注意点:需要自己实现同步机制(如搭配互斥量),避免数据竞争;
适用场景:高频大数据传输(如视频处理软件的帧数据传递、工业软件的实时数据共享)。
三、管道通讯:基于文件系统,适配本地进程交互
核心逻辑:模拟文件读写的方式,通过 “管道” 这一伪文件实现进程间字节流传输,是 Windows 原生的本地 IPC 方案。
代表技术:匿名管道(Pipe,仅支持父子进程或亲缘进程间通讯,单向传输)、命名管道(Named Pipe,支持任意本地进程间通讯,双向传输);
优势:系统原生支持、使用简单、传输可靠;
适用场景:本地进程间字节流传输(如命令行工具的输出传递、本地服务与客户端的通讯)。
四、组件对象模型:跨进程调用,实现功能复用
核心逻辑:通过 COM(组件对象模型)、DCOM(分布式 COM)、OLE 技术等,将一个进程的功能封装为组件,供其他进程远程调用,实现功能复用。
代表技术:COM(本地跨进程组件调用,如 Office 组件嵌入其他软件)、DCOM(跨网络的 COM 调用,支持远程组件访问)、OLE(对象链接与嵌入,如文档中嵌入图片、表格);
优势:封装性好、支持功能复用、接口标准化;
适用场景:跨进程功能调用(如第三方组件集成、软件插件扩展)、分布式系统的远程组件访问。
五、网络通讯类:跨机器 / 广域通讯,突破本地限制
核心逻辑:基于网络协议实现进程间通讯,不仅支持本地进程,还能跨 Windows 机器、跨网络通讯,是最通用的 IPC 方式。
代表技术:Socket(套接字,支持 TCP/UDP 协议,本地与跨网络通讯通用,如客户端 / 服务器架构)、NetBios 函数(早期 Windows 网络通讯接口,支持局域网内进程交互);
优势:通用性强、支持跨机器 / 跨网络、适配各类数据传输场景;
适用场景:网络应用(如浏览器与服务器通讯)、跨机器进程协同(如分布式服务间调用)、客户端 / 服务器架构软件。
六、消息与钩子:Windows 原生机制,适配桌面应用交互
核心逻辑:利用 Windows 的消息机制或钩子函数,实现进程间的消息传递或行为监控。
代表技术:消息通知(Windows 消息队列,进程间发送自定义消息)、钩子函数(Hook,监控或拦截其他进程的消息 / 行为,如键盘钩子、鼠标钩子)、DLL 注入(通过注入 DLL,实现进程间消息传递或功能扩展);
优势:深度适配 Windows 桌面应用、支持行为监控与消息触发;
适用场景:桌面应用交互(如窗口间消息通知)、进程行为监控(如安全软件的行为拦截)、软件功能增强(如通过 DLL 注入扩展第三方软件功能)。
七、中间件 / 存储介质:间接通讯,解耦进程依赖
核心逻辑:通过第三方存储介质或中间件传递数据,进程间不直接交互,降低耦合度,适配复杂场景。
代表技术:文件(通过读写同一文件传递数据,如日志同步、配置共享)、数据库(关系型 / 非关系型数据库,如多进程共享业务数据)、缓存中间件(Redis、etcd、zk,分布式场景下的配置同步与数据共享)、消息队列(MQ,异步通讯,如任务分发、数据异步传递);
优势:解耦进程依赖、支持异步通讯、适配分布式场景;
适用场景:分布式系统(如微服务间通讯)、异步任务处理(如批量数据处理)、跨进程配置共享(如多进程读取同一数据库配置)。
八、其他特色方式:适配特殊场景需求
除了主流方式,还有一些针对特定场景的 IPC 方案:
动态数据交换(DDE):早期 Windows 桌面应用的通讯方式,支持应用间数据实时同步(如 Excel 表格数据同步到 Word 文档);
粘贴板(Clipboard):简单直观的进程间数据传递,用户通过复制粘贴实现(如从浏览器复制文本到记事本);
文件传输协议(FTP):通过 FTP 协议实现文件级别的进程间 / 跨机器数据传输,适用于大容量文件共享。
总结:Windows IPC 选型核心逻辑 ——“按需匹配,兼顾效率与场景”
选择 IPC 方式时,核心看 3 个维度:
传输距离:本地进程选共享内存、管道、COM;跨网络选 Socket、中间件;
数据量与速度:大数据高速传输选共享内存、文件映射;小数据同步选内核对象、消息通知;
交互方式:同步通讯选 Socket、命名管道;异步通讯选 MQ、中间件;功能复用选 COM/DCOM。
Windows 的 IPC 方案覆盖了从本地轻量级同步到分布式跨网络通讯的全场景,掌握不同方式的核心逻辑,就能根据项目需求精准选型,让进程协同更高效。
你在开发中常用哪种 Windows IPC 方式?遇到过哪些兼容性或性能问题?欢迎在评论区留言交流~
WSL2中apt升级systemd时报错:无法锁定passwd文件
1、环境:
Windows10+WSL2+Ubuntu24
PS:另一台电脑Windows11+WSL2+Ubuntu24,不会报错
2、再现方式及错误信息
# apt-get upgrade Reading package lists... Done Building dependency tree... Done Reading state information... Done Calculating upgrade... Done ... ... Setting up systemd (255.4-1ubuntu8.8) ... Initializing machine ID from random generator. Failed to take /etc/passwd lock: Invalid argument dpkg: error processing package systemd (--configure): installed systemd package post-installation script subprocess returned error exit status 1 Errors were encountered while processing: systemd E: Sub-process /usr/bin/dpkg returned an error code (1)
3、错误发生原因
systemd升级的脚本,会调用systemd-sysusers,systemd-sysusers会尝试通过fcntl锁定文件,但WSL中fcntl实现效果与Linux中不同,导致脚本执行失败。
更进一步的解释:
Linux中文件锁是基于文件描述符的,子进程会自动继承该文件锁。
Windows中文件锁是基于进程的,子进程需要自行获取新的文件锁。
WSL中,实现方式,更接近与Windows,重复获取同一个文件的锁自然是失败的。
openat(AT_FDCWD, "/etc/.pwd.lock", O_WRONLY|O_CREAT|O_NOCTTY|O_NOFOLLOW|O_CLOEXEC, 0600) = 3
fcntl(3, F_OFD_SETLKW, {l_type=F_WRLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = -1 EINVAL (Invalid argument)
4、如何绕过该错误
# 原文在此:https://github.com/microsoft/WSL/issues/10397
# 切换到/bin
# 将systemd-sysusers修改为systemd-sysusers.org
# 将systemd-sysusers做成一个echo的符号链接(用于欺骗升级脚本,让其以为得到了正确的结果)
# 切换回之前的目录
cd /bin && mv -f systemd-sysusers{,.org} && ln -s echo systemd-sysusers && cd -
# 修复包依赖
apt --fix-broken install
# 继续升级
apt-get upgrade
Linux5.9源码
最近阅读了部分Linux5.9源码,添加了一些注释,感兴趣的同学可以看下。
https://github.com/neohope/NeoLinux/
Linux虚拟化管理
1、内核模块初始化
module_init(vmx_init)->kvm_init module_init(svm_init)->kvm_init //其中,kvm_init ->kvm_arch_init ->kvm_irqfd_init ->kvm_arch_hardware_setup ->misc_register(&kvm_dev)
2、从数据结构角度,又可以看到了设备皆为文件的思想
static struct miscdevice kvm_dev = {
KVM_MINOR,
"kvm",
&kvm_chardev_ops,
};
static struct file_operations kvm_chardev_ops = {
.unlocked_ioctl = kvm_dev_ioctl,
.llseek = noop_llseek,
KVM_COMPAT(kvm_dev_ioctl),
};
//初始化时,通过misc_register,实现了操作的绑定。
3、通过上面的数据结构,我们就可以找到创建虚拟机的方法,并生成控制文件
kvm_dev.kvm_chardev_ops.kvm_dev_ioctl
或者,ioctl系统调用KVM_CREATE_VM,效果也是一样的:
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_dev_ioctl
->case KVM_CREATE_VM:
-> r = kvm_dev_ioctl_create_vm(arg);
->file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);
//其中,kvm_dev_ioctl_create_vm
->kvm_create_vm
->->kvm_arch_init_vm
->->hardware_enable_all
->->kvm_arch_post_init_vm
->->list_add(&kvm->vm_list, &vm_list);
4、生成虚拟CPU套路很相似,仍是文件操作
static struct file_operations kvm_vm_fops = {
.release = kvm_vm_release,
.unlocked_ioctl = kvm_vm_ioctl,
.llseek = noop_llseek,
KVM_COMPAT(kvm_vm_compat_ioctl),
};
//创建虚拟机时,通过anon_inode_getfile,实际上就把文件和kvm_vm_fops绑定了起来。
anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR)
5、在调用ioctl时
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg) ->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vm_ioctl kvm_vm_ioctl->kvm_vm_ioctl_create_vcpu ->kvm_arch_vcpu_precreate ->kvm_vcpu_init ->kvm_arch_vcpu_create ->kvm_get_kvm ->create_vcpu_fd,生成设备文件inode ->kvm_arch_vcpu_postcreate //其中,kvm_arch_vcpu_create ->kvm_mmu_create ->vcpu->arch.user_fpu = kmem_cache_zalloc(x86_fpu_cache, GFP_KERNEL_ACCOUNT); ->kvm_pmu_init(vcpu); ->kvm_hv_vcpu_init(vcpu); ->kvm_x86_ops.vcpu_create(vcpu); ->kvm_vcpu_mtrr_init(vcpu); ->vcpu_load(vcpu); ->kvm_vcpu_reset(vcpu, false); ->kvm_init_mmu(vcpu, false); //包括init_kvm_tdp_mmu和init_kvm_softmmu两种虚拟化方式
6、启动虚拟机,还是文件操作
static struct file_operations kvm_vcpu_fops = {
.release = kvm_vcpu_release,
.unlocked_ioctl = kvm_vcpu_ioctl,
.mmap = kvm_vcpu_mmap,
.llseek = noop_llseek,
KVM_COMPAT(kvm_vcpu_compat_ioctl),
};
7、在调用ioctl时KVM_RUN
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg) ->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vcpu_ioctl kvm_vcpu_ioctl-> case KVM_RUN: kvm_arch_vcpu_ioctl_run //其中, kvm_arch_vcpu_ioctl_run->vcpu_run->vcpu_enter_guest
8、IO同样有虚拟化和半虚拟化两种
一个处理函数为kvm_fast_pio,另一个为kvm_emulate_instruction
Linux系统调用03
Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节,给Linux系统,增加一个新系统调用功能,获取cpu数量。
1、新建一个源码编译目录
mkdir kernelbuild
2、下载源码,解压
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.59.tar.gz tar -xzf linux-5.10.59.tar.gz cd linux-5.10.59
3、清理
make mrproper
4、修改文件
4.1、arch/x86/entry/syscalls/syscall_64.tbl
#在440后面增加一行
441 common get_cpus sys_get_cpus
4.2、include/linux/syscalls.h
#在最后一个asmlinkage增加一行
asmlinkage long sys_get_cpus(void);
4.3、kernel/sys.c
#在最后一个SYSCALL_DEFINE0后面增加下面几行
//获取系统中有多少CPU
SYSCALL_DEFINE0(get_cpus)
{
return num_present_cpus();
}
5、内核配置
make menuconfig make oldconfig
6、修改.config,去掉一个证书
CONFIG_SYSTEM_TRUSTED_KEYS=“”
7、编译
make -j4
8、安装
sudo make modules_install sudo make install
9、测试
9.1、新建文件cpus.c
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
int main(int argc, char const *argv[])
{
//syscall就是根据系统调用号调用相应的系统调用
long cpus = syscall(441);
printf("cpu num is:%d\n", cpus);//输出结果
return 0;
}
9.2、编译
gcc main.c -o cpus
9.3、运行
./cpus 在没有修改的内核上返回是-1 在修改过的为num_present_cpus数量
Linux系统调用02
Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节处理第二部分:
二、linux内核部分
1、在make时,会通过syscall_64.tbl生成syscalls_64.h,然后包含到syscall_64.c,进行调用号与函数之间的绑定。 arch/x86/entry/syscalls/syscall_64.tbl arch/x86/include/generated/asm/syscalls_64.h arch/x86/entry/syscall_64.c
1.1、以sys_openat为例,在syscall_64.tbl中为
257 common openat sys_openat 441 common get_cpus sys_get_cpus
1.2、make后,在生成的syscalls_64.h中为
__SYSCALL_COMMON(257, sys_openat)
1.3 在syscall_64.c中,展开__SYSCALL_COMMON
#define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym) //展开就是 __SYSCALL_64(257, sys_openat)
1.4、在syscall_64.c中,第一次展开__SYSCALL_64
#define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *); #include <asm/syscalls_64.h> #undef __SYSCALL_64 //展开就是 extern long __x64_sys_openat(const struct pt_regs *); //也就是每个__SYSCALL_64都展开成了一个外部函数
1.5、在syscall_64.c中,第二次展开__SYSCALL_64
#define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};
//展开其实就是指向了外部函数
[257]=__x64_sys_openat,
//全部展开结果,都会被包含到sys_call_table中,从而完成了调用号与函数之间的绑定。
2、当产生系统调用时
2.1、应用直接syscall或通过glibc产生了syscall
2.2、cpu会产生类似于中断的效果,开始到entry_SYSCALL_64执行
//文件路径arch/x86/entry/entry_64.S SYM_CODE_START(entry_SYSCALL_64) //省略代码 call do_syscall_64 SYM_CODE_END(entry_SYSCALL_64) //文件路径arch/x86/entry/entry_64.S,32位兼容模式,过程与64位类似 SYM_CODE_START(entry_SYSCALL_compat) call do_fast_syscall_32 SYM_CODE_END(entry_SYSCALL_compat)
2.3、调用do_syscall_64
#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
}
instrumentation_end();
syscall_exit_to_user_mode(regs);
}
#endif
2.4、根据sys_call_table调用对应的功能函数
sys_call_table[nr](regs) 如果我们传入257,就会调用__x64_sys_openat 如果我们传入441,就会调用__x64_sys_get_cpus
2.5、但咱们实际写的函数sys_get_cpus,好像和实际调用函数__x64_sys_get_cpus,差了一个__x64,这需要一个wrapper
arch\x86\include\asm\syscall_wrapper.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
static long __do_sys_##sname(const struct pt_regs *__unused); \
__X64_SYS_STUB0(sname) \
__IA32_SYS_STUB0(sname) \
static long __do_sys_##sname(const struct pt_regs *__unused)
#define __X64_SYS_STUB0(name) \
__SYS_STUB0(x64, sys_##name)
#define __SYS_STUB0(abi, name) \
long __##abi##_##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__##abi##_##name, ERRNO); \
long __##abi##_##name(const struct pt_regs *regs) \
__alias(__do_##name);
SYSCALL_DEFINE0(get_cpus),会展开成为
__X64_SYS_STUB0(get_cpus)
//然后
__SYS_STUB0(x64, sys_get_cpus)
//然后
long __x64_sys_get_cpus(const struct pt_regs *regs);
这样前后就对上了,glibc和linux内核就通了。
Linux系统调用01
Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节先处理第一部分:
一、glibc部分
1、应用程序调用open函数
//glibc/intl/loadmsgcat.c # define open(name, flags) __open_nocancel (name, flags)
2、展开后实际上调用了
__open_nocancel(name, flags)
3、而__open_nocancel 最终调用了INLINE_SYSCALL_CALL
//glibc/sysdeps/unix/sysv/linux/open_nocancel.c __open_nocancel(name, flags) ->return INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode);
4、宏展开【理解就好,不保证顺序】
4.1、初始为
INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode); 4.2、第1次展开INLINE_SYSCALL_CALL [code lang="c"] #define INLINE_SYSCALL_CALL(...) \ __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__) //展开得到: __INLINE_SYSCALL_DISP(__INLINE_SYSCALL, __VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) 4.3、第2次展开__INLINE_SYSCALL_DISP [code lang="c"] #define __INLINE_SYSCALL_DISP(b,...) \ __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) //展开得到: __SYSCALL_CONCAT(b【__INLINE_SYSCALL】,__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】))(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)
4.4、第3次展开__INLINE_SYSCALL_NARGS
__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) #define __INLINE_SYSCALL_NARGS(...) \ __INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,) //展开得到: __INLINE_SYSCALL_NARGS_X(openat, AT_FDCWD, file, oflag, mode,7,6,5,4,3,2,1,0,) //然后展开__INLINE_SYSCALL_NARGS_X #define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n //展开得到参数个数: 4 //从而4.4的结果为 __SYSCALL_CONCAT(__INLINE_SYSCALL,4)(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)
4.5、然后展开__SYSCALL_CONCAT,其实就是字符拼接
__SYSCALL_CONCAT(__INLINE_SYSCALL,4) #define __SYSCALL_CONCAT_X(a,b) a##b #define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X (a, b) //展开得到: __INLINE_SYSCALL4 //从而4.5的结果为 __INLINE_SYSCALL4(openat, AT_FDCWD, file, oflag, mode)
4.6、然后展开INTERNAL_SYSCALL4
#define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \ INLINE_SYSCALL (name, 4, a1, a2, a3, a4) //展开得到: INLINE_SYSCALL(openat, 4, AT_FDCWD, file, oflag, mode)
4.7、展开INLINE_SYSCALL
//glibc/sysdeps/unix/sysv/linux/sysdep.h
#define INLINE_SYSCALL(name, nr, args...) \
({ \
long int sc_ret = INTERNAL_SYSCALL (name, nr, args); \
__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (sc_ret)) \
? SYSCALL_ERROR_LABEL (INTERNAL_SYSCALL_ERRNO (sc_ret)) \
: sc_ret; \
})
//展开得到
INTERNAL_SYSCALL (openat, 4, args【AT_FDCWD, file, oflag, mode】);
4.8、展开INTERNAL_SYSCALL
#define INTERNAL_SYSCALL(name, nr, args...) \ internal_syscall##nr (SYS_ify (name), args) //展开得到 internal_syscall4(SYS_ify(openat), args【AT_FDCWD, file, oflag, mode】) //展开 SYS_ify(openat) #define SYS_ify(syscall_name) __NR_##syscall_name //得到 __NR_openat //从而得到 internal_syscall4(__NR_openat, args【AT_FDCWD, file, oflag, mode】)
4.9、最后internal_syscall4中,汇编调用了syscall
glibc\sysdeps\unix\sysv\linux\x86_64\64\arch-syscall.h #define __NR_openat 257
最终,syscall时,先传入调用号257,然后是四个真正的参数。
Linux内存管理
一、整理一下思路
NUMA体系下,每个CPU都有自己直接管理的一部分内存,叫做内存节点【node】,CPU访问自己的内存节点速度,快于访问其他CPU的内存节点;
每个内存节点,按内存的迁移类型,被划分为多个内存区域【zone】;迁移类型包括ZONE_DMA、ZONE_DMA32、ZONE_NORMAL 、ZONE_HIGHMEM、ZONE_MOVABLE、ZONE_DEVICE等;
每个内存区域中,是一段逻辑上的连续内存,包括多个可用页面;但在这个连续内存中,同样有不能使用的地方,叫做内存空洞;在处理内存操作时,要避免掉到洞里;
二、整理一下结构
每个内存节点由一个pg_data_t结构来描述其内存布局;
每个pg_data_t有一个zone数组,包括了内存节点下的全部内存区域zone;
每个zone里有一个free_area数组【0-10】,其序号n的元素下面,挂载了全部的连续2^n页面【page】,也就是free_area【0-10】分别挂载了【1个页面,2个页面,直到1024个页面】
每个free_area,都有一个链表数组,按不同迁移类型,对所属页面【page】再次进行了划分
三、分配内存【只能按2^n页面申请】
alloc_pages->alloc_pages_current->__alloc_pages_nodemask ->get_page_from_freelist,快速分配路径,尝试直接分配内存 ->__alloc_pages_slowpath,慢速分配路径,尝试回收、压缩后,再分配内存,如果有OOM风险则杀死进程->实际分配时仍会调用get_page_from_freelist ->->所以无论快慢路径,都会到rmqueue ->->->如果申请一个页面rmqueue_pcplist->__rmqueue_pcplist 1、如果pcplist不为空,则返回一个页面 2、如果pcplist为空,则申请一块内存后,再返回一个页面 ->->->如果申请多个页面__rmqueue_smallest 1、首先要取得 current_order【指定页面长度】 对应的 free_area 区中 page 2、若没有,就继续增加 current_order【去找一个更大的页面】,直到最大的 MAX_ORDER 3、要是得到一组连续 page 的首地址,就对其脱链,然后调用expand函数对内存进行分割 ->->->->expand 函数分割内存 1、expand分割内存时,也是从大到小的顺序去分割的 2、每一次都对半分割,挂载到对应的free_area,也就加入了伙伴系统 3、直到得到所需大小的页面,就是我们申请到的页面了
四、此外
1、在整个过程中,有一个水位_watermark的概念,其实就是用于控制内存区是否需要进行内存回收
2、申请内存时,会先按请求的 migratetype 从对应类型的page结构块中寻找,如果不成功,才会从其他 migratetype 的 page 结构块中分配, 降低内存碎片【rmqueue->__rmqueue->__rmqueue_fallback】
3、申请内存时,一般先在CPU所属内存节点申请;如果失败,再去其他内存节点申请;具体顺序,和NUMA memory policy有关;