想把自己对程序运行中的一些问题、程序的运行过程的理解阐述一下:如堆栈是怎么用的、用户区和内核区的作用、进程和线程的区别、线程同步等。
1.创建进程的过程
启动一个应用程序也就是创建一个进程,来获得系统资源,然后进程中的线程不断的执行来完成各种过程。系统创建进程的过程主要有:
(1) 系统要创建一个进程内核对象
(2) 为进程创建一个私有地址空间
(3) 系统在地址空间中保留一个足够大的区域用于存放该exe文件
(4) 当exe文件被映射到进程的地址空间后,系统访问exe文件的一个部分,该部分列出了包含exe文件中的代码要调用的函数的DLL文件
(5) 系统为每个需要的Dll文件调用LoadLibrary,如果任何一个Dll需要更多的Dll,那么系统也调用LoadLibrary,加载这些Dll。
这一阶段最常见的就是找不到需要的Dll没有打开成功,用Depends看一下需要什么Dll就清楚了。
2.进程和线程的区别
有句常说的话,进程是死的,线程是活的,即进程不执行地址空间的代码,线程负责执行代码。我觉得分成进程和线程是操作系统管理的一种解决方案,是考虑到资源的分配,安全性,并发性。
操作系统需要并发性,需要同时执行多个任务,多线程就可以在一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。而是将进程作为系统进行资源分配和调度的一个独立单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。
同样操作系统也需要安全,需要一个应用程序的执行不影响另一个应用程序。那么进程相当于划定了边界,不用进程有不同地址空间,不能直接相互访问,而同属一个进程的线程共享进程所拥有的全部资源。
3.进程的地址空间
主要可以认为是分为用户方式的分区和内核方式的分区,相应的进程的状态分为用户态和内核态。处于用户态下的进程执行的是应用程序指令,处于内核态下的应用程序进程执行的是操作系统指令。
用户方式分区是进程私有(非共享)地址空间所在的地方。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。所有的exe和Dll模块均加载在这个分区。内核方式分区是存放操作系统代码的地方。用于线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序的代码全部在这个分区加载。驻留在这个分区中的一切均可被所有进程共享。
所以为什么用户方式的线程同步快,内核方式的同步慢就很显然了,因为从执行用户方式代码的位置跳到执行内核方式代码是要耗费时间的。同时为什么用户方式的同步不能用于进程之间的同步而内核方式可以也就清楚了,因为用户方式分区私有,内核方式分区可被所有进程共享。
4.程序中各种变量的存储位置
首先列出些基本概念:
(1) 栈:由系统自动分配和释放,存放函数的参数、局部变量,函数后下一条语句的存放位置等
(2) 堆:程序员分配和释放。
(3) 全局区(静态区):全局变量和静态变量的存储位置,初始化的全局变量和静态变量放在一块区域,未初始化的全局变量和为初始化的静态变量放在相邻的另一块区域内。
(4) 文字常量区:常量字符串就存放在这里。
(5) 程序代码区:存放函数体的二进制代码。
举个例子如下:
int g_nTest = 0;//全局初始化区
char *g_pszTest;//全局未初始化区
int main() {
int nStack;//栈
char sz[]=”test”;//栈
char *pszStack1;//栈
char *pszStack2 = “123”;//123在常量区,pszStack2在栈上
stack int s_nTest = 0;//全局初始化区
g_ pszTest = (char*)malloc(10);
pszStack1 = (char*)malloc(20);//以上两者分配的10字节和20字节在堆区
strcpy(g_ pszTest, “123”);//123在常量区,编译器有可能会将它与pszStack2所指向的“123”优化成一个地方
return 0;
}
通过以上的概念,可以看出:静态变量和全局变量是不入堆栈的,因此他们的值也不会受函数是否执行完,对象是否建立等影响的。而且他们也是在最先被分配存储区域和初始化的。并且他们是可以被其他线程共享的,因为他们不隶属于某个线程的堆栈。
5.堆的分配过程以及堆和栈的区别
操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
栈和堆的区别:
(1) 栈是系统负责分配释放,而堆由程序员负责。
(2) 在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大
(3) 栈由系统自动分配,速度较快。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片。
(4) 栈存放系统需要维持线程运行的东西,如函数参数,指令地址等,而堆的内容由程序员决定。
6.堆栈的作用
如前所述,进程不执行代码,线程执行应用程序的代码。而线程有什么自己的东西呢,主要就是堆栈。因此,程序能动态运行主要靠堆栈。堆栈主要存储临时变量、函数调用时的各参数和函数调用语句的下一条可执行语句。而这些正是代码能起到其作用的主要方面。
如我们做一个求和程序,为了解决两个数相加求和的问题。和界面相关的代码调用专门用于求和的函数:
void CTestStackDlg::OnBnClickedAdd()
{
UpdateData(TRUE);
m_nSum = Add(m_nAddend, m_nAugend);
UpdateData(FALSE);
}
int CTestStackDlg::Add(int Addend, int Augend)
{
int nSum = 0;
nSum = Addend + Augend;
return nSum;
}
那么代码是给出了解决求和问题的办法,但只有靠临时变量在运行时实时保存输入的参数和结果,以及函数之间的调用才能完成运行时的加法运算。
函数调用中堆栈起着重要作用。在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
而且堆栈采用的后进先出的方式,无论是对临时变量(函数开始时后建立的,在退出函数时先释放),还是函数调用都是最恰当的解决方式。