一、约定
1. 作业完成.
2. 50-200 lines codes.
二、课程体系
1. 语言
C C++
2. 算法
算法
数据结构
3. 基础(系统核心(Kernel)编程)
Linux/Unix Window MacOSX
PC机 服务器 ARM
任何一个操作系统都应该具备以下能力:
内存管理
文件目录
IO
进程管理
进程的创建
进程的控制
进程的通信
进程的同步
线程管理
线程创建
线程同步(含控制)
线程通信
应用
网络
数据库(pro*c/c++)
UI
shell
定位:提供编程的能力,为设备驱动与Window应用奠定基础.
三、 内存管理
1. 硬件层次
内存结构管理
2. 内核层次
内存映射
堆扩展
3. 语言层次
c :malloc // #include<stdlib.h>
c++ :new
4. 数据结构
STL Standard Template Library
申请一个容器用来存放数据,数据存放在什么位置不需要管理,其实这里面隐藏着对内存的托管,其解决的就是内存中的一些细节问题。但其有两个瓶颈效应:1. 对多线程支持存在问题。2. 对共享内存的实现存在问题
智能指针
5. Linux对内存的结构描述
5.1 proc目录
在Linux 下,根目录/proc/${pid 进程ID}/这个目录下存放这每个程序运行的所有信息,包括内存结构。
- exe执行程序文件,它指向我们的执行程序。
- cwd指向当前的工作路径,这是个链接方式
- fd 是这个程序打开的所有的文件
- cpuset存放的cpu信息
- cmdline 命令行
- environ 环境参数和环境变量
- maps 程序运行的所有内存结构
查看程序maps结构
在第一个终端下运行下面程序,得到pid=4270
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("%d\n", getpid());
getchar();
return 0;
}
在第二个终端下cd到根目录下,打开
可以看到程序运行的内存结构以及各部分的地址分配。
任何程序的内存空间分成4个基本部分
1)代码区
2)全局栈区
3)堆
4)局部栈
5.2 内存数据结构
运行代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int add(int a, int b){return a+b;}
int a1 = 1;
static int a2 = 2;
const int a3 = 3;
int main()
{
int b1=4;
static int b2=5;
const int b3=6;
int* p1 = malloc(4);
printf("a1=%p\n", &a1);
printf("a2=%p\n", &a2);
printf("a3=%p\n", &a3);
printf("b1=%p\n", &b1);
printf("b2=%p\n", &b2);
printf("b3=%p\n", &b3);
printf("p1=%p\n", p1);
printf("mian=%p\n", main);
printf("func=%p\n", add);
printf("pid=%d\n", getpid());
getchar();
return 0;
}
可以看到
代码区 --- 存放函数地址 + 全局 const 对象
全局栈区 --- 存放全局变量 + static 修饰对象
局部栈区 --- 存放局部变量 + 局部 const 对象
堆 --- 存放 malloc 对象 + new 对象(这里没举例子)
5.3 自加载器
解决代码拷贝到内存中的问题,自加载器负责调用程序文件,找到main函数,拷贝代码到指定位置。
5.4 malloc 与 new
用堆分配空间,分配4字节空间占却用16字节(我这边事64位ubuntu 占用32字节)的实际空间:因为堆是靠链表来维护的。链表中不仅仅包含当前分配空间大小的信息,分配的空间/上一个空间数据/下一个空间/空间大小等信息,所以占用空间比分配空间要更大。
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 这里不需要进行类型转换
// 因为C语言和ObjectC语言是一样的,是弱类型语言,很多类型他是自己去判定
int *p1 = malloc(4);
int *p2 = malloc(4);
int *p3 = malloc(4);
int *p4 = malloc(4);
int *p5 = malloc(4);
printf("%p\n", p1);
printf("%p\n", p2);
printf("%p\n", p3);
printf("%p\n", p4);
printf("%p\n", p5);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p1 = new int;
int *p2 = new int;
int *p3 = new int;
int *p4 = new int;
int *p5 = new int;
printf("%p\n", p1);
printf("%p\n", p2);
printf("%p\n", p3);
printf("%p\n", p4);
printf("%p\n", p5);
return 0;
}
运行可以看到地址分配的间隔 32 字节
malloc 和 new 的关系
相同:new的实现使用的是malloc来实现的。
区别:new使用malloc后,还要初始化空间、基本类型、直接初始化成默认值。UDT(用户自定义类型)类型,调用指定的构造器
delete调用free实现:delete负责调用析构器,然后在调用free。
new与new[]区别
1)new只调用一个析构器初始化.
2)new[]循环对每个区域调用构造器.
delete 与delete[]
1)delete 只调用一个构造器然后在调用free。
2)delete[]循环对每个区域调用构造器、free。.
5.5 堆和栈
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int a1 = 10;
int a2 = 20;
int a3 = 30;
int * p1 = malloc(0);
int * p2 = malloc(0);
int * p3 = malloc(0);
printf("a1=%p\n", &a1);
printf("a2=%p\n", &a2);
printf("a3=%p\n", &a3);
printf("p1=%p\n", p1);
printf("p2=%p\n", p2);
printf("p3=%p\n", p3);
printf("pid=%d\n", getpid());
getchar();
return 0;
}
a1、a2、a3在局部栈,p1、p2、p3在堆,观察地址变化和递增大小。
5.6 函数调用栈空间的分配与释放
#include <stdio.h>
int add(int a,int b)
{
return a+b;
}
int main()
{
int (*func)(int) = (int(*)(int))add;
int r = func(20);
printf("%d\n", r);
return 0;
}
1)函数执行的时候有自己的临时栈。
2)函数的参数就在临时栈中,如果函数传递实参,则用来初始化临时参数变量。
3)通过寄存器返回值,(使用返回值返回数据)
4)通过参数返回值,(参数必须是指针),指针指向的区域必须事先分配。
5)如果参数返回指针,参数就是双指针.
__stdcall __cdecl __fastcall
1)决定函数栈压栈的参数顺序.
2)决定函数栈的清空方式
3)决定了函数的名字转换方式.
拓展
6.5.2 拓展 | __stdcall | __cdecl | __fastcall |
参数传递方式 | 右->左 压栈 | 右->左 压栈 | 左边开始的两个不大于4字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送 |
清理栈方 | 被调用函数清理(即函数自己清理),多数据情况使用这个 | 调用者清理 | 被调用者清理栈 |
适用 场合 | Win API | c/C++MFC默认方式 可变参数的时候使用 | 速度快 |
C编译修饰约定(它们均不改变输出函数名中的字符大小写) | 约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为 _functionname@number | 约定仅在输出函数名前加上一个下划线前缀,格式为_functionname | 调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为 @functionname@number。 |
C++ 修饰 | 1. 修饰名(Decoration name) “C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。 修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。 2. 名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。 a、C编译时函数名修饰约定规则: __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。 __cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。 __fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。 它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。 b、C++编译时函数名修饰约定规则: __stdcall调用约定: 1. 以“?”标识函数名的开始,后跟函数名; 2. 函数名后面以“@@YG”标识参数表的开始,后跟参数表; 3. 参数表以代号表示: X--void, D--char, E--unsigned char, F--short, H--int, I--unsigned int, J--long, K--unsigned long, M--float, N--double, _N--bool, .... PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复; 4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前; 5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。 其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如 int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z” void Test2()-----“?Test2@@YGXXZ” __cdecl调用约定: __fastcall调用约定: 注意: 1、_beginthread需要__cdecl的线程函数地址,_beginthreadex和CreateThread需要__stdcall的线程函数地址。 2、一般WIN32的函数都是__stdcall。而且在Windef.h中有如下的定义: #define CALLBACK __stdcall #define WINAPI __stdcall 3、extern "C" _declspec(dllexport) int __cdecl Add(int a, int b); typedef int (__cdecl*FunPointer)(int a, int b); 修饰符的书写顺序如上。 4、extern "C"的作用:如果Add(int a, int b)是在c语言编译器编译,而在c++文件使用,则需要在c++文件中声明:extern "C" Add(int a, int b),因为c编译器和c++编译器对函数名的解释不一样(c++编译器解释函数名的时候要考虑函数参数,这样是了方便函数重载,而在c语言中不存在函数重载的问题),使用extern "C",实质就是告诉c++编译器,该函数是c库里面的函数。如果不使用extern "C"则会出现链接错误。 一般象如下使用: #ifdef _cplusplus #define EXTERN_C extern "C" #else #define EXTERN_C extern #endif #ifdef _cplusplus extern "C"{ #endif EXTERN_C int func(int a, int b); #ifdef _cplusplus } #endif 5、MFC提供了一些宏,可以使用AFX_EXT_CLASS来代替__declspec(DLLexport),并修饰类名,从而导出类,AFX_API_EXPORT来修饰函数,AFX_DATA_EXPORT来修饰变量 AFX_CLASS_IMPORT:__declspec(DLLexport) AFX_API_IMPORT:__declspec(DLLexport) AFX_DATA_IMPORT:__declspec(DLLexport) AFX_CLASS_EXPORT:__declspec(DLLexport) AFX_API_EXPORT:__declspec(DLLexport) AFX_DATA_EXPORT:__declspec(DLLexport) AFX_EXT_CLASS:#ifdef _AFXEXT AFX_CLASS_EXPORT #else AFX_CLASS_IMPORT 6、DLLMain负责初始化(Initialization)和结束(Termination)工作,每当一个新的进程或者该进程的新的线程访问DLL时,或者访问DLL的每一个进程或者线程不再使用DLL或者结束时,都会调用DLLMain。但是,使用TerminateProcess或TerminateThread结束进程或者线程,不会调用DLLMain。 7、一个DLL在内存中只有一个实例 DLL程序和调用其输出函数的程序的关系: 1)、DLL与进程、线程之间的关系 DLL模块被映射到调用它的进程的虚拟地址空间。 DLL使用的内存从调用进程的虚拟地址空间分配,只能被该进程的线程所访问。 DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。 DLLDLL可以有自己的数据段,但没有自己的堆栈,使用调用进程的栈,与调用它的应用程序相同的堆栈模式。 2)、关于共享数据段 DLL定义的全局变量可以被调用进程访问;DLL可以访问调用进程的全局数据。使用同一DLL的每一个进程都有自己的DLL全局变量实例。如果多个线程并发访问同一变量,则需要使用同步机制;对一个DLL的变量,如果希望每个使用DLL的线程都有自己的值,则应该使用线程局部存储(TLS,Thread Local Strorage)。 |
5.7 far near huge指针
near 16位指针
far 32位指针
huge 综合
四、 虚拟内存
1. 介绍
- 1. 一个程序不能访问另外一个程序的地址指向的空间.
- 2. 每个程序的开始地址0x80084000
- 3. 程序中使用的地址不是物理,而是逻辑地址(虚拟内存)。逻辑地址仅仅是编号,编号使用int 4字节整数表示 4294967296 每个程序提供了4G的访问能力
- 4. 逻辑地址与物理地址关联才有意义:映射过程称为内存映射.
- 5. 虚拟内存的提出:禁止用户直接访问物理存储设备,有助于系统的稳定。
- 虚拟地址与物理地址映射的时候有一个基本单位:【 4k=1000(16进制) =内存页】.
段错误:无效访问,访问的虚拟内存地址没有映射到实际物理内存上。
合法访问: 比如malloc分配的空间之外的空间可以访问,虽然不会有段错误但访问非法。
2. 虚拟内存的分配
栈上的数据:编译器自动生成代码维护
堆上的数据:地址是否映射,映射的空间是否被管理。
1)brk/sbrk 内存映射函数
补充:帮助手册
man + 节 + 关键字
1-8节:不指定节会自动搜索
- 1 = Linux系统(shell)指令
- 2 = 系统函数
- 3 = 标准C函数
- 4 = 特殊文件,例如设备和驱动程序
- 5 = 文件格式,包括配置文件,网络服务列表,可用的shell列表等
- 6 = 游戏和屏幕保护程序
- 7 = 系统编程帮助
- 8 = 系统管理命令,超级用户可能需要用到它们。
int brk(void *end);
- 移动虚拟内存指针的绝对位置
- 如果新位置(地址)大于当前位置(地址 ),则地址拓展部分被分配空间,可以使用。
- 如果新位置(地址)小于当前位置(地址 ),则地址覆盖部分被擦除,不可以使用。
void *sbrk(int size);
- 返回当前地址位置。
- 如果size 等于 0;返回当前地址,系统会分配一个分页(4k)的空白空间,此时此时没有映射到物理内存;
- 如果size 不等于 0,先返回当前地址,再把地址偏移size大小,此时此时映射到物理内存,大小等于size。
原笔记内容
应用:
1.使用sbrk分配空间
2.使用sbrk得到没有映射的虚拟内存首地址.
3.使用brk分配空间
4.使用brk释放空间
理解:
sbrk(int size)
sbrk与brk后台系统维护一个指针,指针默认是null。调用sbrk,判定指针是否是0。
是:得到大块空闲空间的首地址初始化并返回指针;
否:先返回指针地址,再把指针位置+size。
注意:如果是第一次运行该函数,会有如下区别
1. size==0?。为0,则直接返回首地址,但内存页没有映射到物理内存;否,完成一个内存页的虚拟内存到物理内存的映射,但只是完成size大小的内存空间,此时在一页(1024字节,即4k)内可以越界访问,不会有段错误,但是会有危险。
2. sbrk 与 brk 本质上市一样的,区别在于移动相对位置和绝对位置。
测试代码
// break.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("p1=%p\n", sbrk(0));
printf("p2=%p\n", sbrk(0));
printf("p3=%p\n", sbrk(0));
printf("p4=%p\n", sbrk(32));
printf("p5=%p\n", sbrk(0));
printf("p6=%p\n", sbrk(0));
printf("p7=%p\n", sbrk(0));
printf("%d\n", getpid());
getchar();
return 0;
}
// break2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int * p1 = sbrk(0);
int * p2 = sbrk(0);
int * p3 = sbrk(0);
int * p4 = sbrk(32);
int * p5 = sbrk(0);
int * p6 = sbrk(0);
int * p7 = sbrk(0);
printf("p1=%p\n", p1);
printf("p2=%p\n", p2);
printf("p3=%p\n", p3);
printf("p4=%p\n", p4);
printf("p5=%p\n", p5);
printf("p6=%p\n", p6);
printf("p7=%p\n", p7);
printf("%d\n", getpid());
getchar();
return 0;
}
// break3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("p1=%p\n", (int*)sbrk(0));
printf("p2=%p\n", (int*)sbrk(0));
printf("p3=%p\n", (int*)sbrk(0));
printf("p4=%p\n", (int*)sbrk(32));
printf("p5=%p\n", (int*)sbrk(0));
printf("p6=%p\n", (int*)sbrk(0));
printf("p7=%p\n", (int*)sbrk(0));
printf("%d\n", getpid());
getchar();
return 0;
}
运行结果,几次运行结果不一样,不知道为什么。
练习代码
#include <stdio.h>
#include <unistd.h>
int isPrimer(int a)
{
int i;
for(i=2;i<a;i++)
{
if(a%i==0)
{
return 1;
}
}
return 0;
}
main()
{
int i=2;
int b;
int *r;
int *p;
p=sbrk(0);
r=p;
for(;i<100;i++)
{
b=isPrimer(i);
if(b==0)
{
brk(r+1);
*r=i;
r=sbrk(0);
}
}
i=0;
r=p;
while(r!=sbrk(0))
{
printf("%d\n",*r);
r++;
}
brk(p);//释放内存
}
总结:
智能指针
stl
new
malloc
brk / sbrk
异常处理
int brk(void*)
void *sbrk(int);
如果成功.brk返回0、sbrk返回指针,失败 brk返回-1、sbrk返回(void*)-1
Unix函数错误,修改内部变量:
errno
字符串函数 string.h cstring
内存管理函数 malloc memset mamcmp memcpy bzero 等等
错误处理函数
标准IO函数
时间函数
类型转换函数
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
extern int errno;
int main()
{
void *p=sbrk(1000000000*2);
if(p==(void*)-1)
{
//C语言几种错误打印方式
//perror("Hello:");
//printf("Memory:%m\n");
printf("::%s\n",strerror(errno));
}
}