概念:
进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
区别:
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
多进程:操作系统中同时运行的多个程序
多线程:在同一个进程中同时运行的多个任务
优缺点:
多进程编程和多线程编程,都可以使用并行机制来提升系统的运行效率。二者的区别在于运行时所占的内存分布不同,多钱程是共用一套内存的代码块区间;而多进程是各用一套独立的内存区间。
多进程的优点是稳定性好,一个子进程崩溃了,不会影响主进程以及其余进程。基于这个特性,常常会用多进程来实现守护服务器的功能。
多进程编程也有不足,即创建进程的代价非常大,因为操作系统要给每个进程分配固定的资源,并且操作系统对进程的总数会有一定的限制,若进程过多,操作系统调度都会存在问题,会造成假死状态。
多线程编程的优点是效率较高一些,适用于批处理任务等功能;不足之处在于,任何一个线程崩溃都可能造成整个进程的崩溃,因为它们共享了进程的内存资源池。
1)多进程模型的优势是CPU
2)多线程模型主要优势为线程间切换代价较小,因此适用于I/O密集型的工作场景,因此I/O密集型的工作场景经常会由于I/O阻塞导致频繁的切换线程。同时,多线程模型也适用于单机多核分布式场景。
进程间的通信方式:
管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (namedpipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号量(semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( messagequeue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 (sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存(shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字(socket ) :
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
线程间的通信方式:
锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
Linux下使用虚拟内存空间给每一个进程,32位操作系统下,每个进程都有独立的4G虚拟内存空间。
其中包括:
1、内核区:用户代码不可见的区域,页表就存放在这个区域中。
2、用户区:
a、代码段:只可读,不可写,程序代码段。
b、数据段:保存全局变量,静态变量的区域。
c、堆区:就是动态内存,通过malloc,new申请内存,有一个堆指针,可以通过brk系统调用调整堆指针。
d、文件映射区域:通过mmap系统调用,如动态库,共享内存等映射物理空间的内存区域。可以单独释放,不会产生内存碎片。
e、栈区:用于维护函数调用的上下文空间,用ulimit -s 查看。一般默认为8M
关于资源:子进程得到的是除了代码段是与父进程共享的以外,其他所有的都是得到父进程的一个副本,子进程的所有资源都继承父进程,得到父进程资源的副本,既然为副本,也就是说,二者并不共享地址空间。两个是单独的进程,继承了以后二者就没有什么关联了,子进程单独运行。(采用写时复制技术)
关于文件描述符:继承父进程的文件描述符时,相当于调用了dup函数,父子进程共享文件表项,即共同操作同一个文件,一个进程修改了文件,另一个进程也知道此文件被修改了。
(1)互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
(2)同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
(3)同步其实已经实现了互斥,所以同步是一种更为复杂的互斥。
(4)互斥是一种特殊的同步。
同步是个过程,阻塞是线程的一种状态。多个线程操作共享变量时可能会出现竞争。这时需要同步来防止两个以上的线程同时进入临界区,在这个过程中,后进入临界区的线程将阻塞,等待先进入的线程走出临界区。
线程同步不一定发生阻塞!!!线程同步的时候,需要协调推进速度,互相等待和互相唤醒会发生阻塞。
1)正常进程
正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息,直到父进程通过wait / waitpid来取时才释放。保存信息包括:
1进程号the process ID
2退出状态the termination status of the process
3运行时间the amount of CPU time taken by the process等
2)孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
3)僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
僵尸进程是一个进程必然会经过的过程:这是每个子进程在结束时都要经过的阶段。
如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
3)守护进程
守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。守护进程是一个在后台运行并且不受任何终端控制的进程。Unix操作系统有很多典型的守护进程(其数目根据需要或20—50不等),它们在后台运行,执行不同的管理任务。
方法一:父进程回收法
wait函数将使其调用者阻塞,直到其某个子进程终止。故父进程可调用wait函数回收其僵尸子进程。
方法二:init进程回收法
属性:
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
特征:
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。 获得了break指针的位置也就得到了堆内存申请的起始地址。
malloc实际上是将可用空间用一个空闲链表连接起来,若用户申请空间,就遍历该链表,找到第一个满足条件的空闲块,将其进行拆分,返回合适大小的空间给用户,将剩下的部分链接到链表中。当调用free释放空间时,会把这块空间连接到空闲链表上。到最后,该空闲链就会被切成很多的小内存块,一旦用户申请一块较大的空间,空闲链中的空间大小都无法满足需求,malloc会申请延时,对空闲链表进行检查,内存重新整理,把相邻的小片段合并成大的空闲块。
malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。程序运行时,堆空间只是程序向操作系统申请划出来的一大块虚拟地址空间。应用程序通过malloc申请空间,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
共性:
1)都是地址的概念,指针指向某一内存、它的内容是所指内存的地址;引用则是某块内存的别名。
2)从内存分配上看:两者都占内存,程序为指针会分配内存,一般是4个字节;而引用的本质是指针常量,指向对象不能变,但指向对象的值可以变。两者都是地址概念,所以本身都会占用内存。
区别:
1)引用必须被初始化,指针不必。
2)引用初始化以后不能被改变,指针可以改变所指的对象。
3)不存在指向空值的引用,但是存在指向空值的指针。
转换:
指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。
引用转指针:把引用类型的对象用&取地址就获得指针了。
int a = 5;
int *p = &a;
void fun(int &x){}//此时调用fun可使用 : fun(*p);
//p是指针,加个*号后可以转换成该指针指向的对象,此时fun的形参是一个引用值,
//p指针指向的对象会转换成引用X。
注:没有指向引用的指针,因为引用是没有地址的,但是有指针的引用,因为引用不是对象,没有地址
一个程序本质上都是由BSS段、data段、text段三个组成的。可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。
BSS段(未初始化数据区):通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。
数据段:存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配
代码段:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量
text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。
bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。
数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。
可执行程序在运行时又多出两个区域:栈区和堆区。
**栈区:**由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
**堆区:**用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
__stdcall和__cdecl都是函数调用约定关键字:
__stdcall:参数由右向左压入堆栈;堆栈由函数本身清理。
__cdecl:参数也是由右向左压入堆栈;但堆栈由调用者清理。
相同点:
struct能包含成员函数吗? 能!
struct能继承吗? 能!!
struct能实现多态吗? 能!!!
不同点:
(1)关于使用大括号初始化
class和struct如果定义了构造函数,就不能用大括号进行初始化了;若没有定义,struct可以用大括号初始化,而class只有在所有成员变量全是public的情况下,才可以用大括号进行初始化。
#include
using namespace std;
struct SA
{
int a;
int b;
};
SA data1={2,3}; //程序正确
struct SB
{
int a;
int b;
SB(int x,int y)
:a(x)
,b(y)
{}
~SB()
{}
};
SB data2={2,3}; //程序错误
class CA
{
public:
int a;
int b;
};
CA data3={2,3};//正确
class CB
{
public:
int a;
int b;
CB(int x,int y)
:a(x)
,b(y)
{}
~CB()
{}
};
CB data4={2,3}; //错误
class CC
{
public:
CC(int x,int y)
:a(x)
,b(y)
{}
~CC()
{}
private:
int a;
int b;
};
CC data5={2,3};//错误
(2)关于默认权限访问
class中默认成员访问权限是private,而struct的默认访问权限是public
(3)关于继承方式
class中默认继承方式是private,而struct的默认继承方式是public,具体代码如下
将字符char类型转换成int整型
将字符char类型转换成int整型的方法如下:
char str_data='5';
int int_data=(int)(str_data-'0');
cout<<int_data<<endl;
同样的道理将int转换成字符char是利用相反的方法;
int data_int=9;
char data_str=(char)(data_int+'0');
cout<<data_str<<endl;
需要注意的是因为字符只能是单个的字符,所以这种方法处理的int只能是个位数即0-9;
将字符串转化成int
将字符串转化成int的函数是atoi(atoi是C语言库函数)
char p[]="123456";
long a=atoi(p);
cout<<a<<endl;
将int整型转化成字符串型
string str = "123456";
int a= to_string(str);
全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
编译器处理不同
宏定义是一个“编译时”概念,在预处理阶段展开(在编译时把所有用到宏定义值的地方用宏定义常量替换),不能对宏定义进行调试,生命周期结束于编译时期;
const常量是一个“运行时”概念,在程序运行使用,类似于一个只读行数据
存储方式不同
宏定义是直接替换,不会分配内存,存储与程序的代码段中;
const常量需要进行内存分配
类型和安全检查不同
宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;
const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查
作用:
Volatile意思是“易变的”,应该解释为“直接存取原始内存地址”比较合适。 “易变”是因为外在因素引起的,像多线程,中断等;
C语言书籍这样定义volatile关键字:volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)
下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
几个问题:
1)一个参数既可以是const还可以是volatile吗?
可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2) 一个指针可以是volatile 吗?
可以,当一个中服务子程序修改一个指向buffer的指针时。
3). 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
这段代码的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
注意:频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。
1.指针:指针代表一个变量的地址;
例如:
int a =2,b = 1,*pi = &a;
pi= &b;
2.引用:引用即针对一个变量的别名,引用必须被初始化,引用作为参数(形参)时,不会像指针一样使用存储单元,更不会像值传递一样创建该参数的副本,提高空间/时间效率。
例如:int a=2,&b = a;
3.常量引用:格式为 const 变量类型 &变量名,当声明该引用时,不可通过引用对其目标变量的值进行修改,变量自身可以修改,可用于保证函数内形参不可更改,也就是保证传入的实参为常量。
4.指向常量的指针:(《C++ Primer》书中名字是指向常量的指针,网上的叫法是“常量指针”),const int *p
;其本质为一个指针,因为该指针指向一个常量,所以不能通过该指针修改常量的值,但该指针指向的也可为变量,重点在于不能通过该指针修改指向变量(常量)的值;
5.常量指针:(《C++ Primer》书中名字是常量指针,网上的叫法是“指针常量”),int* const p
;其本质为一个常量,所以其指向的值可以改变,但是由于指针为常量,所以声明时必须初始化,且初始化后存放在指针中那个地址不可改变,此地址对应的非常量值仍可被改变。
总结: const在 * 的左边,则为指向常量的指针,即指针指向的变量的值不可直接通过指针改变(可以通过其他途径改变);const在 * 的右边,则为常量指针,即指针的指向不可变。简记为const的 “左定值,右定向”。
对局部变量的两点说明:
1)main() 也是一个函数,在 main() 内部定义的变量也是局部变量,只能在 main() 函数内部使用。
2)形参也是局部变量,将实参传递给形参的过程,就是用实参给局部变量赋值的过程,它和a=b; sum=m+n;这样的赋值没有什么区别。
全局变量:
全局变量的默认作用域是整个程序,也就是所有的代码文件,包括源文件(.c文件)和头文件(.h文件)。如果给全局变量加上 static 关键字,它的作用域就变成了当前文件,在其它文件中就无效了。我们目前编写的代码都是在一个源文件中,所以暂时不用考虑 static 关键字
1) 连接
TCP是面向连接的传输层协议,即传输数据之前必须先建立好连接。
UDP无连接。
2) 服务对象
TCP是点对点的两点间服务,即一条TCP连接只能有两个端点;
UDP支持一对一,一对多,多对一,多对多的交互通信。
3) 可靠性
TCP是可靠交付:无差错,不丢失,不重复,按序到达。
UDP是尽最大努力交付,不保证可靠交付。
4)拥塞控制,流量控制
TCP有拥塞控制和流量控制保证数据传输的安全性。
UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。
5) 报文长度
TCP是动态报文长度,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。
UDP面向报文,不合并,不拆分,保留上面传下来报文的边界。
6)首部开销
TCP首部开销大,首部20个字节。
UDP首部开销小,8字节。(源端口,目的端口,数据长度,校验和)
从特点上我们已经知道,TCP 是可靠的但传输速度慢,UDP 是不可靠的但传输速度快。因此在选用具体协议通信时,应该根据通信数据的要求而决定。
若通信数据完整性需让位与通信实时性,则应该选用TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)。
(1)序列号、确认应答、超时重传
数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号。如果发送发迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一定时间后会进行重传。这个时间一般是2*RTT(报文段往返时间)+一个偏差值。
(2)窗口控制与高速重发控制/快速重传(重复确认应答)
TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定要等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停地发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会立刻进行重发;但还有种情况有可能是数据都收到了,但是有的应答丢失了,这种情况不会进行重发,因为发送端知道,如果是数据段丢失,接收端不会放过它的,会疯狂向它提醒…
(3)拥塞控制
拥塞控制是防止过多的数据注入网络,使得网络中的路由器或者链路过载。流量控制是点对点的通信量控制,而拥塞控制是全局的网络流量整体性的控制。发送双方都有一个拥塞窗口——cwnd。
慢启动:
最开始发送方的拥塞窗口为1,由小到大逐渐增大发送窗口和拥塞窗口。每经过一个传输轮次,拥塞窗口cwnd加倍。当cwnd超过慢开始门限,则使用拥塞避免算法,避免cwnd增长过大。
拥塞避免:
每经过一个往返时间RTT,cwnd就增长1。
在慢开始和拥塞避免的过程中,一旦发现网络拥塞,就把慢开始门限设为当前值的一半,并且重新设置cwnd为1,重新慢启动。(乘法减小,加法增大)
快速重传
接收方每次收到一个失序的报文段后就立即发出重复确认,发送方只要连续收到三个重复确认就立即重传(尽早重传未被确认的报文段)。
快恢复
当发送方连续收到了三个重复确认,就乘法减半(慢开始门限减半),将当前的cwnd设置为慢开始门限,并且采用拥塞避免算法(连续收到了三个重复请求,说明当前网络可能没有拥塞)。
采用快恢复算法时,慢开始只在建立连接和网络超时才使用。
OSI七层模型及其包含的协议如下:
物理层: 通过媒介传输比特,确定机械及电气规范,传输单位为bit,主要包括的协议为:IEE802.3 CLOCK RJ45
数据链路层: 将比特组装成帧和点到点的传递,传输单位为帧,主要包括的协议为MAC VLAN PPP
网络层:负责数据包从源到宿的传递和网际互连,传输单位为包,主要包括的协议为IP ARP ICMP
传输层:提供端到端的可靠报文传递和错误恢复,传输单位为报文,主要包括的协议为TCP UDP
会话层:建立、管理和终止会话,传输单位为SPDU,主要包括的协议为RPC NFS
表示层: 对数据进行翻译、加密和压缩,传输单位为PPDU,主要包括的协议为JPEG ASII
应用层: 允许访问OSI环境的手段,传输单位为APDU,主要包括的协议为FTP HTTP DNS
TCP/IP 4层模型包括:
网络接口层:MAC VLAN
网络层:IP ARP ICMP
传输层:TCP UDP
应用层:HTTP DNS SMTP
经典五层模型:
应用层、传输层、网络层、数据链路层、物理层
1,HTTP/1.0协议使用非持久连接,即在非持久连接下,一个tcp连接只传输一个Web对象,;
2,HTTP/1.1默认使用持久连接(然而,HTTP/1.1协议的客户机和服务器可以配置成使用非持久连接)。
在持久连接下,不必为每个Web对象的传送建立一个新的连接,一个连接中可以传输多个对象
HTTP/1.0规定浏览器与服务器只保持短暂的连接,浏览器每次都需要与服务器建立一个TCP连接,服务器完成请求后,立即断开TCP连接,也就是说,同一个客户第二次访问同一个服务器上的页面时,服务器的响应过程与第一次被访问时是相同的。举例在收到的HTML文档后,文档中有10个图片,每个图片都要重新再次建立连接获取,所以网速较慢的时候,我们有时会看到先出现网页,每个图片再逐一出现。
这样做的好处:简化了服务器的设计,是服务器更容易支持大量并发的HTTP请求
这样做的缺点:每请求一个文档就要有两倍RTT的开销 ,详细过程:HTTP协议首先要和服务器建立TCP连接,这需要三次握手,当三次握手的前两部分经过一个RTT完成后,客户就把HTTP请求报文作为第三次握手的第三个报文的数据发送给万维网服务器,服务器收到HTTP请求报文后,就把所请求的文档作为响应报文返回给客户。每个请求文档花费两倍的RTT时间。
HTTP/1.1支持持续连接和流水线方式
持续连接就是万维网服务器在发送响应后仍然在一段时间内保持这条连接,使同一个客户(浏览器)和该服务器可以继续在这条连接上传送后续的HTTP请求报文和响应报文。这条持续的连接并不局限于传输同一个页面上链接的文档,而是只要文档在同一个服务器上就可以通过这条持续的连接传送。
流水线方式是客户在收到HTTP的响应报文之前就能接着发送新的请求报文。与之相对应的非流水线方式是客户在收到前一个响应后才能发送下一个请求。
URL
URL 统一资源定位符(Uniform Resource Locator),其实就是我们访问web页面时需要输入的”网页地址“”网址“,比如:https://www.google.com/ 就是URL。
完整定义如下:
协议类型 : // 登录信息(认证) @ 服务器地址 : 端口号 / 带层次的文件路径 ? 查询字符串 # 片段标识符
htttp : // user:pass @ www.example.jp : 80 / dir/index.html ? uid=1 # ch
URI
URI 统一资源标识符(Uniform Resource Identifier),就是某个网络协议方案表示的资源的定位标识符,比如:https://www.google.com/ 也同样可以说是,在https网络协议下的一个URI。
如果想要了解 URI统一资源标识符,那么我们必须理清它和 URL统一资源定位符 之间的关系。
第一,URL统一资源定位符 是一个具体的概念,而 URI统一资源标识符 是一个抽象的概念。
第二,URL统一资源定位符 是 URI统一资源标识符 的子集,URL 属于 URI。
Client将标志位SYN(同步号)置为1,随机产生一个(序列号)seq=x,并将该数据包发送给Server,Client进入SYN_SENT(同步发送)状态,等待Server确认。
Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将(同步号)SYN和ACK都置为1,产生一个(确认号)ack=x+1,随机产生一个值(序列号)seq=y,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD(同步接受)状态。
Client收到确认应答后,检查(确认号)ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并将该数据包发送给Server,Server检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,完成三次握手,随后Client与Server之间可以开始传输数据了。
三次握手是为了防止,客户端的请求报文在网络滞留,客户端超时重传了请求报文,服务端建立连接,传输数据,释放连接之后,服务器又收到了客户端滞留的请求报文,建立连接一直等待客户端发送数据。
服务器对客户端的请求进行回应(第二次握手)后,就会理所当然的认为连接已建立,而如果客户端并没有收到服务器的回应呢?此时,客户端仍认为连接未建立,服务器会对已建立的连接保存必要的资源,如果大量的这种情况,服务器会崩溃。
两次不可以:
tcp是全双工通信,两次握手只能确定单向数据链路是可以通信的,并不能保证反向的通信正常
不用四次:
本来握手应该和挥手一样都是需要确认两个方向都能联通的,本来模型应该是:
1.客户端发送syn0给服务器
2.服务器收到syn0,回复ack(syn0+1)
3.服务器发送syn1
4.客户端收到syn1,回复ack(syn1+1)
因为tcp是全双工的,上边的四部确认了数据在两个方向上都是可以正确到达的,但是2,3步没有没有上下的联系,可以将其合并,加快握手效率,所有就变成了3步握手。
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击,SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
防范SYN攻击措施:降低主机的等待时间使主机尽快的释放半连接的占用,短时间受到某IP的重复SYN则丢弃后续请求。
1.数据传输结束后,客户端的应用进程发出连接释放报文段(FIN),并停止发送数据,客户端进入FIN_WAIT_1状态,此时客户端依然可以接收服务器发送来的数据。
2.服务器接收到FIN后,发送一个ACK给客户端,确认号为收到的序列号+1,服务器进入CLOSE_WAIT状态。客户端收到后进入FIN_WAIT_2状态。
3.当服务器没有数据要发送时,服务器发送一个FIN报文,此时服务器进入LAST_ACK状态,等待客户端的确认。
4.客户端收到服务器的FIN报文后,给服务器发送一个ACK报文,确认号为收到的序列号+1。此时客户端进入TIME_WAIT状态,等待2MSL(MSL:报文段最大生存时间),然后关闭连接。
2MSL意义:
1、保证最后一次握手报文能到达,能进行超时重传。
2、2MSL后,这次连接的所有报文都会消失,不会影响下一次连接。
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
一个完整的Linux内核一般由5部分组成,它们分别是内存管理、进程管理、进程间通信、虚拟文件系统、网络接口。
1、内存管理 内存管理主要完成的是如何合理有效地管理整个系统的物理内存,同时快速响应内核各个子系统对内存分配的请求。
Linux内存管理支持虚拟内存,而多余出的这部分内存就是通过磁盘申请得到的,平时系统只把当前运行的程序块保留在内存中,其他程序块则保留在磁盘中。在内存紧缺时,内存管理负责在磁盘和内存间交换程序块。
2、进程管理 进程管理主要控制系统进程对CPU的访问。当需要某个进程运行时,由进程调度器根据基于优先级的调度算法启动新的进程。:Linux支持多任务运行,那么如何在一个单CPU上支持多任务呢?这个工作就是由进程调度管理来实现的。
在系统运行时,每个进程都会分得一定的时间片,然后进程调度器根据时间片的不同,选择每个进程依次运行,例如当某个进程的时间片用完后,调度器会选择一个新的进程继续运行。
由于切换的时间和频率都非常的快,由此用户感觉是多个程序在同时运行,而实际上,CPU在同一时间内只有一个进程在运行,这一切都是进程调度管理的结果。
3、进程间通信 进程间通信主要用于控制不同进程之间在用户空间的同步、数据共享和交换。由于不用的用户进程拥有不同的进程空间,因此进程间的通信要借助于内核的中转来实现。
一般情况下,当一个进程等待硬件操作完成时,会被挂起。当硬件操作完成,进程被恢复执行,而协调这个过程的就是进程间的通信机制。
4、虚拟文件系统 Linux内核中的虚拟文件系统用一个通用的文件模型表示了各种不同的文件系统,这个文件模型屏蔽了很多具体文件系统的差异,使Linux内核支持很多不同的文件系统。
这个文件系统可以分为逻辑文件系统和设备驱动程序:逻辑文件系统指Linux所支持的文件系统,例如ext2、ext3和fat等;设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。
5、网络接口 网络接口提供了对各种网络标准的实现和各种网络硬件的支持。网络接口一般分为网络协议和网络驱动程序。网络协议部分负责实现每一种可能的网络传输协议。
网络设备驱动程序则主要负责与硬件设备进行通信,每一种可能的网络硬件设备都有相应的设备驱动程序。
1.使用API:这是最常使用的一种方式了
A.get_user(x,ptr):在内核中被调用,获取用户空间指定地址的数值并保存到内核变量x中。
B.put_user(x,ptr):在内核中被调用,将内核空间的变量x的数值保存到到用户空间指定地址处。
C.Copy_from_user()/copy_to_user():主要应用于设备驱动读写函数中,通过系统调用触发。
2.使用proc文件系统:和sysfs文件系统类似,也可以作为内核空间和用户空间交互的手段。
/proc 文件系统是一种虚拟文件系统,通过他可以作为一种linux内核空间和用户空间的。与普通文件不同,这里的虚拟文件的内容都是动态创建的。
使用/proc文件系统的方式很简单。调用create_proc_entry,返回一个proc_dir_entry指针,然后去填充这个指针指向的结构就好了,我下面的这个测试用例只是填充了其中的read_proc属性。
3.使用sysfs文件系统+kobject:其实这个以前是编程实现过得,但是那天太紧张忘记了,T_T。每个在内核中注册的kobject都对应着sysfs系统中的一个目录。可以通过读取根目录下的sys目录中的文件来获得相应的信息。除了sysfs文件系统和proc文件系统之外,一些其他的虚拟文件系统也能同样达到这个效果。
4.netlink:netlink socket提供了一组类似于BSD风格的API,用于用户态和内核态的IPC。相比于其他的用户态和内核态IPC机制,netlink有几个好处:1.使用自定义一种协议完成数据交换,不需要添加一个文件等。2.可以支持多点传送。3.支持内核先发起会话。4.异步通信,支持缓存机制。
5.文件:应该说这是一种比较笨拙的做法,不过确实可以这样用。当处于内核空间的时候,直接操作文件,将想要传递的信息写入文件,然后用户空间可以读取这个文件便可以得到想要的数据了。下面是一个简单的测试程序,在内核态中,程序会向“/home/melody/str_from_kernel”文件中写入一条字符串,然后我们在用户态读取这个文件,就可以得到内核态传输过来的数据了。
6.使用mmap系统调用:可以将内核空间的地址映射到用户空间。在以前做嵌入式的时候用到几次。一方面可以在driver中修改Struct file_operations结构中的mmap函数指针来重新实现一个文件对应的映射操作。另一方面,也可以直接打开/dev/mem文件,把物理内存中的某一页映射到进程空间中的地址上。
其实,除了重写Struct file_operations中mmap函数,我们还可以重写其他的方法如ioctl等,来达到驱动内核空间和用户空间通信的方式。
7.信号:从内核空间向进程发送信号。这个倒是经常遇到,用户程序出现重大错误,内核发送信号杀死相应进程。
当操作系统接收到系统调用请求后,会让处理器进入内核模式,从而执行诸如I/O操作,修改基址寄存器内容等指令,而当处理完系统调用内容后,操作系统会让处理器返回用户模式,来执行用户代码
1)概念:
在计算机中,系统调用(英语:system call),又称为系统呼叫,指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供了用户程序与操作系统之间的接口(即系统调用是用户程序和内核交互的接口)。
操作系统中的状态分为管态(核心态)和目态(用户态)。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。特权指令:一类只能在核心态下运行而不能在用户态下运行的特殊指令。不同的操作系统特权指令会有所差异,但是一般来说主要是和硬件相关的一些指令。用户程序只在用户态下运行,有时需要访问系统核心功能,这时通过系统调用接口使用系统调用。
应用程序有时会需要一些危险的、权限很高的指令,如果把这些权限放心地交给用户程序是很危险的(比如一个进程可能修改另一个进程的内存区,导致其不能运行),但是又不能完全不给这些权限。于是有了系统调用,危险的指令被包装成系统调用,用户程序只能调用而无权自己运行那些危险的指令。另外,计算机硬件的资源是有限的,为了更好的管理这些资源,所有的资源都由操作系统控制,进程只能向操作系统请求这些资源。操作系统是这些资源的唯一入口,这个入口就是系统调用。
2)系统调用举例:
对文件进行写操作,程序向打开的文件写入字符串“hello world”,open和write都是系统调用。如下:
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
if (argc<2)
return 0;
//用读写追加方式打开一个已经存在的文件
int fd = open(argv[1], O_RDWR | O_APPEND);
if (fd == -1)
{
printf("error is %s\n", strerror(errno));
}
else
{
//打印文件描述符号
printf("success fd = %d\n", fd);
char buf[100];
memset(buf, 0, sizeof(buf));
strcpy(buf, "hello world\n");
write(fd, buf, strlen(buf));
close(fd);
}
return 0;
}
还有
写数据write,创建进程fork,vfork等都是系统调用。
用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。
内核态和用户态之间的转换方式主要包括:系统调用,异常和中断。
互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。
自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则.会非常浪费CPU资源。
自旋锁禁止处理器抢占;而信号量不禁止处理器抢占。
基于这个原因,如果自旋锁在锁住以后进入睡眠,由于不能进行处理器抢占,其他系统进程将都不能获得CPU而运行,因此不能唤醒睡眠的自旋锁,因此系统将不响应任何操作(除了中断或多核的情况,下面会讨论)。而信号量在临界区睡眠后,其他进程可以用抢占的方式继续运行,从而可以实现内存拷贝等功能而使得睡眠的信号量程序由于获得了等待的资源而被唤醒,从而恢复了正常的代码运行。
1、free命令
free 命令会显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存等。
2、vmstat命令
vmstat 是Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存、进程、CPU活动进行监控,是对系统的整体情况进行的统计。
3、top命令
使用top命令,可以查看正在运行的进程和系统负载信息,包括cpu负载、内存使用、各个进程所占系统资源等,top命令以一定频率动态更新这些统计信息。
4、cat /proc/meminfo
/proc/meminfo是了解Linux系统内存使用状况的主要接口,我们最常用的”free”、”vmstat”等命令就是通过它获取数据的。/proc/meminfo所包含的信息比”free”等命令要丰富得多,但也很复杂,不再一一解释了。
ps aux命令
ps aux 命令可以查看系统中各个进程的运行情况,包括了进程占用的内存,%MEM 列就是各个进程的内存占用百分比。
小端的好处是学计算机原里的时候大多都学的这个,而且无论大端小端的cpu,1个字节内的顺序都是小端,比较符合一般初学者思维惯性。
大端的好处是网络通信的TCP/IP协议中字节序就是按照大端规定的。x86的cpu处理所有网络数据包时都要先将数字进行字节序反转才能进行运算,发送时也要先反转才能发送,增加了一点计算开销。如果你是大尾cpu就不需要反转了。
.c文件生成.obj文件的过程,称为编译,.obj文件生成到.exe文件的过程,称为链接。
.obj文件就是一个是程序编译生成的二进制文件,当.exe文件生成以后.obj文件就会被删除。
事实上,.c文件生成.exe文件的过程总共是经历了预处理,编译,汇编,链接,这四个过程。
1、预处理
2、编译
在预处理结束后,进行的是编译。编译过程所进行的是对预处理后的文件进行语法分析,词法分析,语义分析,符号汇总,然后生成汇编代码。
3、汇编
汇编过程将汇编代码转成二进制文件,二进制文件就可以让机器来读取。每一条汇编语句都会产生一句机器语言。
4、链接
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数等等。所有这些问题,都需要经链接程序的处理方能得以解决。链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
接分为静态链接和动态链接:
静态链接:后缀是.a,主要在编译的时候将库文件里面代码搬迁到可执行的文件中;
动态链接:后缀是.so,主要在执行的时候需要转换到库文件代码执行;
两种链接的优缺点:
(1)静态的链接产生的可执行的文件体积比较的大;而动态链接的可执行文件的体积比较小;
(2)动态的链接的编译的效率比较的高;
(3)静态链接的可执行的文件执行的效率高
(4)静态链接的可执行的文件的“布局”比较好一点;
1、内存溢出
指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误
内存溢出原因:
内存中加载的数据量过于庞大,如一次从数据库取出过多数据
集合类中有对对象的引用,使用完后未清空,使得不能回收
代码中存在死循环或循环产生过多重复的对象实体
使用的第三方软件中的BUG
启动参数内存值设定的过小
2、内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的分类:
1、堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
2、系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
3、没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
(1)管理方式:堆中资源由程序员控制(通过malloc/free、new/delete,容易产生memory leak),栈资源由编译器自动管理。
(2)系统响应:对于堆,系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个大于所申请空间的空间的堆结点,删除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外,系统会将多余的部分重新放入空闲链表中)。对于栈,只要栈的剩余空间大于所申请空间,系统就会为程序分配内存,否则报异常出现栈空间溢出错误。
(3)空间大小:堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址的,自然不是连续),堆的大小受限于计算机系统中有效的虚拟内存(32位机器上理论上是4G大小),所以堆的空间比较灵活,比较大。栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置)。
(4)碎片问题:对于堆,频繁的new/delete会造成大量内存碎片,降低程序效率。对于栈,它是一个先进后出(first-in-last-out)的结构,进出一一对应,不会产生碎片。
(5)生长方向:堆向上,向高地址方向增长;栈向下,向低地址方向增长。
(6)分配方式:堆是动态分配(没有静态分配的堆)。栈有静态分配和动态分配,静态分配由编译器完成(如函数局部变量),动态分配由alloca函数分配,但栈的动态分配资源由编译器自动释放,无需程序员实现。
(7)分配效率:堆由C/C++函数库提供,机制很复杂,因此堆的效率比栈低很多。栈是机器系统提供的数据结构,计算机在底层对栈提供支持,分配专门的寄存器存放栈地址,提供栈操作专门的指令。
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。死锁发生的四个必要条件如下:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决死锁的方法即破坏上述四个条件之一,主要方法如下:
1)资源一次性分配,从而剥夺请求和保持条件
2)可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件
3)资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件
为了解决文件共享问题,Linux引入了软链接和硬链接。除了为Linux解决文件共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若1个inode号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名,使用ln创建。若文件用户数据块中存放的内容是另一个文件的路径名指向,则该文件是软连接。软连接是一个普通文件,有自己独立的inode,但是其数据块内容比较特殊。
1、概念:
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
虚拟地址(virtual memory)
这是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;
之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。
有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。
——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。
2、地址转换
第一步:CPU段式管理中——逻辑地址转线性地址
CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址。
一个逻辑地址由两部份组成,【段标识符:段内偏移量】。
段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:
通过段标识符中的索引号从GDT或者LDT找到该段的段描述符,段描述符中的base字段是段的起始地址
段描述符:Base字段,它描述了一个段的开始位置的线性地址。
一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
段起始地址+ 段内偏移量 = 线性地址
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
第二步:页式管理——线性地址转物理地址
再利用其页式内存管理单元,转换为最终物理地址。
linux假的段式管理
Intel要求两次转换,这样虽说是兼容了,但是却是很冗余,但是这是intel硬件的要求。
其它某些硬件平台,没有二次转换的概念,Linux也需要提供一个高层抽像,来提供一个统一的界面。
所以,Linux的段式管理,事实上只是“哄骗”了一下硬件而已。
按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。
在Linux下,逻辑地址与线性地址总是一致的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。
linux页式管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。
线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。
另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
每个进程都有自己的页目录,当进程处于运行态的时候,其页目录地址存放在cr3寄存器中。
每一个32位的线性地址被划分为三部份,【页目录索引(10位):页表索引(10位):页内偏移(12位)】
依据以下步骤进行转换:
从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
将页的起始地址与线性地址中最后12位相加。
目的:
内存节约:如果一级页表中的一个页表条目为空,那么那所指的二级页表就根本不会存在。这表现出一种巨大的潜在节约,因为对于一个典型的程序,4GB虚拟地址空间的大部份都会是未分配的;
32位,PGD = 10bit,PUD = PMD = 0,table = 10bit,offset = 12bit
64位,PUD和PMD ≠ 0
32位计算机的CPU一次最多能处理32位数bai据,du例如它的EAX寄存器就是32位的,32位计算机通常也可以处理16位和8位数据。64位计算机一次处理数据要比32位大得多,一次运行64位的数据。
中断是指 CPU 对系统发生某事件时的这样一种响应:
CPU 暂停正在执行的程序,在保留现场后自动地转去执行该事件的中断处理程序;执行完后,再返回到原程序的断点处继续执行。
异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。
中断则是由于系统中某事件引起的,该事件与现行指令无关。
相同点:都是CPU对系统发生的某个事情做出的一种反应。
区别:中断由外因引起,异常由CPU本身原因引起。
中断响应的事前准备:
系统要想能够应对各种不同的中断信号,总的来看就是需要知道每种信号应该由哪个中断服务程序负责以及这些中断服务程序具体是如何工作的。系统只有事前对这两件事都知道得很清楚,才能正确地响应各种中断信号和异常。
CPU检查是否有中断/异常信号
根据中断向量到IDT(中断描述符表)中取得处理这个向量的中断程序的段选择符
根据取得的段选择符到GDT(全局描述表)中找相应的段描述符
CPU根据特权级的判断设定即将运行的中断服务程序要使用的栈的地址
保护当前程序的现场
跳转到中断服务程序的第一条指令开始执行
中断服务程序处理完毕,恢复执行先前中断的程序
立刻关机:
halt
init 0
shutdown -h now
shutdown -h 0
休眠:
sudo pm-hibernate
echo “disk” > /sys/power/state
sudo hibernate-disk
待机/挂起:
sudo pm-suspend
sudo pm-suspend-hybrid
echo “mem” > /sys/power/state
sudo hibernate-ram
1.CPU即中央处理器,是英语“Central Processing Unit”的缩写。CPU从内存或缓存中取出指令,放入指令寄存器,并对指令译码分解成一系列的微操作,然后发出各种控制命令,执行微操作系列,从而完成 系统指令的执行。
2.但是,CPU并不能直接调用存储在硬盘上的系统、程序和数据,必须首先将硬盘的有关内容存储在内存中,这样才能被CPU读取运行。因而,内存(即物理 内存,是相对于硬盘这个“外存”而言)作为硬盘和CPU的“中转站”,对电脑运行速度有较大影响。
3.当运行数据超出物理内存容纳限度的时候,部分数据就会自行“溢出”,这时系统就会将硬盘上的部分空间模拟成内存——虚拟内存,并将暂时不运行的程序或 不使用的数据存放到这部分空间之中,等待需要的时候方便及时调用。
4.由于内存是带电存储的(一旦断电数据就会消失),而且容量有限,所以要长时间储存程序或数据就需要使用硬盘(外存储器)。硬盘也会影响系统速度,因为 系统从硬盘中读取数据并通过总线存入内存的速度也会影响系统运行的快慢。
CPU内部结构大概可bai以分为控制单元、运算单元、存储单元和时钟等几个主要部分。
除了用户模式外,其它6种为特殊模式,这些模式下,程序可以访问系统所有资源;这六种特殊模式中,除了系统模式,其它5种又称为异常模式。
2) arm处理器介绍
arm处理器共有37个寄存器,其中31个通用寄存器,6个状态寄存器。
任意时刻可见的寄存器组包括15个通用寄存器(R0-R14),一个或两个状态寄存器及程序计数器(PC),其中通用寄存器中:
a) R0-R7为未备份寄存器,未备份处理器在所有的处理模式下指的都是同一个物理寄存器,在异常中断造成的处理器模式切换时,由于不同的处理器模式使用的都是相同的物理寄存器,可能造成寄存器中的数据被破坏,未备份处理器没有用于特别的用途,任何可采用通用寄存器的场合都可以使用未备份处理器;
b) R8-R14为备份寄存器
c) 程序计数器PC,即R15
由于ARM采用了流水线机制,当正确读取了PC的值时,改值为当前指令地址值加8个字节,其实最终是加8个字节还是12个字节,取决于芯片的流水线级数。
d) 状态寄存器CPSR
CPSR可以在任何处理器模式下被访问,它包含了标志位,中断禁止位,当前处理器模式标志以及其他的一些控制和状态位,每一种处理器模式下都有一个专用的物理状态寄存器称为SPSR(备份状态寄存器)。当特定的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断程序退出时,可以用SPSR中保存的值来恢复CPSR。
在电子通信领域,波特(Baud)即调制速率,指的是有效数据讯号调制载波的速率,即单位时间内载波调制状态变化的次数。
模拟线路信号的速率,也称调制速率,以波形每秒的振荡数来衡量。如果数据不压缩,波特率等于每秒钟传输的数据位数,如果数据进行了压缩,那么每秒钟传输的数据位数通常大于调制速率,使得交换使用波特和比特/秒偶尔会产生错误。
数据处理方bai面用DSP,因为他可以实现比du较复杂的运算,我指的是硬zhi件完成运算,比如除法,dao一般的arm没有除法器,而是把除法变成加法等运算,所以要很多步实现比较慢,这点你看看c编译成的汇编就可以看出。当然不只是除法其它微分卷积等等。
arm优点在于外设,包括ad,da等等,所以一般用在控制类的电子产品上。
当然高级点的arm中有的也会集成dsp处理器,起到相互补充的作用。
随机存取存储器(Random Access Memory,RAM)又称作“随机存储器”,是与CPU直接交换数据的内部存储器,也叫主存(内存)。它可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储媒介。当电源关闭时RAM不能保留数据。如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。RAM和ROM相比,两者的最大区别是RAM在断电以后保存在上面的数据会自动消失,而ROM不会自动消失,可以长时间断电保存。
1、上拉电阻:将一个不确定的信号,通过一个电阻与电源VCC相连,固定在高电平。在IO口为输入模式且为上拉电阻时,IO口的常态为高电平。
2、下拉电阻:将一个不确定的信号,通过一个电阻与地GND相连,固定在低电平。在IO口为输入模式且为下拉电阻时,IO口的常态为低电平。
3、推挽输出:可以输出高、低电平,连接数字器件。推挽结果一般是指两个三极管分别受两互补信号的控制,总是在一个三极管导通时令一个三极管截止。(推挽输出的最大特点是可以真正的输出高电平和低电平,且两种电平下都有驱动能力)。IO输出0-接GND, IO输出1 -接VCC
4、开漏输出:输出端相当于三极管的集电极,要得到高电平状态需要加上拉电阻才行。适合做电流型的驱动,其吸收电流的能力比较强(20mA左右)(开漏输出最主要的特性就是高电平没有驱动能力,需要借助外部上拉电阻才能真正输出高电平)。开漏只能输出低电平,高电平的时候实际上是个高阻态,需要外接电阻来拉高的。
1、什么是扇区和(磁盘)块?
物理层面:一个磁盘按层次分为 磁盘组合 -> 单个磁盘 -> 某一盘面 -> 某一磁道 -> 某一扇区
扇区,顾名思义,每个磁盘有多条同心圆似的磁道,磁道被分割成多个部分。每部分的弧长加上到圆心的两个半径,恰好形成一个扇形,所以叫做扇区。扇区是磁盘中最小的物理存储单位。通常情况下每个扇区的大小是512字节。(由于不断提高磁盘的大小,部分厂商设定每个扇区的大小是4096字节)
逻辑层面: 磁盘块(虚拟出来的)。 块是操作系统中最小的逻辑存储单位。操作系统与磁盘打交道的最小单位是磁盘块。
2、什么是簇?和块什么区别?
通俗的来讲,在Windows下如NTFS等文件系统中叫做簇;在Linux下如Ext4等文件系统中叫做块(block)。每个簇或者块可以包括2、4、8、16、32、64…2的n次方个扇区。
3、为什么存在磁盘块?
读取方便:由于扇区的数量比较小,数目众多在寻址时比较困难,所以操作系统就将相邻的扇区组合在一起,形成一个块,再对块进行整体的操作。
分离对底层的依赖:操作系统忽略对底层物理存储结构的设计。通过虚拟出来磁盘块的概念,在系统中认为块是最小的单位。
4、怎么映射磁盘块?
磁盘控制器,其作用除了读取数据、控制磁头等作用外,还有的功能就是映射扇区和磁盘块的关系。
5、磁盘的读写基本单位是什么?
答案:读写基本单位是扇区。磁盘的原理,物理实现,磁盘控制器是按照扇区这个单位读取等操作数据的。操作系统是通过块簇来做为单位读取等操作数据的。此题问磁盘的读写,和操作系统没有关系,千万不要联系到操作系统层面去了。
文件系统就是操作系统的一部分,所以文件系统操作文件的最小单位是块。
6、磁盘块与扇区的大小
既然磁盘块是一个虚拟概念。是操作系统自己"杜撰"的。软件的概念,不是真实的。所以大小由操作系统决定,操作系统可以配置一个块多大。
一个块大小=一个扇区大小*2的n次方。
N是可以修改的。
7、为什么磁盘块大小必须是扇区大小的整数倍呢?
磁盘读取数据的基本单位就是一个扇区的大小,一个块的大小对于磁盘来说就是一次获取数据读取的扇区数*扇区大小,如果是整数倍的扇区数对于磁盘的IO更好,速度更快,也会更合理的利用资源。否则会对扇区进行分割。
一个扇区是512字节。有些硬盘厂商会提供4k大小扇区。这是物理结构。磁盘定下来的结构就是没法修改的。所以必须要将块设置为磁盘的大小。
8、4k对齐
随着时代发展,硬盘容量不断扩展,使得之前定义的每个扇区512字节不再是那么的合理,于是将每个扇区512字节改为每个扇区4096 个字节,也就是现在常说的“4K扇区”。随着NTFS成为了标准的硬盘文件系统,其文件系统的默认分配单元大小(簇)也是4096字节,为了使簇与扇区相对应,即使物理硬盘分区与计算机使用的逻辑分区对齐,保证硬盘读写效率,所以就有了“4K对齐”的概念。
新标准的”4K扇区”的硬盘在厂商为了保证与操作系统兼容的前提下,也将扇区模拟成512B,会默认定义为4096字节大小为一个簇,但因为其引导区占用了一个磁道共63个扇区,真正的文件系统在63号扇区之后。
我们通过计算得出前63个扇区大小为:512Bx63=32256B
并按照默认簇大小得出63扇区为:32256B÷4096B=7.875簇
即从第63个扇区结束,往后的每一个簇都会跨越两个物理单元,占据前一个单元的一小部分和后一个单元的一大部分。
而“4K对齐”主要是将硬盘的模拟扇区(512B)对齐到8的整数倍个“实际”4K扇区,即4096B*8=32768B,其正好跨过了63扇区的特性,从第64个扇区对齐。
9、块与页的关系
操作系统经常与内存和硬盘这两种存储设备进行通信,类似于“块”的概念,都需要一种虚拟的基本单位。所以,与内存操作,是虚拟一个页的概念来作为最小单位。与硬盘打交道,就是以块为最小单位。
在计算机系统中,CPU和外部通信有两种通信方式:并行通信和串行通信。而按照串行数据的时钟控制方式,串行通信又可分为同步通信和异步通信两种方式。
1、异步串行方式的特点
所谓异步通信,是指数据传送以字符为单位,字符与字符间的传送是完全异步的,位与位之间的传送基本上是同步的。异步串行通信的特点可以概括为:
①以字符为单位传送信息。
②相邻两字符间的间隔是任意长。
③因为一个字符中的比特位长度有限,所以需要的接收时钟和发送时钟只要相近就可以。
④异步方式特点简单的说就是:字符间异步,字符内部各位同步。
2、同步串行方式的特点
所谓同步通信,是指数据传送是以数据块(一组字符)为单位,字符与字符之间、字符内部的位与位之间都同步。同步串行通信的特点可以概括为:
①以数据块为单位传送信息。
②在一个数据块(信息帧)内,字符与字符间无间隔。
③因为一次传输的数据块中包含的数据较多,所以接收时钟与发送进钟严格同步,通常要有同步时钟。
3、场合
同步串行:通信网中,有大批量数据需要传输
异步串行:应用于在工业、实际应用中。适用于短距离、速率不高的情况下。
同步位系统比异步位系统要实用高效。这个比较好理解,计算机对帧的处理比对字符要少的多,在传送相同大小的数据量的时候,计算机要对大量的字符进行开始与结束操作,帧则要少的多。同时同步位系统的下的网络效率也更高,因为每个字符都至少包含两位的开始结束信息,这个在数据量大的时候开销是很客观的。
开始条件:SCL高电平,SDA由高变低
停止条件:SCL高电平,SDA由低变高
总线空闲态:SDA为高电平(没有设备发送开始条件)
应答信号:接受数据的IC在接受到8bit数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据。
写:
1.主机发送开始条件
2.发送从机地址(7位 )+ 写(0)
3.从机会产生一个应答信号(一个CLK下,把SDA拉低)
4.主机得到应答信号,发送8位数据。
5.从机会产生一个应答信号
6.重复 4.5 过程,直到主机发送停止条件
读:
1.主机发送开始条件
2.发送从机地址(7位)+读(1)
3.从机会产生一个应答信号(一个CLK下,SDA拉低)
4.从机给主机发送8位数据
5.主机给从机发送应答
6.从机得到应答,接着发送8位数据。
7.重复5.6过程,直到主机不发送应答信号和产生停止信号