@ Linux内核主要有5个子系统组成:进程调度程序(SCHED)、内核管理程序(MM)、虚拟文件系统(VFS)、网络接口(NET)、进程间通信(IPC)。其中最中心最重要的子系统是进程调度程序。其他所有子系统都依赖于进程调度程序。这些子系统之间是通过函数调用和共享数据结构进行通信的。
@ vm_area 记录着哪块虚拟内存区域被映射到哪个物理页面上
Mem_map 内存管理程序为系统上物理内存的每个页面都维护一个数据结构,该数据结构中存放着表示页面状态的标志。在内核引导时初始化,当页面状态变化时该结构的属性也随之变化。
Free_area用于存储未分配的物理内存页面。
@ tcp协议和udp协议都是在ip协议的基础上实现的。
@ 内核线程(守护进程,daemon)
内核线程是在内核模式中运行的进程,准确的说是内核的一部分。可以访问内核的所有数据结构和函数,但被当作独立的进程来对待,可以中断和恢复执行。内核线程没有虚拟内存。
@ 内存管理子系统的功能:1.大地址空间:虚拟内存可以比物理内存大许多倍;2.保护:系统中每个进程有自己的虚拟地址空间,相互之间完全分离。3.内存映射:把映像和数据文件映像到一个进程的地址空间,文件的内容被直接链接到进程的虚拟地址空间。4.公平物理内存分配;5.共享虚拟内存。
@ linux使用Buddy算法2来高效的分配和回收页块。页分配代码试图分配一个或多个物理页的一块。页面分配是以2的幂大小的块来进行。故可分配1页块,2页块,4页块等。只要系统有足够的空间页来满足请求(nr_free_pages>min_free_pages)分配代码将搜索free_area来寻找请求大小的一个页块。
@ 每个进程的虚拟内存都通过一个mm_struct数据结构表示,包含当前正执行的映像的信息和一些指向vm_area_struct数据结构的指针。每个vm_area_struct数据结构描述虚拟内存区的开始和结束、进程对该内存的访问权及对该内存的一组操作。当一个可执行映像被映射到进程虚拟地址空间时,一组vm_area_struct数据结构将被产生。每个vm_area_struct数据结构表示可执行映像的一部分。
@ linux中每个文件由一个VFS inode数据结构标识,并且每个VFS inode都是唯一的,完全描述一个且唯一的一个文件。页表的索引就由文件的VFS inode和文件内的偏移导出。Linux页缓存的作用是加速对磁盘上文件的访问,内存映射的文件每次读取一页,该页就保存在页缓存中。
@ 内核交换守护进程(kswapd)在启动时由内核初始化进程启动,并等待内核交换定时器周期性超时。超时时它将检查系统中空闲页数目是否太少。若系统中空闲页数目多于free_pages_high,内核交换守护进程就什么也不做,再次睡眠直到其定时器再次超时。若空闲页数少于free_pages_high,则内核交换守护进程将试图用三种方法来减少系统中使用的物理页数:1.减少缓冲区和页缓存的大小2.交换出system V共享内存页。3.换出及淘汰页。若空闲页数低于free_pages_low,内核交换守护进程将在下次运行前试图释放6页,否则将试着释放3页。
@ who -r可以查看当前的运行级别。
@ 类中的const数据成员只在某个对象生存期内是常量,对于整个类而言却是可变的。该类的不同对象的const数据成员可以是不同的。且const数据成员不能在声明时初始化,它只能在类构造函数的初始化表中进行(引用变量也是如此)。
Class A
{
A(int size);//构造函数
const int SIZE;
};
A::A(int size)
:SIZE(size)
{ ……
}
A a(100);//对象a的SIZE值为100
A b(200);//对象b的SIZE值为200
那么怎样才能建立在整个类中都恒定的常量呢?别指望const数据成员了,应该用类中的枚举常量来实现。如:
Class A
{
enum {SIZE1=100,SIZE2=200};
Int array1[SIZE1];
Int array2[SIZE2];
……
}
枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
@ return语句不可返回指向“栈内存”的指针或引用。因为该内存在函数体结束时被自动销毁。如:
char *Func(void)
{
char str[]="yulian"; //str的内存位于栈上
...........
Return str; //将导致错误。
}
Ps:在栈上分配的空间,在函数调用结束时自动销毁。而用动态分配在堆上的空间,需要手动销毁。函数的形参,返回地址等都在栈上分配空间,而new,malloc分配的动态内存在堆上分配。故new 了之后要用 delete。Malloc之后要用free。(new和delete会调用构造函数和虚构函数而malloc和free不会。New自动计算分配的内存大小,而malloc需显示定义分配的内存大小)
@ 引用与指针的区别:
1.引用被创建的同时必须被初始化;指针则可以在任何时候被初始化
2.不能有NULL引用,引用必须与合法的存储单元关联;指针则可以是NULL
3.一旦引用被初始化,就不能改变引用的关系;指针若不是const指针则可以改变所指的对象。
@ 内存分配方式:
1.从静态存储区域分配:内存在程序编译的时候就已经分配好,在程序的整个运行期间都存在。如全局变量,static变量。
2.在栈上创建:在执行函数时,函数内的局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3.从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存,动态内存的生存期由程序员决定,故非常灵活,但问题也最多。
@ 常见的内存错误及其对策
1.内存分配未成功,却使用了它。
此时可以加入断言进行检查如assert(p!=NULL),或用if(p==NULL)或if(p!=NULL)j进行防错处理。
2.内存分配虽然成功,但尚未初始化就引用它。
内存的缺省初值究竟是什么没有统一的标准。故无论用何种方式创建数组,都别忘了赋初值。
3.内存分配成功并且已经初始化,但操作越过了内存的边界。
如使用数组时经常发生的下标多1或少1操作
4.忘记了释放内存,造成内存泄露。
动态内存的申请与释放必须配对。程序中malloc与free的使用次数一定要相同,否则肯定有错误。New和delete也是一样的道理。
5.释放了内存却继续使用它
有三种情况:
A. 程序中的对象调用关系过于复杂 ,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应重新设计数据结构,从根本上解决对象管理的混乱局面。
B. 函数的return语句写错了。注意千万不要返回指向栈内存的指针或引用。
C. 使用free或delete释放了内存后,没有将指针设置为NULL,导致产生了“野指针”。
@ 规则:
1.用malloc或new申请了内存之后,应立即检查指针值是否为NULL。防止使用指针值为NULL的内存
2.千万别忘记为数组和动态内存赋初值。防止将未初始化的内存作为右值使用。
3.避免数组或指针的下标越界,特别要当心发生多1或少1的操作。
4.动态内存的申请与释放必须配对,防止内存泄露。
5.用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
@ 指针与数组的区别:
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变。只有数组的内容可以改变。
指针可以随时指向任意类型的内存块。它的特征是“可变”。故常用指针来操作动态内存。指针远比数组灵活,但也更危险。
@ char a[]="hello";
a[0]='X'; //a的内容可以改变。
cout<<a<<endl;
char *p="world"; //注意p指向常量字符串(位于静态存储区),常量字符串的内容 是不可以被修改的。
p[0]='X'; //编译器不能发现该错误,但是运行时将产生错误
cout<<p<<endl;
@ 不能对数组名直接进行复制和比较。可以用库函数来完成复制和比较数组。
char a[]="hello";
char b[10];
strcpy(b,a); //不能用 b=a;
If(strcmp(b,a)==0) //不能用if(b==a)
@ char a[]="hello world";
char *p=a;
cout<<sizeof(a)<<endl; //12字节,包括字符串结束符
cout<<sizeof(p)<<endl; //4字节。
sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char *)而不是p所指的内存容量。
C/C++语言没有办法知道指针所指向的内存容量。除非在申请内存时记住它。
注意:当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
如:
void func(char a[100])
{
cout<<sizeof(a)<<endl; //4字节,而不是100字节!因为此时a为char类型的指针。
}
@ free和delete只是把指针所指的内存给释放掉。并没有把指针本身干掉。指针p被free以后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾。P成了“野指针”。若此时不把p设置为NULL,会让人误以为p是个合法的指针。
如:
char *p=(char *)malloc(100);
strcpy(p,"hello");
free(p);
.........
If(p!=NULL) //没有起到防错的作用
{
strcpy(p,"world"); //出错!!!!
}
@ 如果函数的参数是一个指针,不用指望用该指针去申请动态内存。如:
void GetMemory(char *p,int num)
{
p=(char *)malloc(sizeof(char)*num);
}
void test(void)
{
char *str=NULL;
GetMemory(str,100); //str仍然为NULL
strcpy(str,"hello"); //运行错误
}
解释:编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p=p。如果函数体内的代码修改了_p的内容,则p的内容也作相应的修改。这就是指针可以用作输出参数的原因。此例中,只是把_p所指的内存地址修改了,但是p丝毫没有改变。
且此程序中每执行一次GetMemory函数就会泄露一块内存,因为没有用free释放内存。
注意:如果非得要用指针参数去申请内存,那么应该用指向指针的指针
如:
void GetMemory2(char **p,int num)
{
*p=(char *)malloc(sizeof(char )*num);
}
void test2(void)
{
char *str=NULL;
GetMemory2(&str,100);
strcpy(str,"hello");
cout<<str<<endl;
free(str);
}
上面的方法,用指向指针的指针来申请动态内存虽然方法是对的,但是理解起来比较困难。可以用函数返回值来传递动态内存!!!
如:
void *GetMemory3(int num)
{
char *p=(char *)malloc(sizeof(char )*num);
return p;
}
void test3(void)
{
char *str=NULL;
GetMemory3(100);
strcpy(str,"hello");
cout<<str<<endl;
free(str);
}
但是下面的方法是错的:
char *Get(void)
{
char p[]="hello world";
return p; //编译器提示警告。
}
void test(void)
{
char *str=NULL;
str=Get(); //str的内容是垃圾
cout<<str<<endl;
}
在上面的错误代码中作修改:
char *Get(void)
{
char *p="hello world";
return p;
}
void test5(void)
{
char *str=NULL;
str=Get();
cout<<str<<endl;
}
运行此test5函数虽然不会出错,但是函数Get的设计概念却是错误的。因为,Get内的"hello world"是常量字符串,位于静态存储区。它在程序生命期内恒定不变。无论什么时候调用Get函数,它返回的始终是同一个只读的内存块。
@ 野指针不是NULL指针,而是指向垃圾内存的指针。
野指针的成因主要有两种:
1.指针变量没有初始化。任何指针变量刚被创建时不会自动成为NULL指针,指针变量的缺省值是随机的。故,指针变量在创建的同时应当被初始化,要么将指针变量设置为NULL,要么让它指向合法的内存。如:
char *p=NULL;
char *st=(char *)malloc(100);
2.指针p被free或delete之后,没有置为NULL,让人误以为p是个合法的指针。
3.指针操作超越了变量的作用域。如下:
void test(void)
{
A *p;
{
A a;
p=&a; //注意a的生命期
}
p->func(); //p是野指针
@ malloc/free 与new/delete
malloc与free是C/C++语言的标准库函数,而new/delete是C++的运算符。都可以用于申请动态内存和释放动态内存。
对于非内部数据类型的对象而言,由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,故它们无法满足动态对象的要求,无法自动执行构造函数和虚构函数。若用malloc/free构造动态对象则要另外完成初始化和清除工作。而new与delete则可以自动调用构造函数和析构函数。所以不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。C++程序没有把malloc/free淘汰出局,因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。若用free释放new创建的动态对象,则该对象由于无法执行析构函数而可能导致程序出错。若用delete释放malloc申请的动态内存,理论上讲程序不会出错,但可读性很差。故new/delete malloc/free必须配对使用。
@ 处理内存耗尽
若在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
1.判断指针是否为NULL,是则马上用return语句终止本函数。如:
void func(void)
{
A *a=new A;
If(a==NULL)
{
return;
}
................
}
2.判断指针是否为NULL,是则马上用exit(1)终止整个程序的运行。如:
void func(void)
{
A *a=new A;
If(a==NULL)
{
cout<<"memory exhauted"<<endl;
exit(1);
}
.........
}
3.为new和malloc设置异常处理函数。
@ free(p),若p是NULL指针,那么free对p无论操作多少次都不会出问题。若p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。
@ new/delete的使用要点
运算符new使用起来比库函数malloc简单得多。如下:
int *p1=(int *)malloc(sizeof(int)*length);
int *p2=new int[length];
因为new内置了sizeof,类型转换和类型安全检查功能。
若对象有多个构造函数,则new的语句也可以有多种形式。
class Obj
{
public:
Obj(void); //无参数构造函数
Obj(int x);
...........
};
Void test(void)
{
Obj *a=new Obj;
Obj *b=new Obj(1); //初值为1
....
delete a;
delete b;
}
但是,如果用new创建对象数组,则只能使用对象的无参数构造函数。如:
Obj *objects=new Obj[100]; //创建100个动态对象。
Obj *objects=new Obj[100](1); //此法错误!!!!
而在用delete释放对象数组时,千万不要丢了符号 []
delete []objects; //正确!!!
delete objects; //错误!!! 相当于delete objects[0],漏掉了另外99个对象。
@ 对比于C语言的函数,C++增加了重载、内联、const和virtual四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,而const和virtual机制仅用于类的成员函数。
@ 全局函数和类的成员函数同名不算重载,因为函数的作用域不同。如:
void print(...); //全局函数
class A
{
...
void print(...); //成员函数
}
A a=new A;
a.print(); //调用成员函数
a.::print(); //调用全局函数
全局函数被调用时应加"::"标志。如::print(...)表示print是全局函数而非成员函数
@ 成员函数的重载、覆盖、隐藏
成员函数被重载的特征:
1.相同的范围(在同一个类中);
2.函数名字相同;
3.参数不同;
4,virtual关键字可有可无。
而覆盖是指派生类函数覆盖基类函数,特征是:
1.不同的范围(分别位于派生类和基类)
2.函数名字相同
3.参数相同
4.基类函数必须有virtual关键字。
而隐藏是指派生类的函数屏蔽了与其同名的基类函数。规则如下:
1.如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。(不是重载)
2.如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时,基类的函数将被隐藏。(不是覆盖)
相当于全局变量在模块内被局部变量所隐藏。
@ 参数的缺省值只能出现在函数的声明中,不能出现在定义体中。如:
void Foo(int x=0, int y=0); //正确!缺省值出现在函数的声明中
void Foo(int x=0, int y=0) //错误!!!缺省值不能出现在函数的定义体中
{
...........
}
且,如果函数有多个参数,参数的缺省值只能从右往左缺省。否则出错。如:
void Foo(int x, int y=0, int z=0); //正确!
void Foo(int x=0, int y, int z=0); //错误!!!
不合理的使用参数的缺省值将导致重载函数产生二义性。如:
void output(int x);
void output(int x,float y=0.0);
int x=1;
output(x); //错误!!!!!!!!!!!!将产生二义性。
@ C++中,有些运算符是不能被重载的。规则如下:
1.不能改变C++内部数据类型(如 int,float等)的运算符
2.不能重载‘.’。
3.不能重载目前C++运算符集合中没有的符号。。如#,@,$等。
4.对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。
@ 用内联取代宏代码
在C语言中,可用宏提高执行效率,预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高速度。而C++语言支持内联函数。函数内联是如何工作的?对于任何内联函数,编译器在符号表里放入函数的声明(包括函数名、参数类型、返回值类型)。若编译器没有发现内联函数存在错误,则该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查或进行自动类型转换),如果正确,内联函数的代码会直接替换函数调用,于是省去了函数调用的开销。此过程与预处理有显著的不同。因为预处理器不能进行类型安全检查或进行自动类型转换。若内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。C++语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由的操作类的数据成员。故在C++程序中,应该用内联函数取代所有宏代码。但是除了断言(assert)以外。assert是仅在debug版本起作用的宏,它用于检查“不应该”发生的情况,为了不在程序的debug版本和release版本引起差别,assert不应该产生任何副作用,若assert是函数,由于函数调用会引起内存、代码的变动,那将导致debug版本与release版本存在差异。故assert不是函数,而是宏。
@ 关键字inline必须与函数定义体放在一起才能使函数成为内联。仅将inline放在函数声明前不起任何作用。建议在使用内联函数时:声明函数时不加inline关键字,在函数定义时加上inline关键字。内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。若执行函数体内代码的时间,相比于函数调用的开销较大,则效率的收获会很少。此外,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:1.若函数体内的代码比较长,使用内联将导致内存消耗代价较高。2.若函数体内出现循环,则执行函数体内代码的时间要比函数调用的开销大。
在类声明中定义的函数自动成为内联函数。要当心构造函数和析构函数可能会隐藏一些行为,如偷偷的执行了基类或成员对象的构造函数和析构函数,故不要随便的将构造函数和析构函数的定义体放在类声明中。
@ 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其他的称为普通构造函数)。对于任意一个类A,若不想编写上述函数,C++编译器将自动为A产生四个缺省的函数。如:
A(void); //缺省的无参数构造函数
A(const A &a); //缺省的拷贝构造函数
~A(void); //缺省的析构函数
A & operte=(const A &a); //缺省的赋值函数
缺省的拷贝构造函数和缺省的赋值函数均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
构造函数与析构函数的特别之处之一是没有返回值类型。这与返回值类型为void的函数是不同的。构造函数和析构函数规定没有返回值类型。构造函数有个特殊的初始化方式叫初始化表,初始化表位于函数参数表之后,却在函数体{}之前,表明初始化表里的初始化工作发生在函数体内的任何代码被执行之前。该表的使用规则:1.若类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。2.类的const常量只能在初始化表里被初始化,不能在函数体内用赋值的方式初始化。3.类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式。效率不完全相同。对于非内部数据类型的成员对象应当在初始化表里初始化,以获取更高的效率。
@ 构造函数和析构函数的次序:构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数,析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。所以,成员对象初始化的次序完全不受它们在初始化表中次序的影响,是由成员对象在类中声明的次序决定的,因为类的构造函数可以有多个,故会有多个不同次序的初始化表,而类的声明是唯一的。若成员对象按照初始化表的次序进行构造,将导致析构函数无法得到唯一的逆序。故成员对象必须按照类中声明的次序进行初始化而与初始化表的次序无关。
@ 基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,应注意:1.派生类的构造函数应在其初始化表里调用基类的构造函数。2.基类与派生类的析构函数应该为虚(即加关键字virtual)。3.在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。
@ 一个类要继承另一个类,类的继承规则是:若在逻辑上 B 是 A 的“一种”,并且 A 的所有功能和属性对于 B 而言都有意义,则允许 B 继承 A 的功能和属性。