Linux内存管理

内存概述 Linux内核并不是将物理内存直接分配给进程,而是采用虚拟内存结构。每个进程拥有一个独立的虚拟内存空间和内存映射表(page table),虚拟内存到物理内存的地址映射关系记录于page table中,当CPU执行指令时,若该指令是从内存中读取或写入数据时,就需要通过MMU(Memory Manage Unit)将内存virtual address替换为physical address 物理内存 物理内存根据用途划分为不同的区域,比如:DAM, ZONE_HIGHMEM , ZONE_NORMAL , 每个区域又划分为多个page, page是物理内存进行分配的最小单位,每个page的大小是由CPU架构决定的,有些架构支持多种page size,可以在内核初始化时通过内核配置文件选择其中一种尺寸。默认采用4KB。 若是多颗处理器的机器,内存类型可能是NUMA(Non-Uniform Memory Access),CPU读取不同内存(是否与自己相邻,相邻的称为该CPU的local memory)的速度是不一样的。与之相对应的UMA读取所有内存速度一样。NUMA将内存划分为不同的bank,每个bank对应一个Node, 每个Node下面分不同区域。 虚拟内存 由于物理内存采用page作为基本单位进行分配,虚拟内存也采用page作为基本单位进行内存的分配。虚拟内存到物理内存的地址映射通过记录于page table中的映射条目完成,每个物理内存页可以映射多个虚拟内存页,这样可以实现不同进程的内存共享。 采用虚拟内存架构可以带来以下好处: 隔离用户进程和内核进程内存空间,隔离用户进程之间的内存空间: 由于进程使用的时虚拟内存地址,虚拟地址所对应的物理地址是由MMU映射完成的,所以进程无法访问其他进程的内存空间,除非有意共享。 硬件抽象: 物理内存通常比较复杂,比如多块物理内存,甚至是NUMA,虚拟内存将物理内存的各种细节隐藏,对每个进程抽象出一个统一的虚拟地址空间。 由于内核可以自由的修改虚拟内存到物理内存地址的映射,为了节省物理内存,内核并不及时将虚拟内存映射到物理内存,而是当进程真正使用到了才给其进行映射,使用多少映射多少,这就是延时分配和按需分配。 其次如果物理内存紧张,内核可以将最近没使用,且使用频率不高的占用内存的数据换出到硬盘,使用到时再换出到内存。 实现内存中的数据移动到硬盘交换分区中 实现内存区域访问权限控制 实现物理内存共享,将物理内存page映射到多个进程虚拟地址空间中 MMU MMU(Memory Manage Unit)是CPU和内存之间的一块硬件,用于virtual-memory-address 到 physical-memory-address的转换,MMU不但实现地址转换,还实现内存权限控制,管理TLB缓存等 为了加快虚拟地址到物理地址的映射速度,CPU使用TLB缓存,TLB访问速度介于CPU和CPU缓存之间,TLB缓存记录了最近的内存地址映射条目,CPU首先查找TLB中是否有映射条目,没有才取查询内存中的page table。 虚拟地址空间 Linux中,不仅是用户空间进程,内核也使用虚拟地址,虚拟地址空间被划分为内核和用户空间,虚拟地址包括以下三部分: 内核空间  Kernel Logical Address Kernel Virtual Address

process_capabilities

capabilities概述 传统的UNIX系统将进程分为两类: provileged特权进程:effective user ID是0的进程,内核对此类进程跳过所有权限检查,即有权限执行任何操作。 unprivileged非特权进程:effective user ID不是0的进程,内核对此类进程进行全面权限检查,根据effective UID, effective GID, supplementary grop list决定是否有权限执行某项操作。 传统UNIX对于普通进程执行特权操作就必须使进程的effective user ID为0(通过set-user-ID),如此进程就拥有了所有特权,若进程执行非法或意外操作将导致严重破坏。为了安全Linux-kernel-2.2后将特权(以前属于effective user ID = 0的)拆分为不同的细小单元。一个单元只拥有一部分特权。这些单元就是capabilities,其实capabilities是属于线程的特性,也就是可以给线程分配不同的capability。 capabilites 列表 capability 名称 描述 CAP_AUDIT_CONTROL 启用和禁用内核审计;改变审计过滤规则;检索审计状态和过滤规则 CAP_AUDIT_READ 允许通过 multicast netlink 套接字读取审计日志 CAP_AUDIT_WRITE 将记录写入内核审计日志 CAP_BLOCK_SUSPEND 使用可以阻止系统挂起的特性 CAP_CHOWN 修改文件所有者的权限 CAP_DAC_OVERRIDE 忽略文件的 DAC 访问限制 CAP_DAC_READ_SEARCH 忽略文件读及目录搜索的 DAC 访问限制 CAP_FOWNER 忽略文件属主 ID 必须和进程用户 ID 相匹配的限制 CAP_FSETID 允许设置文件的 setuid 位 CAP_IPC_LOCK 允许锁定共享内存片段 CAP_IPC_OWNER 忽略 IPC 所有权检查 CAP_KILL 允许对不属于自己的进程发送信号 CAP_LEASE 允许修改文件锁的 FL_LEASE 标志 CAP_LINUX_IMMUTABLE 允许修改文件的 IMMUTABLE 和 APPEND 属性标志 CAP_MAC_ADMIN 允许 MAC 配置或状态更改 CAP_MAC_OVERRIDE 忽略文件的 DAC 访问限制 CAP_MKNOD 允许使用 mknod() 系统调用 CAP_NET_ADMIN 允许执行网络管理任务 CAP_NET_BIND_SERVICE 允许绑定到小于 1024 的端口 CAP_NET_BROADCAST 允许网络广播和多播访问 CAP_NET_RAW 允许使用原始套接字 CAP_SETGID 允许改变进程的 GID CAP_SETFCAP 允许为文件设置任意的 capabilities CAP_SETPCAP 参考 capabilities man page CAP_SETUID 允许改变进程的 UID CAP_SYS_ADMIN 允许执行系统管理任务,如加载或卸载文件系统、设置磁盘配额等 CAP_SYS_BOOT 允许重新启动系统 CAP_SYS_CHROOT 允许使用 chroot() 系统调用 CAP_SYS_MODULE 允许插入和删除内核模块 CAP_SYS_NICE 允许提升优先级及设置其他进程的优先级 CAP_SYS_PACCT 允许执行进程的 BSD 式审计 CAP_SYS_PTRACE 允许跟踪任何进程 CAP_SYS_RAWIO 允许直接访问 /devport、/dev/mem、/dev/kmem 及原始块设备 CAP_SYS_RESOURCE 忽略资源限制 CAP_SYS_TIME 允许改变系统时钟 CAP_SYS_TTY_CONFIG 允许配置 TTY 设备 CAP_SYSLOG 允许使用 syslog() 系统调用 CAP_WAKE_ALARM 允许触发一些能唤醒系统的东西(比如 CLOCK_BOOTTIME_ALARM 计时器) 相关系统调用 cap_t cap_get_proc() // 获取当前进程的capalibities cap_t cap_get_pid(pid_t pid) // 获取指定进程的capalibities cap_t cap_from_text(char *buf_p) // 将文本转化为capablibities char *cap_to_text(cap_t caps, ssize_t *length_p) // 将capalibities转为文本 int cap_from_name(char *name, int *cap_p) // 将字符串表示的capability转为对应的数字 char *cap_to_name(int cap) // 将cap的数字转为文本 int cap_get_flag(cap_t cap_p, cap_value_t cap, cap_flag_t flag, cap_flag_value_t *value_p); // 设置能力,配合cap_set_proc()使用 int cap_set_proc(cap_t cap_p) //设置当前进程的能力 cap_t cap_init() // creates a capability state int cap_free() // 释放capability state内存 获取、修改文件capability的命令 获取文件capabilities

进程凭证

每个进程都有一套用数字表示的useridentifiers(UIDs)和group identifiers(GIDs),这些ID称为进程的凭证,决定了进程的权限。 UIDs和GIDs包含以下几种: 实际用户ID:real user ID(RUID) 和实际组ID: real group ID(RGID) 有效用户ID:effective user ID(EUID) 和有效组ID: effective group ID(EGID) 保存set-user-ID:saved set-user-ID(SUID)和保存set-grop-ID:saved set-group-ID(SGID) 文件系统用户ID:filesystem user ID(FUID)和文件系统组ID:filesystem group ID(FGID) 附加组ID: supplementary group IDs FUID和FGID通常与EUID和EGID一样,后文不再对其单独叙述。 UIDs, GIDs作用 RUID(RGID): 实际用户(组)ID决定了进程所属的用户和组(可能被进程执行的程序指令改变)。CentOS发行版上,当我们登录bash(即进入bash进程提供的命令行终端)时,登录程序会读取/etc/passwd文件对应用户的第三、第四字段的值作为bash进程的RUID和RGID,我们在bash里面执行的各种命令的RUID和RGID都继承自父进程bash。同样,当创建新进程时,该进程的RUID, RGID也继承自其父进程。 EUID(EGID): 有效用户(组)ID决定了进程执行各种操作时所拥有的权限,通常EUDI等同于RUID,但以下两种情况会使其不一样: 当可执行文件(程序或命令)设置了set-user-ID,set-group-ID,则执行该程序时,进程的有效用户(组)ID会被改变为可执行文件的属主ID(属组ID)。若可执行文件属主、属组ID是0(比如root),则进程就间接拥有了超级权限。 进程执行了setuid()之类的系统调用,改变了进程的有效用户ID。 SUID(SGID):save set-user-ID的初值从EUID复制而来,通常配合set-user-ID标志位使用,设置了该标志位的程序启动后,进程的EUID为程序的属主ID,从而save set-user-ID的值也为程序属主ID,进程通过seteuid()等指令可以自由的将EUID设置为RUID或save set-user-ID。当进程需要执行特权操作时,将EUID设置为save set-user-ID而获得需要的权限,而执行普通操作时,将EUID设置为RUID,降低进程的权限,保证系统安全。

进程管理

创建并运行新进程 fork(), execl(), exit(), getpid(), getppid() fork(): 当前进程(父进程)创建一个新的进程(子进程),创建后的父、子进程继续执行fork()调用点后面的程序,当然也可以使用比如execl() 来替换后续要执行的程序。 execl(): 替换当前进程后续要执行的程序(从execl()调用点后续的程序被替换,也就是说原来在execl()调用点后面的程序不再执行,而去执行execl参数中指定的程序)。 exit(): 终止一个进程,将进程占用的所有资源(内存,文件描述符)交还内核,由其进行再次分配。 getpid(): 获取当前进程PID getppid(): 获取父进程PID /* * 获取当前进程pid: int getpid(); * 获取父进程pid: int getppid(); * 执行新的程序: int execl(char *path, char *arg, ...); * 创建子进程: int fork(); * 终止进程: void exit(int status); */ #include <stdlib.h>#include <sys/types.h>#include <unistd.h>#include <stdio.h> int main(void) { long pid; pid = fork(); /* fork执行错误 */ if(pid == -1) { perror("fork"); exit(EXIT_FAILURE); } /* parent */ if(pid > 0) { int pret; printf("pid: %d, child_pid: %d\n", getpid(), pid); pret = execl("/usr/bin/sleep","sleep", "100", NULL); if(pret == -1) { perror("parent execl"); exit(EXIT_FAILURE); } } /* child */ if(pid == 0) { int cret; printf("pid: %d, parent_pid: %d\n", getpid(), getppid()); cret = execl("/usr/bin/sleep","sleep", "100", NULL); if(cret == -1) { perror("child execl"); exit(EXIT_FAILURE); } } } 输出