计算机基础知识 - linux和操作系统

Sep 25, 2016   #reviews  #memory  #fork 

内存

进程中内存分布

top和free可以查看系统中的内存情况。

  1. 堆(heap):动态分配,手动释放
  2. 栈(stack):大小在编译后确定;栈被放置在高地址: 调用函数(加帧)是减 esp 的,函数返回(减帧)是加 esp 的,调用在前,所以栈是向低地址扩展的,放在高地址再合适不过了。
  3. 只读数据段:文字常量区,存放常量字符串
  4. 静态存储区域:初始化/未初始化的全局变量
  5. 代码段:函数体的二进制代码

堆栈的区别:栈是静态,而堆是动态的;栈内存系统自动回收,而堆分配的内存,需手动显式回收。malloc,new分配的内存是在上,程序结束前需用free,delete回收。

int a = 0; //全局初始化区
char *p1; //全局未初始区
void main()
{
  int b; //栈
  char s[] = "abc"; //栈,运行时赋值
  char *p2; // 栈
  char *p3 = "123456"; //123456\0在常量区,p3在栈上,编译时确定
  static int c = 0; //全局(静态)初始化区
  p1 = (char *)malloc(10); //p1指向堆区
}

虚拟内存/交换空间

一种内存管理的技术,将实际上多个被分隔的物理内存碎片和磁盘存储器上的部分空间虚拟成一段连续完整的地址空间。编制程序时使用的地址称为虚地址或逻辑地址,而计算机物理内存的访问地址则称为实地址或物理地址。这里面涉及到调度算法,替换策略,更新策略等。

内存操作

malloc/calloc/realloc(头文件stdlib.h,free释放)

函数名 原型 作用
malloc void *malloc(size_t size); 动态分配内存,分配size个字节的内存空间
calloc void *calloc(size_t n, size_t size); 动态分配内存并清零,分配n个长度为size的连续空间
realloc void *realloc(void *mem_address, unsigned int newsize); 动态调整内存,指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)
memset void *memset(void *s, int ch, size_t n) char型初始化函数,n个字节的内容全部设置为ch(string.h)

malloc(size) + memset = calloc(1,size)

  1. malloc在分配了大量的内存之后,会变得越来越慢,因为malloc的分配过程是在内存管理模块的”空闲链表”里找到一个合适大小的内存返回,如果空闲链表太长,势必影响速度。

  2. new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间、调用构造函数、返回正确的指针。和delete相对应。

  3. 数据的对齐是指数据的地址和由硬件决定的内存块大小之间的关系,一个变量的地址是变量本身大小的倍数时,称为自然对齐。比如一个32bit的变量,大小是4个字节,自然对齐的情况下,地址应该是4的倍数(一个地址对应一个字节)。不对齐的数据载入可能会引起性能的下降进程的陷入

  4. malloc,calloc,realloc返回的地址对于任何的C类型都是对齐的,在32位系统以8字节为边界对齐,在64位系统以16字节为边界对齐。但对于更大的边界,如页面,需要动态对齐。

  5. posix_memalign: 函数原型:int posix_memalign(void **memptr, size_t alignment, size_t size); 分配长度为size的连续空间,地址以alignment为边界对齐,memptr保存返回的内存块地址。 成功时返回0,失败时,memptr没有被定义,错误码:EINVAL(alignment不是2的幂,或不是void指针的倍数),ENOMEM(没有足够内存),

char *buf;
int ret;
ret = posix_memalign(&buf, 256, 1024);
if(ret){
    fprintf(stderr, "posix_memalign:%s\n",strerror(ret));
    return -1;
}
free(buf); //释放内存

内存池(Memory Pool)

通常的内存分配是使用new,malloc申请,缺点:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

内存池,作为一种内存分配方式,是在真正使用内存前,先申请分配一定数量的、大小相等的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

具体代码实现

本地进程通信(IPC)

  1. 无名管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
  3. 有名管道(named pipe): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  4. 消息队列(message queue): 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 信号量(semophore): 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 信号(sinal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  7. 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  8. 套接字(socket) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

fork

一个进程,包括代码、数据和分配给进程的资源。fork通过系统调用创建一个与原来进程几乎完全相同的新进程,系统为新进程分配资源,相当于克隆了一个自己。

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;

线程与进程

具体见这里

一个进程中可以包含若干个线程,每个线程可以利用进程所拥有的资源。进程是分配资源的基本单位线程是独立运行和独立调度的基本单位

子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程可以节约CPU时间。

  1. 地址空间和其它资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  2. 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
  3. 调度和切换:线程上下文切换比进程上下文切换要快得多。
  4. 在多线程OS中,进程不是一个可执行的实体。

参考链接

  1. 进程内存分布

  2. 内存池

  3. 用C++实现自己的内存池