1.程序:
程序构成:
(1)源代码
(2)可执行的二进制代码
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。由操作系统加载其可执行的二进制代码,分配相应的数据结构:进程控制块PCB(Process Control Block),进行一些列初始化操作(创建进行ID、分配时间片等)后得到进程。
2.进程:分配资源的最小单位
进程构成:
(1)内核对象:存放进程相关信息
(2)地址空间:可执行模块、DLL的代码和数据以及动态分配的内存空间
是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器并由处理器执行的一个实体。
进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
特征 进程的特征:
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
进程的切换:
进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。
这里所说的从某个进程收回处理器,实质上就是把进程存放在处理器的寄存器中的中间数据找个地方存起来,从而把处理器的寄存器腾出来让其他进程使用。那么被中止运行进程的中间数据存在何处好呢?当然这个地方应该是进程的私有堆栈。
让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。
这就像多个同学要分时使用同一张课桌一样,所谓要收回正在使用课桌同学的课桌使用权,实质上就是让他把属于他的东西拿走;而赋予某个同学课桌使用权,只不过就是让他把他的东西放到课桌上罢了。
在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的 切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程 的上下文是存储在进程的私有堆栈中的。
进程的状态: 进程执行时的间断性,决定了进程可能具有多种状态。事实上,运行中的进程可能具有以下三种基本状态。
1)就绪状态(Ready): 进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
2)运行状态(Running): 进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
3)阻塞状态(Blocked): 由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理机分配给该进程,也无法运行。
进程的创建过程: 一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语Creat()按下述步骤创建一个新进程。
1) 申请空白PCB。为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。
2) 为新进程分配资源。为新进程的程序和数据以及用户栈分配必要的内存空间。显然,此时操作系统必须知道新进程所需要的内存大小。
3) 初始化进程控制块。PCB的初始化包括:①初始化标识信息。将系统分配的标识符和父进程标识符,填入新的PCB中;②初始化处理机状态信息。使程序计数器指向程序的入口地址,使栈指针指向栈顶;③初始化处理机控制信息。将进程的状态设置为就绪状态或静止就绪状态,对于优先级,通常是将它设置为最低优先级,除非用户以显式的方式提出高优先级要求。
4) 将新进程插入就绪队列。如果进程就绪队列能够接纳新进程,便将新进程插入到就绪队列中。
3.虚拟地址空间:
虚拟地址空间构成:
(1)内核方式分区:内核代码、设备驱动、I\O高速缓冲等使用
(2)用户方式分区:进程的私有地址空间,维护进程数据
操作系统分配给进程的虚拟地址的范围。32位下为232B = 4GB。所以不同进程的同一个内存地址互不相关。
Windows 使用基于分页机制的虚拟内存。每个进程有4GB的虚拟地址空间。基于分页机制,这4GB地址空间的一些部分被映射了物理内存,一些部分映射硬盘上的交换文件,一些部分什么也没有映射。程序中使用的都是4GB地址空间中的虚拟地址。而访问物理内存,需要使用物理地址。
物理地址 (physical address): 放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
虚拟地址 (virtual address): 4G虚拟地址空间中的地址,
程序中使用的都是虚拟地址。
如果CPU寄存器中的分页标志位被设置,那么执行内存操作的机器指令时,CPU会自动根据页目录和页表中的信息,把虚拟地址转换成物理地址,完成该指令。
使用了分页机制之后,
4G的地址空间被分成了固定大小的页,
每一页或者被映射到物理内存,或者被映射到硬盘上的交换文件中,或者没有映射任何东西。对于一般程序来说,4G的地址空间,只有一小部分映射了物理内存,大片大片的部分是没有映射任何东西。物理内存也被分页,来映射地址空间。对于32bit的Win2k,
页的大小是4K字节。
CPU用来把虚拟地址转换成物理地址的信息存放在叫做页目录和页表的结构里。 物理内存分页,一个
物理页的大小为4K字节,第0个物理页从物理地址 0x00000000 处开始。
由于页的大小为4KB,就是0x1000字节,所以第1页从物理地址 0x00001000 处开始。第2页从物理地址 0x00002000 处开始。可以看到由于页的大小是4KB,所以只需要32bit的地址中高20bit来寻址物理页。
??? 页表,一个
页表的大小为4K字节,放在一个物理页中。由1024个4字节的页表项组成。页表项的大小为4个字节(32bit),所以一个页表中有1024个页表项。页表中的每一项的内容(每项4个字节,32bit)高20bit用来放一个物理页的物理地址,低12bit放着一些标志。
页目录,一个页目录大小为4K字节,放在一个物理页中。由1024个4字节的页目录项组成。页目录项的大小为4个字节(32bit),所以一个页目录中有1024个页目录项。页目录中的每一项的内容(每项4个字节)高20bit用来放一个页表(页表放在一个物理页中)的物理地址,低12bit放着一些标志。
4.线程:资源调度的最小单位
线程的构成:
(1)内核对象:存放线程相关信息
(2)线程堆栈:维护执行代码时所需的参数和变量
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。
下面看看C++创建进程的相关函数:
1 HANDLE WINAPI CreateThread(
2 __in LPSECURITY_ATTRIBUTES lpThreadAttributes,
3 __in SIZE_T dwStackSize,
4 __in LPTHREAD_START_ROUTINE lpStartAddress,
5 __in LPVOID lpParameter,
6 __in DWORD dwCreationFlags,
7 __out LPDWORD lpThreadId
8 );
1 uintptr_t _beginthreadex(
2 void *security,
3 unsigned stack_size,
4 unsigned ( *start_address )( void * ),
5 void *arglist,
6 unsigned initflag,
7 unsigned *thrdaddr
8 );
两个函数都是用于创建线程,第一个是Windows API函数,在WinBase.h头文件中,第二个不是API函数,在process.h头文件中
参数说明:
1.线程安全性:表示是否可以被子进程所继承
2.初始堆栈大小:如果为0或者小于默认值,则使用和调用线程同样大小的空间
3.线程其实地址:一个函数指针,指向线程函数
4.参数:传递给线程函数的参数
5.创建选项:如果为CREATE_SUSPENDED表示创建后挂起,如果为0表示创建后立即执行
6.线程ID
两个函数的区别:
malloc、fopen、ctime等函数需要专门的线程局部存储数据块,这个数据块在创建线程时创建。如果用CreateThread,则不会创建,这样,函数能够正常使用,但是会自动创建数据块,但是函数并不会释放创建的数据库,所以并不会将其删除,就导致内存泄露!!!
而_beginthreadex(内部也调用CreateThread)和_beginthreadex(会自动调用CloseHandle关闭句柄)对这个内存块做了处理。
代码演示:
1 #include <iostream>
2 #include <Windows.h>
3 #include <process.h>
4 using namespace std;
5
6
7 DWORD WINAPI CreateFun(LPVOID lParam)
8 {
9 cout << "CreateThread" << endl;
10 return 0;//0表示成功
11
12 }
13
14 UINT _stdcall beginFun(LPVOID lParam)
15 {
16 cout << "beginthreadex" << endl;
17 return 0;
18 }
19 int main(void)
20 {
21
22 DWORD dwID;
23 UINT nID;
24 HANDLE hC;
25 HANDLE hB;
26
27 hC = CreateThread(NULL, 0, CreateFun, NULL, 0, &dwID);
28
29 if (NULL != hC)
30 {
31 CloseHandle(hC);
32 }
33
34
35
36 hB = (HANDLE)_beginthreadex(NULL, 0, beginFun, NULL, 0, &nID);
37 if (NULL != hB)
38 {
39 CloseHandle(hB);
40 }
41
42 Sleep(1000);
43 }
CloseHandle:关闭句柄
调用CloseHandle并不会终止线程的执行,而是递减线程内核对象句柄计数,线程执行完毕后也会自动递减,当计数为0时释放线程内核对象。当进程终止时也会清理内核对象。
但是,如果不关闭,可能导致有些进程拥有的资源无法释放,导致内存泄露。
线程的相关函数:
(1)CreateThread:创建线程,失败返回NULL,成功返回线程句柄
(2)SuspendThread:挂起线程
(3)ResumeThread:恢复线程
(4)OpenThread:打开线程,根据线程ID得到线程句柄
(5)ExitThread:退出线程
(6)TerminateThread:终止线程
(7)GetExitCodeThread:获取线程运行状态,如果为STILL_ALIVE表示正在运行。
(8)GetCurrentThread:获取当前线程句柄
(9)GetCurentThreadID:获取当前线程ID
注意:最好不要显式的调用ExitThread和TerminateThread,因为可能导致线程无法清理某些东西,导致内存泄露~