//使用信号量来限制活动线程数的文件semaphore.cpp的源代码 #include
using namespace std;
//控制活动线程数目的信号量(保护线程共享资源) HANDLE g_hSemThreads=INVALID_HANDLE_VALUE; //简单的线程过程,可暂停、释放信号量并告诉操作者 static DWORD WINAPI ThreadProc(LPVOID lpParam){
LONG nPauseMs=reinterpret_cast
Sleep(nPauseMs); //释放信号量
if(g_hSemThreads!=INVALID_HANDLE_VALUE) {
LONG nPrevCt(0); //显示消除过程
if(ReleaseSemaphore(
g_hSemThreads,//要释放的信号量 1,
&nPrevCt)) {
printf(\,slots left:%d\\n\, nPauseMs,nPrevCt+1); }
return(0); }}
int main() {
//创建允许5个线程同时存在的信号量 g_hSemThreads=CreateSemaphore( NULL,//默认的安全性 5, //开始5个打开槽 5, //仅允许5个槽 NULL);//匿名对象
//创建100个线程,信号量仅允许5个线程同时是活动的 for(int nTotal=100;nTotal>0;--nTotal) {
//等待,直到有空位可以创建一个新线程
waitForSingleObject(g_hSemThreads,INFINITE): //新线程的暂停时间
LONG nPauseMs=nTotal*5: //启动线程
HANDLE hThread=CreateThread( NULL, //默认的安全性
0, //默认堆栈
ThreadProe, //线程函数
reinterpret_cast
printf(\,nPauseMs); //释放线程句柄
CloseHandle(hThread);
hThread=INVALID_HANDLE_VALUE: }
//关闭信号量
CloseHandle(g_hSemThreads);
g_SemThreads=INVALID_HANDLE_VALUE; }
实验内容
1、运行例程3-1,注意观察运行结果。请回答线程的实际工作在哪个方法中来实现的,并描述程序的功能。
2、修改例程3-1,创建两个线程MyThred1和MyThred2:线程MyThred1输出10次“A。。。” ,线程MyThred2输出10次“B^^^”。多次运行该程序,并注意观察输出结果是否一样?并回答在程序运行过程中,任务管理器中能看见这2个线程吗?(为了便于任务管理器观察线程的执行,可将循环次数设置大些。)
3、运行例程3-2,线程A和线程B哪个先运行结束?为什么?将程序中语句: wtA.SetPriority(THREAD_PRI0RITY_L0WEST)
中的参数THREAD_PRI0RITY_L0WEST修改为THREAD_PRI0RITY_HIGHEST后,再运行程序,输出结果有何不同?
4、运行例程3-3,线程A和线程B哪个先运行结束?为什么?
5、运行例程3-4,写出屏幕中的输出信息,并请描述该程序的功能。 6、运行例程3-5,请描述该程序的功能。 7、运行例程3-6,请描述该程序的功能。
8、有能力的同学选做:编写一个程序,利用信号量来实现多个生产者一消费者问题。
实验四 处理机调度
实验目的
在多道程序或多任务系统中,系统同时处于就绪态的进程有若干个。也就是说能运行的进程数远远大于处理机个数。为了使系统中的各进程能有条不紊地运行,必须选择某种调度策略,以选择一进程占用处理机。要求学生设计一个摸拟单处理机调度的算法,以巩固和加深处理机调度的概念。
实验内容
本实验模拟3个调度算法:
1、 设计一个按先来先服务调度的算法
(1)假设系统中有5个进程,每个进程由一个进程控制块(PCB)来标识。进程控制块内容如图4—1所示。
进程名即进程标识。
链接指针:按照进程到达系统的时间将处于就绪状态的进程连接成一个就绪队列。指针指出下一个到达进程的进程控制块首地址。最后一个进程的链指针为NULL。
估计运行时间:可由设计者任意指定一个时间值。
到达时间:进程创建时的系统时间或由用户指定。调度时,总是选择到达时间最早的进程。
进程状态:为简单起见,这里假定进程有两种状态:就绪和完成。并假定进程一创建就处于就绪状态,用R表示。当一个进程运行结束时,就将其置成完成态,用C表示。
(2)设置一个队首指针head,用来指出最先进入系统的进程。各就绪进程通过链接指针连在一起。
(3)处理机调度时总是选择队首指针指向的进程投入运行。由于本实验是模拟实验,所以对被选中进程并不实际启动运行,而只是执行:
估计运行时间减1
用这个操作来模拟进程的一次运行,而且省去进程的现场保护和现场恢复工作。
(4)在所设计的程序中应有显示或打印语句,能显示或打印正运行进程的进程名、已运行时间、还剩时间、就绪队列中的进程等。所有进程运行完成时,给出各进程的周转时间和平均周转时间。
进程名
链接指针
到达时间
估计运行时间
进程状态
图4—1进程控制块结构
2、设计一个按优先级调度的算法
(1)进程控制块表示如图4—2所示。其中,进程的优先数由用户自己指定或程序任意设定,且优先数越低,优先级越高。调度时,总是选择优先级最高的进程运行。其他说明同1、。
(2)为了调度方便,设计一个指针指向5个进程排成的就绪队列的第一个进程。另外再设一个当前
运行进程指针,指向当前正运行的进程。
(3)处理机调度时,总是选择队列中优先级最高的进程运行。为了采用动态优先级调度,进程每运行一次,其优先级就减l。
由于本实验是模拟实验,所以对被选中进程并不实际启动运行,而只是执行:优先数加1和估计运行时间减1,用这两个操作来模拟进程的一次运行。
(4)进程运行一次后,若剩余的运行时间不为0,且其优先级低于就绪队列的进程的优先级,则选择一个高优先级进程抢占CPU;若剩余运行时间为0,则把它的状态改为完成状态(C),并撤出就绪队列。
(5)若就绪队列不空,则重复上述的(3)和(4)直到所有进程成为完成状态。
(6)在所设计的程序中应有显示或打印语句,以显示或打印每次被选中进程的进程名以及运行一次后进程的变化,就绪队列中的各进程排队情况等。
(7)为5个进程任意确定一组“优先级”和“要求运行时间”,启动处理机调度程序,显示或打印每次选中的进程名以及进程控制块的动态变化过程:包括进程已运行时间,剩余时间,就绪队列中的进程等。
进程名
链接指针
进程的优先级
到达时间
估计运行时间
进程状态
图4—2进程控制块
3、设计一个按时间片轮转法调度的算法
(1)假设系统有5个进程,每个进程用一个进程控制块PCB来代表。PCB的格式如图4-1所示。
其中,进程名即进程标识。
链接指针:指出下一个到达进程的进程控制块首地址。按照进程到达的顺序排队。系统设置一个队头和队尾指针分别指向第一个和最后一个进程。新进程放队尾。
(2)为每个进程任意确定一个要求运行时间和到达时间。
(3)按照进程到达的先后顺序排列成一个循环队列。再设一个队首指针指向第一个到达进程的首址。
(4)执行处理机调度时,开始选择队首的第一个进程运行。另外再设一个当前运行进程指针,指向当前正在运行的进程。
(5)由于本实验是模拟实验,所以对被选中进程并不实际启动运行,而只是执行: ①估计运行时间减1; ②输出当前运行进程的名字。 用这两个操作来模拟进程的一次运行。
(6)进程运行一次后,以后的调度则将当前指针依次下移一个位置,指向下一个进程,即调整当前运行指针指向该进程的链接指针所指进程,以指示应运行进程。同时还应判断该
进程的剩余运行时间是否为零。若不为零,则等待下一轮的运行;若该进程的剩余运行时间为零,则将该进程的状态置为完成态C,并退出循环队列。
(7)若就绪队列不空,则重复上述的(5)和(6)步骤直到所有进程都运行完为止。
(8)在所设计的调度程序中,应包含显示或打印语句。以便显示或打印每次选中进程的名称及运行一次后队列的变化情况。
4、例3-1 按时间片轮转法进行CPU调度的实例 /*数据结构定义及符号说明*/ #define N 20
#include
typedef struct pcb /*进程控制块定义*/ {
char pname[N]; /*进程名*/ int runtime; /*运行时间*/ int arrivetime; /*到达时间*/ char state; /*进程状态*/ struct pcb *next; /*链接指针*/ }PCB;
PCB head_input; PCB head_run; PCB *pcb_input;
static char R='r',C='c';
unsigned long current; /*记录系统当前时间的变量*/ void inputprocess(); /*建立进程函数*/ int readyprocess(); /*建立就绪队列函数*/ int readydata(); /*判断进程是否就绪函数*/ int runprocess(); /*运行进程函数*/ FILE *f;
/*定义建立就绪队列函数*/ int readyprocess() {
while(1) {
if(readydata()==0)/*判断是否有就绪进程入队*/ return 1; else
runprocess(); /*运行进程*/ } }
/*定义判断就绪队列是否有进程*/ int readydata()
《操作系统》实验讲义
计算机科学与工程学院
2015年3月
实验一 Linux初步
实验预备知识
一、 创建实验平台
如果实验室里的计算机安装了Windows操作系统,则可以先安装VMWare软件(一般使用工作站版本)并启动它,来创建一个虚拟机,然后在其中安装Linux操作系统。这样就可以从虚拟机中启动Linux系统,并完成相应的实验。
VMWare软件和Linux操作系统的安装十分简单,这里就不做介绍。
二、 Linux下的proc文件系统
在Linux操作系统中,提供了一套非常有用的在用户态检查内核状态和系统特征的机制,这就是proc文件系统。该文件系统安装在 /proc 目录中。比起Windows的任务管理器来,proc文件系统的功能更强大:它能提供更多的系统信息,能修改部分系统信息,还能通过编程来扩充其中的内容。
该文件系统将进程的地址空间、系统的硬件信息(包括CPU、内存状态以及网卡等各种硬件设备)、系统相关机制(中断、I/O)等内容全部设置成虚拟的Linux文件。它以一种特殊的文件系统的方式,为访问系统内核数据的操作提供接口。也就是说,这个文件系统中所有的文件都是特殊文件,这些特殊文件一般与外部设备无关,所涉及到的介质通常室内存和CPU。当从一个特殊文件“读”出时,所读出的数据都是由系统内部按一定的规则临时生成的,或从内存中收集、加工出来的,反之亦然。换言之,这些文件的内容都不存在任何存储设备上,而是在读/写的时候才根据系统中的有关信息生成出来,或映射到系统中的有关变量或数据结构中。
/proc 目录中的每个文件都有一组分配给它的非常特殊的文件许可权,并且每个文件属于特定的用户标识,这里面的文件仅仅包含以下几种权限(除非root用户特别授权):
? 只读 任何用户都不能更改该文件,它用于表示系统信息。
? root写 /proc 目录中的一些文件是可写的,但通常只能由root用户来写。 ? root读 有些文件对一般系统用户是不可见的,而对root用户是可见的。 ? 其他:三种许可权的组合
在Linux的/proc的目录中,除了/proc/sys目录下的文件外,其余大部分的属性都属于root,并且对全部用户是只读的。/proc/sys目录下则存放的是内核参数,并设计成为运行时可修改的。
下表列出/proc目录下的一些重要文件。
目录/文件名 apm cmdline cpuinfo devices filesystems interrupts ioports
描述
高级电源管理 内核命令行 CPU信息 可用设备信息
系统支持的文件系统 中断信息 端口使用信息
kcore kmsg meminfo modules mounts partions stat swap version uptime sys 内核映象 内核消息 内存信息
内核加载模块列表 已加载文件信息 系统识别的分区表 全面信息统计状态表 交换分区使用情况 内核版本
系统正常运行时间 内核参数
在/proc目录下你会发现一些以数字命名的子目录,它们是进程目录。系统中当前运行的每一个进程都有一个对应的目录在/proc目录下,以进程ID号为目录,他们就是读取进程信息的接口,每个进程中包含的文件如下表所示:
子目录名 cmdline enviroment Fd Mem stat cwd root maps statm exe
包含内容
该进程的命令行参数 进程环境变量的值
进程打开的文件的描述符 进程的内存使用情况 进程状态
进程的当前目录 进程的根目录 内存映象
进程内存状态信息
当前进程的可执行文件(链接)
在/proc目录下,有一个特殊的子目录sys,该目录下的文件记录了内核各方面的运行参数。用户可更改这些文件的值,结果是直接修改内核中的相应参数。在修改之前,最好确切地了解这些内核的参数的作用,以及安全的取值范围。下面举几个例子:
1、/proc/sys/kernel/acct
该文件有3个可配置值,根据包含日志的文件系统上可用空间数量(百分比表示),这些值控制何时开始进行进程记帐:
第一个值:如果可用空间低于这个百分比值,则停止记帐; 第二个值:如果可用空间高于这个百分比值,则开始记帐; 第三个值:检查上面2个值的频率(以秒为单位)。
要更改这个文件的某个值,应该回送用空格隔开,默认设置为2 4 30,表示如果包含日志的文件系统上只有少于2%的可用空间,则这些值会使记帐停止;如果有高于4%的可用空间,则再次启动记帐;每30秒做一次检查。
2、/proc/sys/fs/file-max
该文件指定了可以分配的文件句柄的最大数目。如果用户得到错误信息,说明由于打开文件数已经达到了最大值,而不能打开更多的文件,则可能需要增加该值。可将这个值设置为任意多个文件,并且能通过将一个新数字值写入该文件来更改该值。
3、/proc/sys/kernel/domainname
该文件允许配置网络域名。它没有默认值;可能已经设置了域名,也可能没有。 4、/proc/sys/kernel/hostname
该文件允许配置网络主机名。同样,它也没有默认值;可能已经设置了主机名,也可能没有。
5、/proc/sys/kernel/printk
该文件有四个数字值,它们根据日志记录消息的重要性,定义将其发送到何处。关于不同日志级别的更多信息。该文件的四个值依次为:
·控制台日志级别:优先级高于该值的消息将被打印至控制台。 ·默认的消息日志级别:将用该优先级来打印没有优先级的消息。
·最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)。 ·默认的控制台日志级别:控制台日志级别的默认值。 ·默认设置为6 4 1 7。 6、/proc/sys/kernel/shmall
该文件是在任何给定时刻系统上可以使用的共享内存的总量(以字节为单位)。默认设置为2097152。
7、/proc/sys/kernel/shmax
该文件指定内核所允许的最大共享内存段的大小(以B(字节)为单位)。默认设置为33554432。
8、/proc/sys/kerneI/shmmni
该文件表示用于整个系统共享内存段的最大数目。
如上所述,/proc/sys目录不仅提供了内核信息,而且可以通过它修改内核参数。但是必须很小心,因为不当的修改可能会造成系统崩溃。最好是先找一台无关紧要的计算机,测试成功后,再应用到读者自己的系统上。要改变内核的参数,只要用vi编辑或echo参数重定向到文件即可。
实验部分
一、 实验目的与要求
通过proc文件系统观察整个Linux内核和系统的一些重要特征,并编写一个程序,使用proc文件系统获得以及修改系统的各种配置参数。
本实验需要学生具有Linux的基本操作技能,以及采用C语言编写程序的能力。
二、 实验内容
以超级用户的身份登录Linux系统,并进入/proc目录,键入“ls”命令,查看该目录下的内容,同时查看每个文件的读写权限。
1、 请回答下列问题:
·CPU的类型和型号。 ·所使用的Linux的版本。
·从启动到当前时刻所经过的时间。 ·当前内存状态。
2、编写一个程序getinfo.c,编译后并在命令行带命令参数来运行该程序,获得内核参数 (例如网络主机名、共享内存容量、文件句柄的最大参数等) 在屏幕上显示出来。
运行过程实例如下(获取文件句柄最大参数): [root@Linux / ]# ./getinfo filemax filemax :18626
3、编写一个程序setsys.c,编译后并在命令行带命令参数来运行该程序,用来修改内核参数(任意的参数均可(例如网络主机名、共享内存容量、文件句柄的最大参数等)。
运行过程实例如下:
[root@Linux / ]# ./setsys filemax Input filemax parameter:21545 注释:实例中加波浪线的部分为键盘输入内容。通过运行getinfo filemax来确认是否修改成功。
三、 解决方案
本实验完全不涉及任何的内核编程,而完全使用标准c库中的函数。事实上,只需要编写一个简单的读文本文件的程序,就可以直接用于读proc文件系统中的文件。下面给出一个简单的程序框架,读者可以在这个基础上添加自己的代码,从而完成上述的实验。
# include
int main(int argc,char* argv[ ]) { ??
int fd=open(argv[1],FLAG,MODE); // 文件名作为参数传入。FLAG和
// MODE的值由打开的功能决定
if (fd!=-1) {
?? // 读/写相应的内核参数 close(fd); } else {
?? //做错误处理 } ?? return EXIT_SUCCESS; }
if(argc>1&&strcmp(argv[1],\ {
// 将父进程创建的事件置为有信号状态 SignalParent(); } else {
// 创建一个事件并等待子进程发出信号 WaitForChild();
printf(\.\\n\ }
return 0; }
2.互斥体
互斥体是一个可命名的安全内核对象,其主要目的是控制线程对共享资源的互斥访问。拥有单一访问资源的线程创建互斥体,所有想要访问该资源的线程应该在实际执行操作之前获得互斥体,而在访问结束时立即释放互斥体,以便下一个等待线程获得互斥体。
与事件对象类似,互斥体也容易创建、打开,使用之后关闭。利用CreateMutex()API可创建互斥体,创建时还可以指定一个初始的拥有权标志,通过使用这个标志,只有当线程完成了资源的所有的初始化工作时,才允许创建线程释放互斥体。
为了获得互斥体,首先调用线程可使用OpenMutex()API来获得互斥体对象的句柄;然后,线程通过调用waitForsingleObject()等待互斥体变为有信号状态。当某线程调用ReleaseMutex()API释放对资源的访问权之后,从而使互斥体变为有信号状态,并使等待线程获得资源,结束等待。
相关APl函数说明如下: (1)CreateMutex
函数功能:该函数用来构造和打开一个互斥体。 函数格式:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName); 参数:
·lpMutexAttributes 指向SECURITY_ATTRIBUTES结构,表明返回的互斥体句柄能否被继承。
·bInitialOwner如果创建进程希望立即拥有互斥体,则设为TRUE。一个互斥体 同时只能由一个线程拥有。
·lpName指定互斥体的名称。
返回值:如果执行成功,返回互斥体对象的句柄,否则返回值为0。
为了掌握互斥体的使用方法,下面举例说明如何使用互斥体来保护共享资源。 例3-5:使用互斥体来保护共享资源。 #include
// 封装创建互斥体的类 class CCountUpDown {
public:
//构造函数创建两个访问共享变量的线程 CCountUpDown(int nAccesses):
m_hThreadInc(INVALID_HANDLE_VALUE), m_hThreadDec(INVALID_HANDLE_VALUE), m_hMutexValue(INVALID_HANDLE_VALUE), m_nValue(0), m_nAccess(nAccesses) {
//创建互斥体用于访问数值 m_hMutexValue=CreateMutex(
NULL, //默认的安全性
TRUE, //初始时拥有,此时互斥体为无信号状态 NULL); //匿名的 m_hThreadInc=CreateThread(
NULL, //默认的安全性 0, //默认堆栈
IncThreadProc, //类线程进程
reinterpret_cast
NULL); //忽略返回的id m_hThreadDec=CreateThread(
NULL, //默认的安全性 0, //默认堆栈
DecThreadProc, //类线程进程
reinterpret_cast
NULL); //忽略返回的id //释放对互斥体的拥有权
ReleaseMutex(m_hMutexValue); }
//析构函数释放对对象的引用 virtual ~CCountUpDown() {
CloseHandle(m_hThreadInc); CloseHandle(m_hThreadDec); CloseHandle(m_hMutexValue); }
//简单的等待方法,在两个线程终止之前可暂停主调者 virtual void WaitForCompletion() {
//确保所有对象都已准备好
if(m_hThreadInc!=INVALID_HANDLE_VALUE&&m_hThreadDec!=INVALID_HANDLE_VALUE)
{
//等待两者完成(顺序并不重要)
WaitForSingleObject(m_hThreadInc,INFINITE); WaitForSingleObject(m_hThreadDec,INFINITE); } }
protected:
//改变共享资源的方法
virtual void DoCount(int nStep) {
//循环,直到所有的访问都结束为止 while(m_nAccess>0) {
//等待访问数值
WaitForSingleObject(m_hMutexValue,INFINITE); //改变并显示该值 m_nValue+=nStep; printf(\:%d value:access:%d\\n\
m_nAccess); //睡眠等待 --m_nAccess; Sleep(500);
//释放对数值的访问,使互斥体变为有信号状态 ReleaseMutex(m_hMutexValue); } }
static DWORD WINAPI IncThreadProc(LPVOID lpParam) {
//将参数解释为this指针
CCountUpDown * pThis=reinterpret_cast
static DWORD WINAPI DecThreadProc(LPVOID lpParam) {
//将参数解释为this指针
CCountUpDown*pThis=reinterpret_cast
%d
}
protected:
HANDLE m_hThreadInc; HANDLE m_hThreadDec; HANDLE m_hMutexValue; int m_nValue; int m_nAccess; };
int main() {
CCountUpDown ud(10); ud.WaitForCompletion(); return 0; }
3.信号量
信号量是可命名的、安全的线程同步对象,是最不常用的对象,主要目的是创建允许同时访问共享资源的对象数的互斥体。
信号量就像一个门卫,启动时带有可访问资源的最大数目和当前空闲资源数。每当另一个线程要访问资源时,线程就调用内核等待函数,这使得信号量对象再次检查其计数。如果有空闲资源,那么就发出进入信号;如果没有,线程就被挂起在门口。当线程完成使用源资后,将通知信号量,告诉门卫可让另一个线程进入。
为了创建信号量对象,CreateSemaphore()API需要标准的安全性和命名,以及最大值和初始计数。信号量在空闲资源的计数达到0之前处于有信号状态。每个等待信号量对象的线程递减其计数。当线程使用资源完成后,必须调用ReleaseSemaphore()API。
一旦内核对象使用信号量挂起等待访问资源的线程,那么可保证下一个获得访问的是等待队列中的第一个线程。带有最大线程计数的信号量在行为上与互斥体相同,但信号量与互斥体是不可交换的,不能把互斥量句柄传递给信号量API。
相关API函数说明如下: (1)CreateSemaphore
函数功能:该函数用来创建一个有名或者无名的信号量对象。 函数格式:
HANDLE CreateSemaphore(
LPSECURITYATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName); 参数:
·lpSemaphoreAttributes指定了应用于信号量的安全属性参数,包括访问权和继 承权等,如果是NULL就表示要使用默认属性。
·lInitialCount信号量的初值,必须大小或等于0,并且小于或等于MaximumCount。 ·lMaximumCount信号量的最大值,这也就是在同一时间内能够锁住信号量线程的最多个数。
·lpName信号量的名称(一个字符串)。任何线程(或进程)都可以根据这一名称引用到这个信号量。这个值可以是NULL,即产生一个无名的信号量。
返回值:如果函数调用成功,则返回一个信号量句柄,否则返回NULL。不论哪一种情况,GetLastError函数都会返回一个合理的结果。如果指定的信号量名称已经存在,则该函数还是成功的,GetLastError函数会传回ERROR_ALREADY_EXISTS。
备注:信号量使用计数器实现同步。每次取信号量时,信号量计数器递减;每次调用ReleaseSemaphore释放信号量值时,信号量计数器递增。计数永远不会小于0或大于在参数lMaximumCount中定义的值。
(2)ReleaseSemaphore
函数功能:该函数用来释放信号量。当线程使用资源完成后,必须调用ReleaseSemaphore(),来增加信号量的计数。
函数格式:
BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount,
LPLONG lpPreviousCount); 参数:
·hSemaphore信号量的句柄。
·lReleaseCount信号量计数增加值,该值不可以为负或0。 ·lpPreviousCount返回信号量原来的值。
返回值:如果函数成功调用,则返回TRUE,否则返回FALSE。失败时可通过调用GetLastError函数获得原因。
备注:无论该函数对于信号量的当前值怎样增加,都绝对不会超过CreateSemaphore函数所指定的1MaximumCount。任何线程都可以在任何时间调用ReleaseSemaphore,解除被任何线程锁定的信号量。
(3)OpenSemaphore
函数功能:该函数用于打开一个已经存在的命名的信号量对象,且信号量必须是函数CreateSemaphore()创建的。
函数格式:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess, BOOLblnheritHandle, LPCTSTR lpName); 参数:
·dwDesiredAccess指定信号量对象期望的访问形式,可以是下列参数的一个或几个的组合:
SEMAPHORE_ALL_ACCESS为信号量指定所有可能的访问权限。
SEMAPHORE_MODIFY_STATE允许使用ReleaseSemaphore中的句柄。 SYCHRONIZE允许使用在任何等待函数中的句柄。 ·bInheritHandle指定的继承标志。
·lpName信号量的名称(一个字符串)。任何线程(或进程)都可以根据这一名称引用到这个信号量。这个值可以是NULL,即产生一个无名的信号量。
返回值:若函数调用成功,返回信号量对象的句柄,否则返回NULL。 为了掌握信号量的使用方法,下面举例说明如何使用信号量来限制应用程序中的活动线程数目。
例3-6:使用信号量来限制活动线程数
static DWORD WINAPI ThreadProc(LPVOID lpParam) {
// 参数中获得this指针并调用对象工作方法
CWorkerThread *pThis= reinterpret_cast
virtual void DoStuff()
{ // 重复发送线程的ID和名称5次 for(int n=0;n<5;++n) {
printf(\ } }
protected:
HANDLE m_hThread; // 指向线程内核对象的指针 LPCTSTR m_szName; // 显示此线程的名称 };
int main() {
//创建两个线程
CWorkerThread wtA(\ CWorkerThread wtB(\ // 暂停,直到两者都完成为止 wtA.WaitForCompletion(); wtB.WaitForCompletion(); // 报告线程完成
printf(\.\\n\ return 0; }
二、 为线程分配优先权
在应用程序中可为不同的线程指定不同的优先级。为了使系统给予进程的某一部分较多的CPU周期,这是必要的。Windows 2000为进程和线程提供了广泛的优先级授权。优先权可由1~31内的整数来代表,1的优先权最低,31最高。进程优先级表明系统中一个应用程序比另一个应用程序重要的程度,线程优先级表示进程中一个线程比另一个线程重要的程度。Windows按优先级增加的次序,优先级依次为idle(空闲)、lowest(最低)、below-normal(低于正常)、normal(正常)、above-normal(高于正常)、highest(最高)和time-critical(与时间相关的)。
在创建线程后,就可以改变其优先级了。改变优先级可在实际开始执行之前进行,可将该线程创建为挂起的,改变其优先级,然后调用ResumeThread()API函数。
1.相关API函数说明 (1)GetCurrentThreadId
函数功能:该函数可获得当前线程的id。对线程而言,该值是惟一的,可以在全系统运行。
函数格式:
DWORD GetCurrentThreadId(void)。 参数:无。
返回值:若函数调用成功则返回当前线程的标志符,否则返回NULL。 (2)SetThreadPriority
函数功能:该函数用来设置线程优先级。 函数格式:
BOOL SetThreadPriority( HANDLE hTread,int nPriority); 参数:hTread线程句柄;nPriority指定线程的优先级。 返回值:若函数调用成功,返回非零值,否则返回值为0。 2.改变线程优先级的实例程序
为了掌握SetPriority()API函数的使用方法,下面举例来说明。 例3-2 :改变线程的优先级。 #include
// 封装创建工作线程的类 class CWorkerThread {
public:
CWorkerThread(LPCTSTR SzName):
m_szName(SzName),m_hThread(INVALID_HANDLE_VALUE) {
// 创建新线程并令其启动 m_hThread=CreateThread(
NULL, // 默认的安全性 0, // 默认堆栈
ThreadProc, // 类范围内的线程proc
reinterpret_cast
virtual ~CWorkerThread() {
CloseHandle(m_hThread); }
// 等待线程完成的方法
virtual void WaitForCompletion() {
WaitForSingleObject(m_hThread,INFINITE); }
// 改变线程的优先级
virtual void SetPriority(int nPriority)
{
SetThreadPriority(m_hThread,nPriority); }
protected:
static DWORD WINAPI ThreadProc(LPVOID lpParam) {
// 从参数lpParam中获得this指针并调用线程对象工作方法 CWorkerThread*pThis=
reinterpret_cast
virtual void DoStuff() {
// 重复8次地发送线程的ID和名称 for(int n=1; n<=8; ++n) {
printf(\:%d,count %d\\n\ GetCurrentThreadId(),n); } }
protected:
HANDLE m_hThread; //指向线程内核对象的指针 LPCTSTR m_szName;//显示此线程的名称 };
int main() {
// 创建两个线程
CWorkerThread wtA(\ CWorkerThread wtB(\ // 使线程A的优先级尽可能低
wtA.SetPriority(THREAD_PRIORITY_LOWEST); wtB.SetPriority(THREAD_PRIORITY_NORMAL); // 暂停,直到两者都完成为止 wtA.WaitForCompletion(); wtB.WaitForCompletion(); // 报告线程完成
printf(\.\\n\}
三、 挂起和激活线程
前面的例1展示了通过创建两个线程之后,主线程调用WaitForCompletion()API等待
两个线程完成,并将线程从准备运行的线程队列中移人到挂起队列中。一旦目标满足了等待函数的条件(线程完成),内核就将该线程再移回准备运行队列中。对现有线程可以执行的另一个操作是人工暂停和恢复它们,也可用程序响应的方式进行这些操作。下面借用一个例子来展示如何使用SuspendThread()和ResumeThread()API函数,将线程挂起,然后再恢复为运行状态。
1.相关API函数说明 (1)ResumeThread
函数功能:该函数重新激活被挂起的线程,系统会为每个线程建立一个挂起计数。调用一次该函数,使挂起计数减1,只要挂起计数恢复为0时,就恢复这个挂起线程的运行。
函数格式:
DWORD ResumeThread(HANDLE hTread);
参数:hTread指定待挂起的线程对象的句柄。
返回值:若函数调用成功,返回该线程先前的挂起记数,否则返回OxFFFFFFFF。 (2)SuspendThread
函数功能:该函数用于挂起一个线程,系统会为每个线程建立一个挂起计数。调用一次该函数,使挂起计数加1,只要挂起计数大于0,则挂起该线程。
函数格式:
DWORD SuspendThread(HANDLE hTread);
参数:hTread指定待挂起的线程对象的句柄。
返回值:若函数调用成功,返回该线程先前的挂起记数,否则返回OxFFFFFFFF。 2.线程暂停与恢复的实例程序 例3-3: 线程的暂停与恢复。 #include
// 封装创建工作线程的类 class CWorkerThread {
public:
CWorkerThread(LPCTSTR szName):
m_szName(szName),m_hThread(INVALID_HANDLE_VALUE) {
// 创建新线程并令其启动 m_hThread=CreateThread(
NULL, // 默认的安全性 0, // 默认堆栈
ThreadProc, // 类范围内的线程proc
reinterpret_cast
virtual ~CWorkerThread() {
CloseHandle(m_hThread); }
// 等待线程完成的方法
virtual void WaitForCompletion() {
WaitForSingleObject(m_hThread,INFINITE); }
// 改变线程的优先级
virtual void SetPriority(int nPriority) {
SetThreadPriority(m_hThread,nPriority); }
// 线程挂起
virtual void Suspend() {
SuspendThread(m_hThread); }
// 线程恢复
virtual void Resume() {
ResumeThread(m_hThread); }
protected:
static DWORD WINAPI ThreadProc(LPVOID lpParam) {
// 从参数中获得this指针并调用对象的工作方法
CWorkerThread*pThis= reinterpret_cast
virtual void DoStuff() {
// 重复8次发送线程的ID和名称 for(int n=1; n<8; ++n) {
printf(\:%d,count %d\\n\ GetCurrentThreadId(),n); } }
protected:
HANDLE m_hThread; //指向线程内核对象的指针 LPCTSTR m_szName; //显示此线程的名称 };
int main() {
// 创建两个线程
CWorkerThread wtA(\ CWorkerThread wtB(\
// 使线程A的优先级降得尽可能低
wtA.SetPriority(THREAD_PRIORITY_LOWEST); //挂起线程B
printf(\挂起线程B...\\n\ wtB.Suspend(); //完成前暂停
wtA.WaitForCompletion(); //继续执行线程B
printf(\继续执行线程B...\\n\ wtB.Resume();
wtB.WaitForCompletion(); //报告线程完成
printf(\.\\n\}
四、 终止线程
终止线程与终止进程很类似,最好使用ExitThread()API来终止线程,因为系统要求每个线程都要从其线程例程中退出。而TerminateThread()API可由其他线程在不清除有关资源的情况下杀死该线程。
与进程对象一样,在调用ExitThread()或TerminateThread()API后,线程对象转换为有信号状态。当发生转换时,系统释放所有等待该线程对象的其他线程。这两个API函数的说明如下。
(1)ExitThread
函数功能:该函数用来终止一个线程。 函数格式:
VOID ExitThread(DWORD dwExitCode);
参数:dwExitCode:定义调用线程的退出代码。使用GetExitCodeThread函数来检测一个线程的退出代码。
返回值:无。
备注:调用ExitThread函数,是终止一个线程的较好的方法。调用该函数后(或者直接地调用,或者从一个线程过程返回),当前线程的堆栈取消分配,线程终止。若调用该函数时,该线程为进程的最后一个线程,则该线程的进程也被终止。
线程对象的状态为有信号状态,以释放所有正在等待该线程终止的其他线程。线程的终止状态从STILL_ACTIVATE变为dwExitCode参数的值。
线程结束时不必从操作系统中移去该线程对象。当线程的最后一个句柄关闭时,该线程对象被删除。
(2)TerminateThread 函数功能:该函数用来终止一个线程的,通常由父线程调用以终止无法正常结束的线程。函数格式:
BOOL TerminateThread( HANDLE hThread,DWORD dwExitCode);
参数:hThread指定待终止线程对象的句柄,该对象必须拥有THREAD_TERMINATE的访
问权限;dwExitCode定义调用线程的退出代码。
返回值:若函数调用成功则返回TRUE,否则返回FALSE。
五、 线程的同步
多线程编程中关键的一步是保护所有的共享资源状态的一致性,线程同步工具主要有事件对象、互斥体、信号量等。
1.事件
在进程内或进程间实现线程同步的最简单的方法是使用事件对象,这个事件对象可允许线程直接控制其信号状态(见表2-1)。内核事件对象是人工对同一进程或其他进程中的线程进行广播的一个简单方法。
事件的使用很简单。用CreateEvent()API创建一个事件并返回事件句柄。事件分为人工复位和自动复位,可使用SetEvent()API把人工复位事件改变为有信号状态,也可使用ResetEvent()API使人工复位事件变为无信号状态。使用PulseEvent()API可将自动重置事件转化为有信号状态,此函数释放所有等待的线程之后,使事件又转化为无信号状态。
当另一个进程需要访问事件对象时,可共享事件对象句柄。这时该进程必须使用事件名称,调用OpenEvent()API,创建引用。当事件对象不再被引用时,需要使用CloseHandle()API关闭所有对象事件的引用,之后事件不再存在。
相关API函数说明如下: (1)CreateEvent
函数功能:该函数用来创建一个命名或未命名的事件对象。 函数格式:
HANDLE CreateEvent(
LPSECURITYATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL blnitialState, LPCTSTR lpName);
表2-1 用于管理事件对象的API API名称 CreateEvent() 描 述 在内核中创建一个新的事件对象。此函数允许有名称规范、安全性设置、用于人工还是自动重置的标志以及表明初始状态为有信号还是无信号状态 打开已经存在的事件对象。此API函数需要事件名称、继承标志和所需的访问级别 将人工重置事件转化为有信号状态 将人工重置事件转化为无信号状态 将自动重置事件对象转化为有信号状态。当系统释放所有的等待它的线程时此种转化立即发生 OpenEvent() SetEvent() ResetEvent() PulseEvent()
参数:
·lpEVentAttributes指定一个结构,用于设置新事件对象的安全属性,包括访问 权和继承权等。
·bManualReset如果参数为TRUE,函数将创建一个人工重置事件;如果参数为 FALSE,表示创建一个自动重置事件。
·bInitialState如果参数为TRUE,则事件为初始状态有信号,否则无信号。
·lpName指定事件对象名称。
返回值:如果执行成功,返回事件对象的句柄,否则返回值为0。 (2)OpenEvent
函数功能:该函数用来打开一个已命名的事件对象,以获得该对象的句柄。 函数格式:
HANDLE OpenEvent(
DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName); 参数: · dwDesiredAccess 指定事件对象的访问形式,可以是下列参数的一个或几个的组合: EVENT_ALL_ACCESS 为事件对象指定所有可能的访问权限标志。
EVENT_MODIFY_STATE 在SetEvent和ResetEvent函数中启动事件句柄的使用,以更改事件的状态。
SYNCHRONIZE 允许启用在任何等待函数中的事件句柄,等待事件的状态成为有信号态。
·BInheritHandle 指定的继承标志。若句柄可以由子进程继承,则设置为TRUE,否则设置为FALSE。
·LpName指向包括事件对象名的字符串的指针。
返回值:如果函数执行成功,返回事件对象的句柄,否则返回值为FALSE。 备注:此函数可以在多个线程中为同一个事件对象打开句柄,但此事件对象必须是用函数CreateEvent创建的。当不再需要句柄时,可以用函数CloseHandle来关闭句柄。
(3)SetEvent 函数功能:该函数用于将事件对象的状态改为有信号状态。有信号的事件将允许线程等待,以便事件继续执行。
函数格式:
BOOL SetEvent(HANDLE hEvent);
参数:hEvent指定要改变为有信号态的事件对象的句柄。 返回值:如果函数执行成功则返回TRUE,否则返回FALSE。 (4)ResetEvent 函数功能:该函数用于将事件对象的状态改为无信号状态。一个处于无信号态的事件将阻塞所有等待线程。
函数格式:
BOOL ResetEvent(HANDLE hEvent);
参数:hEvent指定要改变为无信号态的事件对象的句柄。 返回值:如果函数执行成功则返回TRUE,否则返回FALSE。 (5)PulseEvent 函数功能:该函数将自动重置事件对象转化为有信号状态。当系统释放所有的等待事件对象的线程时,此种转化立即发生。
函数格式:
BOOL PulseEvent(HANDLE hEvent);
参数:hEyent指定等待得到信号的事件对象的句柄。该事件必须拥有EVENT_MODIFY_STATE的访问权限。
返回值:如果函数执行成功则返回TRUE,否则返回FALSE。
备注:该函数对于人工复位事件对象,释放所有可以立即释放的等待线程;对于自动复位事件对象,则只是释放一个等待线程,而不管其他的等待线程。若没有线程可以释放,则将事件对象设置为无信号态后再返回。
为了更好地掌握事件对象的使用,下面举一个例子来实现进程间的通信。 例3-4: 创建和打开进程间的要发送信号的事件对象。 #include
// 以下是句柄事件。实际中很可能使用共享的包含文件来进行通信
static LPCTSTR g_szContinueEvent=\.EventDemo.event.Continue\// 这个方法只是创建了一个进程的副本 BOOL CreateChild() {
TCHAR szFilename[MAX_PATH]; // 提取当前可执行文件的文件名
GetModuleFileName(NULL,szFilename,MAX_PATH);
//格式化用于子进程的命令行,指明是一个EXE文件和子进程 TCHAR szCmdLine[MAX_PATH];
sprintf(szCmdLine,\ STARTUPINFO si; // 子进程的启动信息结构
ZeroMemory(reinterpret_cast
PROCESS_INFORMATION pi; //返回的子进程的进程信息结构 //用同样的可执行文件和命令行创建进程 BOOL bCreateOK=CreateProcess(
szFilename, //可执行的应用程序的名称
szCmdLine, //指定创建一个子进程的符号标识 NULL, //默认的进程安全性 NULL, //默认的线程安全性 FALSE, //不继承句柄 0, //特殊的创建标志 NULL, //新环境 NULL, //当前目录 &si, //启动信息结构
&pi); //返回进程和线程信息 // 运行结束,关闭进程和其线程的句柄 if(bCreateOK) {
CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
return(bCreateOK); }
// 下面的方法创建一个事件和一个子进程,然后等待子进程在返回前向事件发出信号 void WaitForChild( )
{
// 创建事件和子进程,然后等待子进程给事件发送信号 HANDLE hEventContinue=CreateEvent(
NULL, // 默认的安全性,子进程将具有访问权限 TRUE, // 人工重置事件
FALSE, // 初始时是无信号状态 g_szContinueEvent); // 事件名称 if(hEventContinue!=NULL) {
printf(\.\\n\ //创建子进程
if(CreateChild()) {
printf(\.\\n\
printf(\.\\n\\n\ // 等待,直到子进程将事件置为有信号状态
WaitForSingleObject(hEventContinue,INFINITE);
printf(\process.\\n\
}
//句柄
CloseHandle(hEventContinue); } }
//以下方法在子进程模式下被调用,其功能只是向父进程发出终止信号 void SignalParent() {
//打开句柄
HANDLE hEventContinue=OpenEvent(
EVENT_MODIFY_STATE, //所要求的最小访问权限 FALSE, //不是可继承的句柄
g_szContinueEvent); //事件名称 if(hEventContinue!=NULL) {
printf(\\\n\
printf(\ getchar();
SetEvent(hEventContinue); }
CloseHandle(hEventContinue) ;//清除句柄 }
int main(int argc,char *argv[]) {
// 检查父进程或子进程是否启动