Day3
内存管理
自动分配/释放内存(auto_ptr | STL | 调用标准C++中的new/delete |
new/delete | 构造/析构 | C++ |
malloc/free | 标准C | 调用POSIX |
brk/sbrk | POSIX | 调用Linux系统接口 |
mmap/munmap | Linux | 调用内核接口 |
kmalloc/vmalloc | 内核 | 调用驱动 |
get_free_page | 驱动 | 。。。 |
进程映像
程序是保存在磁盘上的可执行文件,加载到内存中被操作系统调用执行的程序叫做进程(一个程序可以被同时执行多次形成不同的进程)。
进程在内存中的分布情况叫映像,从低地址到高地址一次排列是:
- 代码段/只读段
- 二进制指令,字符串字面值,具有const属性且被初始化过的全局静态变量
- 数据段
- 被初始化过的全局变量和静态变量
- bss段
- 没有被初始化过的全局变量和静态变量,进程一旦加载成功会被清理为0
- 堆
- 动态的分配,管理,需要程序员手动操作
- 栈
- 非静态的局部变量,包括函数的参数,返回值
- 从高地址向低地址使用,和堆内存存在一段空隙,防止相互影响
- 命令行参和环境变量表
- 命令行参数,环境变量表
练习七:在一个程序中打印各段内存的地址,然后与操作系统中的内存分配情况表比较,然后一一对应内存的分配情况
getpid()
获取进程的编号
cat /proc/xxx/maps
homework
虚拟内存
每一个进程都有各自独立的4G字节的虚拟地址空间,在编程时使用的永远都是这4G的虚拟地址空间中的地址,永远无法直接访问物理地址。
操作系统不让程序直接访问物理内存而只能使用虚拟地址空间,一方面为了操作系统自身的安全,另一方面可以让程序使用到比物理内存更大的内存空间(把硬盘上的特殊文件与虚拟地址空间进行映射)
4G的虚拟地址空间被分为两个部分
- 0-3G 用户空间
- 3-4G 内核空间
注意:用户空间的代码不能直接访问内核空间的代码和数据,但可以通过系统调用(不是函数,但以函数形式调用)进入到内核间接与内核交换数据。
如果使用了没有映射过的虚拟内存地址,或者访问了没有权限的虚拟内存地址,就会产生段错误(非法内存访问)。
一个进程对应一个用户空间,进程一旦切换,用户空间也会发生变化,但内核空间有操作系统管理,它不会随着进程的切换而发生变化,内核空间由内和所管理的一张独立且唯一的init_mm
表进行内存映射,而用户空间表是每一个进程一张。
注意:每个进程的内存空间完全独立,不同的进程交换虚拟内存地址没有任何意义。所以进程之间不能直接进行通信。需要由内核中转协调。
虚拟内存到物理内存映射是以页为单位的(一页 = 4K)
内存管理API
都可以进行映射内存和取消映射(系统级的内存管理)
void *sbrk(intptr_t increment);
increment:
- 0 获取未分配前的内存首地址(也就是已经分配尾地址)
- > 0 增加内存空间
- < 0 释放内存空间
返回值:未分配前的内存首地址,以字节为单位
#include <unistd.h>
#include <stdio.h>
int main(){
int* ptr = sbrk(8);
printf("%d\n",ptr[1024]);//1024会段错误,1023不会
//因为sizeof(int)*1024 = 4K = 一页
}
int brk(void *addr);
设置未分配内存的首地址
返回0成功 -1失败
int main(){
int* end = sbrk(0);
if(0 == brk(end+2));//8字节
printf("%d\n",end[1024]);
brk(end);
}
他们背后维护着一个指针,记录着未分配内存的首地址(当前堆内存的最后一个字节的下一个位置),
他们都可以进行映射内存和取消映射(系统级内存管理),但为了方便起见,sbrk
一般用于分配内存,brk
用于释放内存
注意 :二者分配和释放的都是使用权,真正的映射工作由其他系统调用完成
练习8:计算1000以内的素数,存储到堆内存中,不要浪费内存
homework练习9:使用sbrk和brk实现顺序栈,容量无限
homework
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
把虚拟内存地址与物理内存或者文件建立映射关系。
- addr:要映射的虚拟内存地址,如果为NULL操作系统会自动选择一个虚拟地址与物理内存映射
- length:要映射的字节数
- prot:权限
- flags:映射标志
- fd:文件描述符(与内存映射没有关系)
- offset:文件映射偏移值
- 返回值:映射成功后的虚拟内存地址,如果出错,返回
0xffffffff
#include <sys.mman.h>
int main(){
int *ptr = mmap(NULL,4,PROT_READ|PORT_WRITE,MAP_PRIVATE,MAP_ANONYMOUS,0,0);
if(NULL == ptr){
printf("内存映射失败\n");
return 0;
}
*ptr = 100;
//ptr[1024] = 100不一定会段错误,因为它选择的是内存中随机的空间,还是以页为单位进行映射,后面的空间有可能已经映射过了.如果给定上个例程堆的地址,会出现段错误.
printf("%d %p\n",*ptr,ptr);//如果给定的地址非法,会返回随机地址.
printf("%d\n",munmap(ptr,4));
}
int munmap(void *addr, size_t length);
取消映射
- addr:需要取消映射的内存首地址
- length:需要取消映射的字节数
- 返回值:成功0,失败-1
文件操作
系统调用
UNIX/Linux系统的绝大部分功能都是通过系统调用实现的,比如:open/close…
UNIX/Linux把系统调用都封装成了C函数的形式,但它们并不是标准C的一部分
标准库中的函数绝大部分时间都工作在用户态,但部分时间也需要切换到内核(进行了系统调用),比如:fread/fwrite/malloc/free
我们自己所编写的代码也可使进行直接调用系统接口进入内核态(进行系统调用)比如brk/sbrk/mmap/munmap
系统调用的功能代码存在于内存中,接口定义在C库中,该接口通过系统中断实现调用,而不是函数进行的跳转
注意:内核态和用户态的相互切换都会消耗时间
time a.out
real 0m0.137s #总执行时间=user+sys+切换时间
user 0m0.092s #用户态的执行时间
sys 0m0.040s #内核态的执行时间
strace a.out
可以跟踪系统调用
一切皆文件
在UNIX/Linux系统下,几乎所有资源都是以文件形式存在的,把操作系统的服务,功能,设备抽象成简单的文件,提供一套简单统一的访问接口,这样,程序就可以访问磁盘上的文件一样访问串口,终端,打印机,网络等功能
在大多数情况下,只需要 open/read/write/ioctl/close就可以实现对各种设备的输入,输出,设置,控制
UNIX/Linux下,几乎任何对象都可以当作特殊类型的文件,可以以文件的形式访问
- 目录文件 记录的是一些文件信息,相关条目
- 设备文件 /dev,所有的设备文件 stderr,stdin.stdout…
- 普通文件 链接文件,管道文件,socket文件
文件相关系统调用
open
打开或创建文件creat
创建文件close
关闭文件read
度文件write
写文件lseek
设置文件读写位置unlink
删除链接remove
删除文件
文件描述符
一个非负的整数,表示打开的文件,由系统调用open/creat/socket返回值
为什么使用文件描述符而不是像标准库那样使用文件指针
因为记录文件相关信息的结构体存储在内核中,为了不暴内核的地址,因此文件结构指针不能直接给用户操作,内核中记录了一张表,其中一列是文件描述符,对应一列文件结构指针,文件描述符就相当于获取文件结构指针的下标
内核中已经有3个已经打开的文件描述符,他们的宏定义在unistd.h
STDIN_FILENO
stdin 0STDOUT_FILENO
stdout 1STdERR_FILENO
stderr 2- 0,1,2都代表的是终端
#include <stdio.h>
int main(){
char buf[25] = {};
fscanf(stdin,"%s",buf);
//fprintf(stdin,"hh\n");
fprintf(stdout,"xx\n");
fprintf(stderr,"hehe\n");
}
open/creat/close
#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_NOCTTY
当打开的设备是终端设备文件,不要把该文件当作主控终端O_TRUNC
打开时是否清空O_APPEND
追加
int open(const char *pathname, int flags, mode_t mode);
- 打开并创建文件
pathname
:文件的路径flags
:打开文件的权限O_CREAT
文件不存在则创建O_EXCL
如果文件存在则创建失败
mode
: 设置文件的权限S_IRWXU
00700 user (file owner) has read, write, and execute permissionS_IRUSR
00400 user has read permissionS_IWUSR
00200 user has write permissionS_IXUSR
00100 user has execute permissionS_IRWXG
00070 group has read, write, and execute permissionS_IRGRP
00040 group has read permissionS_IWGRP
00020 group has write permissionS_IXGRP
00010 group has execute permissionS_IRWXO
00007 others have read, write, and execute permissionS_IROTH
00004 others have read permissionS_IWOTH
00002 others have write permissionS_IXOTH
00001 others have execute permission
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd = open("hehe.txt",O_CREAT|O_EXCL,0755);
if(fd < 0){
perror("open");
return -1;
}
else{
perror("open");
printf("文件创建成功\n");
}
}
read/write
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- 从文件中读取数据到内存
fd
文件描述符,open函数的返回值buf
数据存储位置count
读取的字节数- 返回 成功读取到字节数
ssize_t write(int fd, const void *buf, size_t count);
- 把数据写入到文件
fd
文件描述符buf
要写入的数据内存首地址count
写入的字节数- 返回值 成功写入的字节数
- 注意 如果把结构体以文本形式写入到文件,需要先把结构体转换成字符串
转换成字符串后写入:
Student stu = {"hehe",'m',18};
char buf[256] = {};
sprintf(buf,"%s %c %hd",stu.name,stu.sex,stu.age);
write(fd,buf,strlen(buf));
读取后转换:
Student stu1 = {};
char buf[256] = {};
read(fd,buf,sizeof(buf));
sscanf(buf,"%s %c %hd",stu.name,&stu.sex,&stu.age);
printf("%s %c %hd\n",stu1.mane,stu1.sex,stu1.age);