Unix_Linux操作系统-笔记Day3(内存管理,文件操作)

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 
#include 

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

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 
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 0
  • STDOUT_FILENO stdout 1
  • STdERR_FILENO stderr 2
  • 0,1,2都代表的是终端
#include 

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 
#include 
#include 

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 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
#include 
#include 
#include 
#include 
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

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);

你可能感兴趣的:(笔记)