操作系统
Linux的内存管理
内存管理的调用关系
用户层
STL | 自动申请/自动释放 | 调用C++ |
C++ | new/delete | 调用C |
C | malloc/free | 调用POSIX 或者 Linux |
POSIX | brk/sbrk | 调用内核kernal |
Linux | mmap/munmap | 调用内核kernal |
系统层
kernal | kmalloc/vmalloc | 调用驱动 |
driver | get_free_page |
进程映像
-
程序是存储在磁盘上的可执行文件(二进制文件、脚本文件)
-
当执行程序时,系统回自动将该文件加载到内存中,在内存的分布情况称为进程映像
-
从低地址到高地址的分区:
-
text 代码段
-
data 数据段
-
bss 静态数据段
-
heap 堆
-
stack 栈:生长方向从高到低
-
environ 环境变量表
-
argv 命令行参数
-
命令:ps -aux 查看当前所有进程信息,可以查看进程号
/proc/进程号/maps
总结:
- 栈内存的增长方向受操作系统影响,大部分是从高地址向低地址增长,但也有些系统例如Ubuntu就是从低地址到高地址增长
- 如果是栈内存存储数组数据,数组中元素的增长方向一定是从低地址到高地址增长
虚拟内存
-
操作系统会为每个进程分配4G的虚拟内存(32位)
-
用户只能使用虚拟内存,不能直接使用物理内存
-
虚拟内存要与物理内存进行映射后才能被用户使用,如果使用了没有映射的虚拟内存就会产生段错误
-
虚拟内存与物理内存的映射由操作系统(MMU)动态维护
-
虚拟内存能让系统使用更安全,不会暴露真实的物理内存地址;另一方面操作系统可以让进程使用比实际物理内存更大的地址空间
-
4G的虚拟内存地址分为两个部分
- [0G~3G)用户空间
- [3G~4G)内核空间
-
当进程\线程运行在用户空间时称进程处于用户态,当进程\线程运行在内核空间时称进程处于内核态
-
当进程处于内核态时,进程运行存储使用在内核空间,此时CPU可以发出执行任何指令,运行的代码不受任何限制,可以自由地访问任意有效的地址,也可以直接访问接口
-
当进程处于用户态时,进程运行存储使用在用户空间,此时被执行的代码要受到CPU很多的检查,例如:用户进程只能访问自己映射过的内存
-
所有进程的内核空间的代码、数据都是映射在同一块物理内存中,由内核来负责维护
-
用户空间的代码不能直接访问内核空间的代码和数据,可以通过系统调用(API 调用系统接口)来切换内核态,间接的访问内核、与内核交换数据
映射虚拟内存与物理内存的函数
-
sbrk/brk/mmap/munmap
-
关于malloc获取虚拟内存空间的底层实现,跟libc.so版本有关
-
大概的逻辑:
- 如果分配的内存小于128k时,调用sbrk、brk
- 如果大于128k时,调用mmap、munmap
-
#include <unistd.h> void *sbrk(intptr_t increment); // 功能:通过increment参数调整映射位置指针的位置,既可以映射内存也可以取消映射 /* increment: 0 >0 映射内存 <0 取消映射 */ // 返回值:该指针映射前原来的位置 int brk(void *addr); // 功能:直接使用addr地址修改映射位置指针的位置 /* addr: < 原来位置指针的位置 映射内存 > 原来位置指针的位置 取消映射 */ // 返回值:成功为0,失败为-1
- 注意:sbrk、brk底层共同维护一个映射位置指针,该指针指向映射过的内存的下一个位置,或者说时未映射的内存的第一个位置
- 注意:系统映射内存是以页(1页=4096字节)为最小单位的
- 注意:sbrk、brk都可以单独进行映射、取消映射的操作,但是一般习惯配合使用:sbrk负责记录映射前的位置+映射操作,brk负责取消整个映射
-
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); // 功能:映射虚拟内存与物理内存 // addr:指定要映射的虚拟内存首地址,可以手动指定,如果是NULL,则是系统自动指定 // length:映射的长度,字节为单位 /* prot:映射后的权限 PROT_EXEC 执行权限 PROT_READ 读权限 PROT_WRITE 写权限 PROT_NONE 无权限 PROT_READ | PROT_WRITE 按位或 */ /* flags:映射方式标志 MAP_SHARED 映射内存与文件内容后如果修改内存中的数据,文件内容会随之改变 MAP_PRIVATE 映射内存与文件内容后如果修改内存中的数据,文件内容不会改变 MAP_ANONYMOUS 映射到虚拟内存,而不是映射文件 MAP_FIXED 如果手动提供的addr无法进行映射,则会执行失败(默认情况下会映射一个正确的地址,加了该标志后则不会调整) */ // fd:文件描述符,如果不映射文件,一般给0即可 // offset:文件的偏移位置,不用给0即可 // 返回值:成功返回映射后的首地址,失败返回(void*) -1\0xFFFFFFFF int munmap(void *addr, size_t length); // 功能:取消映射 // addr:要取消映射的首地址 // length:取消的字节数 // 返回值:成功返回0,失败返回-1
- 注意:mmap、munmap底层不维护任何东西
- 注意:可以使用strace ./a.out可以大概跟踪代码底层的执行情况
- sbrk底层调用了brk
- 总结:
- 重点是理解Linux内存管理机制(虚拟内存),而不是sbrk/brk/mmap/munmap系统函数的使用
- brk\sbrk底层维护一个虚拟内存位置指针,通过移动该指针来建立映射关系和取消映射关系
- mmap\munmap底层不维护任何东西,只返回一个映射后的虚拟内存地址
- malloc\free底层调用的是brk\sbrk 或者 mmap\munmap
系统调用(系统API)
- 系统调用就是操作系统提供的一些功能以函数的形式给程序员使用,但是注意,虽然格式很像标准C的函数,但是它们并不是标准C的内容,也不是真正的函数
- 一般程序大部分时间都工作在用户态(0 ~ 3G),当偶尔发生系统调用时就会工作在内核态(3 ~ 4G)
- 系统调用的代码是内核的一部分,其外部接口以函数形式定义在共享库中
- 系统调用的执行是借助软中断的方式从用户态进入到内核态后执行真正的系统调用
- ldd ./a.out
- time ./a.out 测试进程的运行时间分布
- real 0m0.002s 进程总执行时间
- user 0m0.002s 用户态执行时间
- sys 0m0.000s 内核态执行时间
- 系统调用的执行是借助软中断的方式从用户态进入到内核态后执行真正的系统调用
- time\ldd\size\strace ./a.out
Linux文件IO
一切皆文件
- UNIX\Linux系统把所有的服务、设备都抽象成文件看待,并提供了一套简单而统一的系统接口,这部分系统接口就是所谓的系统文件读写,简称系统IO
- 如果使用的是标准C库中的文件读写函数,简称为标准IO
文件的分类
文件名 | 符号 | 特点 |
---|---|---|
普通文件 | - | 包括纯文本文件、二进制文件、压缩文件等 |
目录文件 | d | 必须有x权限才能进入目录 |
块设备文件 | b | 用于存储大块数据的设备,例如硬盘 |
字符设备文件 | c | 例如键盘、鼠标 |
管道文件 | p | 有名管道为主、匿名管道文件 |
链接文件 | l | 类似于Windows的快捷方式 |
Socket文件 | s | 通常用于网络设备之间数据的连接交互 |
文件操作相关的系统调用
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
// 功能:打开文件
// pathname:文件的路径
/* flags:文件的打开方式
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
O_APPEND 追加
O_CREAT 文件不存在则创建
O_EXCL 文件存在,如果配合O_CREAT则失败
O_TRUNC 文件存在则清空打开
*/
// 返回值:文件描述符,成功返回一个非负整数,类似FILE*,代表了打开后的文件
int open(const char *pathname, int flags, mode_t mode);
// 功能:创建文件
// flags:必须为O_CREAT mode参数就需要给
/* mode:
用户:
S_IRWXU 00700(权限掩码) user(file owner) has read, write, and execute permission
S_IRUSR 00400 user has read permission
S_IWUSR 00200 user has write permission
S_IXUSR 00100 user has execute permission
同组:
S_IRWXG 00070 group has read, write, and execute permission
S_IRGRP 00040 group has read permission
S_IWGRP 00020 group has write permission
S_IXGRP 00010 group has execute permission
其他人:
S_IRWXO 00007 others have read, write, and execute permission
S_IROTH 00004 others have read permission
S_IWOTH 00002 others have write permission
S_IXOTH 00001 others have execute permission
*/
// 注意:可以直接使用八进制数表示三组权限,例如:0644、0664、0775
// 返回值:文件描述符,成功返回一个非负整数,类似FILE*,代表了打开后的文件
int creat(const char *pathname, mode_t mode);
// 功能:创建文件
// mode:同上
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
// 功能:把内存中的数据写入文件中
// fd:文件描述符,open的返回值
// buf:待写入内存首地址
// count:要写入的字节数
// 返回值:成功写入的字节数
ssize_t read(int fd, void *buf, size_t count);
// 功能:从文件中读取数据到内存中
// fd:文件描述符,open的返回值
// buf:存储数据的内存首地址
// count:要读取的字节数
// 返回值:实际读取的字节数
int close(int fd);
// 功能:关闭文件
// 返回值:成功返回0,失败返回-1
-
注意:write、read是fwrite和fread的底层调用
-
结论:
- 正常情况下,标准IO比系统IO的速度更快
-
原因:
- 标准IO中有缓冲区机制,在写数据时不是每次都调用系统IO,而是把写入的数据存储在临时的缓冲区中,当缓冲区满时,才会调用一次系统IO写入数据到文件中
- 而且直接使用系统IO时会频繁地切换内核态和用户态,非常耗时
- 但如果给系统IO增加缓冲区,它的速度一定会比标准IO快
随机读写
-
系统IO下,每个打开的文件都有一个读写的位置指针,记录了从哪个字节开始进行读写文件,并且对文件进行读写操作时,它会自动往后移动
-
当想要在任意位置进行读写文件时,可以通过改变该位置指针的位置来进行随机读写
-
#include <sys/types.h> #include <unistd.h> off_t lseek(int fd,off_t offset,int whence); // offset:偏移值 字节为单位 /* whence: 基础位置 SEEK_SET:文件开头 SEEK_CUR:当前位置 SEEK_END:文件末尾 */ // 返回值:调整后位置指针所在文件的第几个字节
-
注意:当文件位置指针越过末尾后写入数据时,越过的地方会形成“黑洞”,该段“黑洞”会计算入文件大小中,但是不占用磁盘空间
系统IO读写文本文件
- 系统IO中没有提供 fprintf/fscanf 的函数,因此不能直接读写文本文件
写文本文件
- 数据通过 sprintf 转换成字符串,然后系统IO写入
读文本文件
- 读到字符串中通过 sscanf 解析转换成对应的数据再使用
文件描述符
- 非负整数,代表了打开的文件
- 由系统调用完成后返回,要被内核空间使用
- 它代表了一个内核对象(类似文件指针),因为内核不能暴露它的地址,所以不能像文件指针返回一个对象地址
- 内核中有一张表格记录了所有打开的文件对象,使用它们所在位置的下标作为该对象的文件描述符,相当于访问文件的凭证,是当前时间内唯一的
内核中有三个默认打开的文件描述符
-
0 标准输入STDIN_FILENO stdin FILE* 1 标准输出STDOUT_FILENO stdout 2 标准错误STDERR_FILENO stderr
相关函数
#include <unistd.h>
int dup(int oldfd);
// 功能:复制一个打开的文件描述符
// 返回值:返回一个没有使用过的最小的文件描述符,失败返回-1
int dup2(int oldfd, int newfd);
// 功能:复制一个打开的文件描述符,复制成指定的值
// 返回值:返回一个,失败返回-1
// 注意:如果newfd复制前已经打开,会先关闭,后复制
// 注意:如果复制成功,相当于两个值不同的文件描述符对应同一个文件
文件同步
-
1、在写入数据时,内存与磁盘之间有一块缓冲区
- 好处是降低了磁盘读写次数,提高了读写的效率
-
2、但是这种机制带来的后果是磁盘中的数据有可能与实际写入的数据不匹配,系统提供了系统函数可以让缓冲区中的数据立即写入到磁盘
-
#include <unistd.h> void sync(void); // 功能:把缓冲区中的数据立即同步到磁盘 // 注意:并不会等待所有数据同步完成后才返回,而是发出同步指令后立即返回 int fsync(int fd); // 功能:把指定文件的内容从缓冲区同步到磁盘 // 注意:会等待同步结束后才返回 int fdatasync(int fd); // 功能:把指定文件的内容从缓冲区同步到磁盘,只同步文件的内容,不会同步文件属性 // 注意:会等待同步结束后才返回
文件属性
-
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> int stat(const char *pathname, struct stat *buf); // 功能:根据文件路径获取文件的属性 // buf:存储文件属性的结构体指针,是输出型参数 int fstat(int fd, struct stat *buf); // 功能:根据文件描述符获取文件的属性 int lstat(const char *pathname, struct stat *buf); // 功能:获取软链接文件的文件属性 struct stat { dev_t st_dev; // 设备ID ino_t st_ino; // inode节点号 mode_t st_mode; // 文件的类型和权限 nlink_t st_nlink; // 硬链接数 uid_t st_uid; // 用户ID gid_t st_gid; // 组ID dev_t st_rdev; // 特殊设备ID off_t st_size; // 总字节数 blksize_t st_blksize; // IO块字节数 blkcnt_t st_blocks; // 占用512字节的块数 struct timespec st_atim;// 最后访问时间 struct timespec st_mtim;// 最后修改时间 struct timespec st_ctim;// 属性最后修改时间 };
-
st_mode: 定义如下
-
S_IFMT 0170000 识别文件类型的掩码 S_IFSOCK 0140000 socket文件 S_IFLNK 0120000 链接文件 S_IFREG 0100000 普通文件 S_IFBLK 0060000 块设备文件 S_IFDIR 0040000 目录文件 S_IFCHR 0020000 字符设备文件 S_IFIFO 0010000 管道文件 -
注意:可以使用 st_mode & S_IFMT 的结果对比是哪个文件
-
另外:还可以使用提供的宏函数识别st_mode是哪个文件
-
宏函数 S_ISREG(st_mode) is it a regular file? S_ISDIR(m) directory? S_ISCHR(m) character device? S_ISBLK(m) block device? S_ISFIFO(m) FIFO? S_ISLNK(m) symbolic link? S_ISSOCK(m) socket?
-
文件权限
-
S_IRWXU 00700 owner has read, write, and execute permission S_IRUSR 00400 owner has read permission S_IWUSR 00200 owner has write permission S_IXUSR 00100 owner has execute permission -
S_IRWXG 00070 group has read, write, and execute permission S_IRGRP 00040 group has read permission S_IWGRP 00020 group has write permission S_IXGRP 00010 group has execute permission -
S_IRWXO 00070 others (not in group) have read, write, and execute permission -
S_IROTH 00004 others have read permission S_IWOTH 00002 others have write permission S_IXOTH 00001 others have execute permission
文件权限
-
#include <unistd.h> int access(const char *pathname, int mode); // 功能;检查当前用户对文件的权限有哪些 // pathname:待测试的文件 /* mode:想要测试的权限 R_OK 是否有读权限 W_OK 是否有写权限 X_OK 是否有执行权限 F_OK 是否存在该文件 */ // 返回值:有该权限返回0,没有返回-1 int chmod(const char *pathname, mode_t mode); // 功能:根据路径修改文件权限 /* mode:由3位八进制数组成的权限码 0644 普通文件 0755 可执行文件 */ int fchmod(int fd, mode_t mode); // 功能:根据文件描述符修改文件权限
权限屏蔽码
-
如果我们有一些权限不想用户在创建文件的时候具备,可以通过设置权限屏蔽码来屏蔽文件创建是具备某些权限
-
命令:
- umask 查看当前终端的权限屏蔽码
- umask 0xxx 临时修改当前终端的权限屏蔽码
- umask 查看当前终端的权限屏蔽码
-
函数:
-
#include <sys/types.h> #include <sys/stat.h> mode_t umask(mode_t mask); // 功能:修改当前进程权限屏蔽码 // mask:新的屏蔽码 // 返回值:原来的屏蔽码
-
注意:权限屏蔽码不会影响命令chmod 函数chmod,只会影响创建文件函数open/creat
-
注意:修改权限屏蔽码只是临时修改,如果关闭终端后,会还原,想要永久修改需要修改配置文件
-
修改文件大小
-
#include <unistd.h> #include <sys/types.h> int truncate(const char *path, off_t length); // 功能:修改文件大小 // length:想要修改后的总字节数 // 注意:length < 原总字节数 从末尾抹除多余内容 int ftruncate(int fd, off_t length); // 功能:修改文件大小
删除和重命名文件
-
#include <stdio.h> int remove(const char *pathname); // 功能:C标准库提供删除文件函数,底层调用unlink\rmdir // pathname:建议写绝对路径 // 返回值:成功返回0,失败返回1 int rename(const char *oldpath, const char *newpath); // 功能:重命名文件 // 返回值:成功返回0,失败返回1
-
#include <unistd.h> int unlink(const char *pathname); // 功能:系统调用函数,删除文件
链接文件
- Linux的文件系统主要有两个分区部分:
inode信息块区:
- 默认128B,记录了文件权限、大小、所有者、修改时间等
block数据块区:
-
默认4k,记录了文件名和文件中真正的数据内容
-
每个文件都有一个唯一的inode和若干个block,当要读取文件时需要先借助目录文件的block中记录的目录中文件的文件名和inode号来找到该文件的inode,从而读取到对应的block数据块
什么是软、硬链接文件?
硬链接文件
- 硬链接文件没有专门创建新的独属于自己的inode和block,只是在不同于被链接文件的目录中复制了一份被链接文件的inode,通过该inode也可以访问被链接文件的block
软链接文件
- 软链接文件会建立属于自己的新的 inode和block,但是block中存储的是被链接文件的inode号和文件名
区别:
- 1、删除被链接文件时,只是删除了该文件的inode信息块的内容,而不会删除block,所以硬链接文件不受影响,而软链接文件无法继续访问原文件
- 2、当硬链接数为0时,系统才认为该文件被删除
- 3、如果修改硬链接文件的内容,被链接文件也会随之更改
- 4、硬链接不能链接目录,软链接可以
- 5、可以跨文件系统创建软链接、对不存在的文件创建软链接
创建链接文件函数:
-
#include <unistd.h> int link(const char *oldpath,const char *newpath); // 功能:创建硬链接文件 // 注意:硬链接文件的类型与被链接文件相同 int symlink(const char *target, const char *linkpath); // 功能:创建软链接文件 // 注意:文件类型为l
目录文件的操作
-
#include <sys/stat.h> #include <sys/types.h> int mkdir(const char *pathname, mode_t mode); // 功能:创建一个空目录 // mode:目录的权限,要有执行权限才能进入目录
-
#include <unistd.h> int rmdir(const char *pathname); // 功能:删除空目录 int chdir(const char *path); // 功能:在当前进程中更改当前工作路径为path,相当于cd int fchdir(int fd); // 功能:更改当前工作路径为 fd目录文件描述符所在目录 char *getcwd(char *buf, size_t size); // 功能:获取当前的工作路径,相当于pwd
-
#include <sys/types.h> #include <dirent.h> DIR *opendir(const char *name); // 功能:打开一个目录 // 返回值:该目录的目录流,目录流中记录了该目录中所有的文件的信息 DIR *fdopendir(int fd); // 功能:通过目录的文件描述符,打开一个目录 // 返回值:该目录的目录流,目录流中记录了该目录中所有的文件的信息 struct dirent *readdir(DIR *dirp); // 功能:从目录流中读取一条文件记录 // 返回值:存储一个文件信息的结构体指针 struct dirent { ino_t d_ino; // inode号 off_t d_off; // 下一条记录的偏移量 unsigned short d_reclen; // 当前记录的长度 unsigned char d_type; // 文件类型 char d_name[256]; // 文件名 };