精华内容
下载资源
问答
  • java、杀死启动进程、定时执行程序、隐藏黑窗口(dos窗口)、线程异常死亡(保活):就是java自动重启JBOSS
  • Windows下多线程编程

    千次阅读 2019-03-26 17:58:25
    熟练掌握Windows下的多线程编程,能够让我们编写出更规范多线程代码,避免不要的异常Windows下的多线程编程非常复杂,但是了解一些常用的特性,已经能够满足我们普通多线程对性能及其他要求。 进程与线程 1. ...

    前言

    熟练掌握Windows下的多线程编程,能够让我们编写出更规范多线程代码,避免不要的异常。Windows下的多线程编程非常复杂,但是了解一些常用的特性,已经能够满足我们普通多线程对性能及其他要求。

    进程与线程

    1. 进程的概念

    进程就是正在运行的程序。主要包括两部分:

    • 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。

    • 另一个是地址空间,它包含所有可执行模块或 DLL 模块的代码和数据。它还包含动态内存

    2. 线程的概念

    线程就是描述进程的一条执行路径,进程内代码的一条执行路径。一个进程至少有一个主线程,且可以有多个线程。线程共享进程的所有资源。线程主要包括两部分:

    • 一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放

    线程统计信息的地方。

    • 另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。

    3. 进程与线程的优劣

    进程使用更多的系统资源,因为每个进程需要独立的地址空间。而线程只有一个内核对象及一个堆栈。如果有空间资源和运行效率上的考虑,则优先使用多线程。正因为每个地址有自已独立的进程空间,所以每个进程都是独立互不影响的。而一个进程中所有线程是共用进程的地址空间的,这样一个线程出问题可能影响到所有线程。像多标签浏览器容易一个见面假死导致整个浏览无法使用。所以像360浏览器等每个标签页都是一个进程,这样一个标签页面出问题并不会影响到其他标签页面。

    4. 一个进程可以创建多少线程

    32位windows中,0~4G线性内存空间。0~2G为应用程序内存空间(处于其中每个进程都有独立的内存空间),2G~4G为系统内核空间(内核进程完全共享)。那么进程的最大可用内存就是2G,每个线程栈的默认大小是1MB,理论上最多创建2048个线程,实际进程中还有一些其他地方占用内存,所以一般情况下可创建的线程总数为2000个左右。当然,如果想创建更多线程,可以缩小线程的栈大小。

    与线程有关的函数

    1. 线程的创建与终止

    线程创建API

    HANDLE CreateThread(
    
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    
    SIZE_T dwStackSize,
    
    LPTHREAD_START_ROUTINE lpStartAddress,
    
    LPVOID lpParameter,
    
    DWORD dwCreationFlags,
    
    LPDWORD lpThreadId);

    • lpThreadAttributes,描述线程安全的结构体,默认传NULL.

    • dwStackSize,堆栈大小,默认1MB.

    • lpStartAddress,线程函数入口地址。

    • lpParameter,线程函数参数。

    • dwCreationFlags,线程创建时的状态,0表示线程创建之后立即运行。CREATE_SUSPENDED表示线程创建完挂起,直到调用ResumeThread才运行。

    • lpThreadId,指向1个变量接受线程ID,可为NULL。

    线程终止API

    void ExitThread(DWORD dwExitCode);

    函数将强制终止线程的运行,并导致损伤系统清除该线程所使用的所有操作系统资源。但是C++对象可能由于析构函数没有正常调用导致资源不能得到正确释放。附加的退出码,可以用GetExitCodeThread()函数可以获取。不建议使用此线程终止函数,因为可能导致资源没有正确的释放,一般都让线程正常退出。另外,即便要强制终止线程,也要使用_endThreadEx(不使用_endThread),因为它兼顾了多线程资源安全。

    BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);

    该函数也是强制退出线程的,只不过此函数是异步的,即它告诉系统去终止指定线程,但是不能保证函数返回时线程已经被终止了。因此调用者必须使用WaitForSingleObject函数来确定线程是否终止。因此此函数调用后终止的线程堆栈资源不会得到释放。一般不建议使用此函数。

    2. 线程安全

    对线程安全没有一个比较具体的说明,简单来说线程函数的操作是安全的。这里的操作对象主要为:变量、函数、类对象。

    线程安全变量

    这里的变量指非自定义类型的全局变量/静态变量,或者通过线程参数传入的变量。

    •所有线程只读取该变量,那么该变量肯定线程安全的。

    •有1个线程写操作该变量,其他线程读取该变量。这时就需要考虑volatile。当一段线程代码多次读取变量的值时,编译器默认会优化代码只第1次会从内存上读取值,其他时候直接是从寄存器上读取的。这样如果其他线程更新了变量的值,读取的线程可能依然是从寄存器上读取的。这个时候就需要告诉编译器该变量不要优化,永远是从内存上读取。效率可能低一点,但是保证线程中变量的安全更重要。

    •有多个线程同时写操作该变量,那么就必须考虑临界区读写锁等方法。

    线程安全函数

    多线程出现之前就已经有C/C++运行时库,所以C/C++运行时库不一定是线程安全的。例如GetLastError()获取的就是一个全局的变量值,针对多线程可能就会出错。针对这个问题,MS提供了C/C++多线程运行时库,并且需要配合相应的多线程创建函数。

    _beginthreadex

    不建议使用_beginthread,因为它是早期不成熟的函数,因为它创建完成线程之后立即结束了句柄,导致不能有效控制线程。C/C++运行时库函数_beginthreadex是对操作系统函数CreateThread的封装,并且这里使用了线程局存储(TLS)来保证每个线程都有自已的单独的一些共用变量,例如像GetLastError()使用的变量。这样每个线程就能够保证所有的API函数都是线程安全的。

    AfxBeginThread

    如果当前代码环境是基于MFC库的,那么多线程创建函数必须使用MFC库函数AfxBeginThread。这是因为MFC库是对C/C++运行库的再封装,同样会面临MFC库本身存在的一些线程不安全变量的操作。AfxBeginThread其实是对_beginthreadex函数的再封装,在调用_beginthreadex之前完成一些安全载入MFC DLL库的的操作。这样基于MFC的库函数的调用才是安全的。

    线程安全类

    除了C/C++运行时库、MFC库因为已经有处理线程安全外,其他第三方库,甚至包括STL都不是线程安全的。这些自定义的类库,都需要自已去考虑线程安全。 这里可以利用锁、同步及异步等内核对象来解决,当然也可以使用TLS来解决。

    3. 线程的暂停与恢复

    在线程内核对象的内部有一个值,用于指明线程的暂停计数。当调用CreateThread函数时,就创建了线程的内核对象,并且内核对象里的暂停计数被初始化为 1,这样操作系统就不会再分配时间片给线程。当创建的线程指定CREATE_SUSPENED标志时,那么线程就处于暂停状,这个时候可以给线程进行一些优先级设置等其他初始化。当初始化完成之后,可以调用ResumeThread来恢复。单个线程可以暂时多次,如果暂停了3次,则需要ResumeThread恢复3次才能重新让线程获得时间片。

    除了创建线程指定CREATE_SUSPENED来暂停线程外,还可以调用SuspendThread来暂时线程。调用SuspendThread时,因为不知道当前线程正在做什么,如果是正在进行内存分配或者正在一个锁操作当中,可能导致其他线程锁死之类的。所以使用SuspendThread时一定要加强措施来避免可能出现的问题。

    用户模式与内核模式

    运行 Windows 的计算机中的处理器有两个不同模式:“用户模式”和“内核模式”。根据处理器上运行的代码的类型,处理器在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。多个驱动程序在内核模式下运行,但某些驱动程序在用户模式下运行。

    1. 用户模式

    当启动用户模式的应用程序时,Windows 会为该应用程序创建“进程”。进程为应用程序提供专用的“虚拟地址空间”和专用的“句柄表格”。由于应用程序的虚拟地址空间为专用空间,一个应用程序无法更改属于其他应用程序的数据。每个应用程序都孤立运行,如果一个应用程序损坏,则损坏会限制到该应用程序。其他应用程序和操作系统不会受该损坏的影响。

    用户模式应用程序的虚拟地址空间除了为专用空间以外,还会受到限制。在用户模式下运行的处理器无法访问为该操作系统保留的虚拟地址。限制用户模式应用程序的虚拟地址空间可防止应用程序更改并且可能损坏关键的操作系统数据。

    2. 内核模式

    实现操作系统的一些底层服务,比如线程调度,多处理器的同步,中断/异常处理等。

    3. 内核对象

    顾名思义,内核对象即内核创建的对象。由于内核对象的数据结构只能被内核访问,所以应用程序无法在内存中找到这些数据内容。因为要用内核来创建对象,所以必从用户模式切换到内核模式,而从用户模式切换到内核模式是需要耗费几百个时钟 周期的。建和操作若干类型的内核对象,比如存取符号对象、事件对象、文件对象、文件映射对象、I / O完成端口对象、作业对象、信箱对象、互斥对象、管道对象、进程对象、信标对象、线程对象和等待计时器对象等。内核对象是跨进程的,所以跨进程可以使用内核对象进行通信。

    时间片和原子操作

    1. 时间片

    早期CPU是单核单线程,所以不可能做到真正的多线程。时间片即是操作将CPU运行的时间划分成长短基本一致的时间区,即是时间片。多线程主要是通过操作系统不停地切换时间给不同的线程,来让线程快速交替运行,因为时间相隔很短,用户看起来像是几个线程同时在运行。当然现在CPU有多核多线程,可以做到真正的多线程了。可以使用SetThreadAffinityMask来指定线程运行在不同CPU上。

    sleep(0),当1个线程有大量计算量,容易导致CPU使用很高,而其他进程线程得不到时间片。这个时候调用sleep(0),相当告诉操作系统重新来分配时间片,这个时候同优先级的线程就可能分配得时间片,减缓计算线程大量占用时间片。

    2. 原子操作

    线程同步问题在很大程度上与原子访问有关,所谓原子访问,是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源。

    例如:

    int g_nVal = 0;
    
    DWORD WINAPI ThreadFun1(PLOVE pParam)
    
    {
    
    g_nVal++;
    
    return 0;
    
    }
    
    DWORD WINAPI ThreadFun2(PLOVE pParam)
    
    {
    
    g_nVal++;
    
    return 0;
    
    }

    因为g_nVal++是先从内存上取值放寄存器上再来进行计算,因为线程调度的不可控性,导致可能两个线程先后都是从内存上取到的0,这样自加后的结果都是1。这与我们实际想要的结果2并不一致。为了避免这种情况,就需要原子操作InterlockedExchangeAdd(g_nVal, 1)来达到效果。互锁函数操作一个内存地址时,会防止另一个CPU访问内一个内存地址。

    InterlockedExchanged/InterlockedExchangePointer,前者是交换一个值,后者是交换一组值。其作用是原子交换指定的值,并返回原来的值。因此它可以有如下的应用。

    void Fun()
    
    {
    
    while (InterlockedExchange(&g_bVal, TRUE) == TRUE)
    
    Sleep(0);
    
    // do something
    
    InterlockedExchange(&g_bVal, FALSE);
    
    }

    上面的代码能够达到一个锁的效果。原子操作不用切换到内核模式,所以速度比较快。但是上面的代码依然需要不停地循环来达到等待的效果。临界区与原子操作一样,都可以直接在用户模式下操作,并且临界区则是直接等待完全不用给当前线程分配CPU时间片。所以效率上还是临界区更优一点。

    线程池

    当线程频繁创建时,大量线程的创建销毁会占用大量的资源,导致效率低下。这个时候就可以考虑使用线程池。线程池的主要原理,即创建的线程暂时不销毁,加入空闲线程列表。当需要创建新线程时,优先去空闲线程列表中查询是否有空闲线程,有就直接用,如果没有再创建新的线程。这样就能够达到减少线程的频繁创建与销毁。

    协程

    像Python、Lua都提供了协程,尤其是Lua,因为它没有多线程,所以非常依赖协程,Lua也是将协程发挥得比较好的脚本语言。像其他语言也都有第三方实现的协程库可用。Windows多线程是由内核提供的,所以创建多线程需要切换到内核模式,因为从用户模式切换到内核模式分花费几百个时钟周期。而一种直接由用户模式提供的轻量级类多线程,其实就是协程(Coroutine)。具体来讲就是函数A调用协程函数B,然后B执行到第5行中断返回函数A继续执行其他函数C,然后下次再次调用到B时,这个时候是从B函数的第5行开始执行的。看起来就是先执行协程函数B,执行了一部分,中断去执行C,执行完C接着从上次的位置执行B。看起来是简陋的多线程,其实是利用同步达到异步的效果。C++的主要实现原理,是通过保存函数的寄存器上下文以及堆栈,下次执行协程函数时,首先恢复寄存器上下文以及堆栈,然后跳转到上次执行的函数。如果有大规模的并发,不希望频繁调用多线程,可以考虑使用协程。

    线程的分类

    1.     有消息循环线程

    •  MFC中有用户界面线程,从CWinThread派生出一个新的类作为UI线程类CUIThread,然后调用AfxBeginthread(RUNTIME_CLASS(CUIThread));启动线程。UI线程可以直接创建模态对话框,而不用担心消息循环的问题,因为UI线程默认自带消息循环。
    •  MFC非用户界面线程,不能创建模态对话框,但是可以创建非模态对话框或普通窗口,但是必须自己写消息循环。
    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
     

    2.     无消息循环线程

    • MFC中的工作者线程
    • 其他没有加消息循环的普通线程。

    线程间的通信

    1.   共享内存变量

    l  因为线程是共享进程内存的,所以通过全局/静态变量来进行通信效率最最高的。参数需要考虑是否加volitile。

    l  通过传递的参数,如引用和指针。参数需要考虑是否加volitile。

    2.   消息通知

    • 如果是子线程向主线程通信,因为主线程有消息循环,所以子线程可以通过发送消息来向主线程通信。通过消息通信能够避免使用全局变量带来的耦合性。

    SendMessage必须等待消息函数处理完成才返回,PostMessage则直接将消息放入消息队列立即返回。所以SendMessage的消息参数可以是临时变量,而PostMessage的消息参数必须保证足够的生存周期。

    • 如果子线程有自定义的消息循环,也可以通过PostThreadMessage来指定线程通信。
    while(true)
    {
        if(GetMessage(&msg,0,0,0)) //get msgfrom message queue
        {
            switch(msg.message)
            {
                case MY_MSG:
                // Todo:
                break;
            }
        }
    };

    3.   其他方式

    • 所有跨进程的通信方式,当然可以用于跨线程了。

    线程之间的状态

    1.   异步

    即多个线程彼此独立,不受外部线程的影响。线程本身就是实现异步的一种方式。

    2.   同步

    即多个线程彼此依赖,线程A的计算结果是线程B的计算的前提,也就是说在开始线程B的计算之前必须等待线程A的计算完。

    3.   互斥

    即多个线程在操作同一个资源时,一个线程必须等另一个线程结束了才能继续操作。互斥与同步不同之处是,互斥没有先后关系。同一个资源,可以指全局变量,也可以指一个文件对象或是其他的内核对象。因为内核对象是跨进程的,所以更是跨线程的。

                等待函数

    1.    概念

    WaitForSingleObject函数是等待内核对象从无信号状态到有信号状态或是超时即返回。也即无信号状态时等待,有信号或超时立即返回。

    WaitForMulitpleObjects函数是等待多个内核对象从无信号状态到有信号状态或是超时即返回(可以指明是所有对象或是任一对象)。

    Windows拥有几种内核对象可以处于已通知状态和未通知状态:进程、线程、作业、文件、控制台输入/输出/错误流、事件、等待定时器、信号量、互斥对象。

    2.    等待函数与内核对象之间的关系

    对象

    无信号状态

    有信号状态

    成功等待副作用

    进程

    进程活动时

    进程终止时

    线程

    线程活动时

    线程终止时

    文件

    I/O请求正在处理时

    I/O请求结束时

    控制台输入

    不存在任何输入

    存在输入时

    文件修改通知

    没有任何文件修改通知

    文件系统发现修改时

    重置通知

    自动重置事件

    ResetEvent, PulseEvent或等待成功

    当调用SetEvent或PulseEvnet时

    重置事件

    人工重置事件

    ResetEvent,或PulseEvent

    当调用SetEvent或PulseEvnet时

    自动重置定时器

    CancelWaitableTimer或等待成功

    当时间到时(SetWaitableTimer)

    重置定时器

    人工重置定时器

    CancelWaitableTimer

    当时间到时(SetWaitableTimer)

    信号量

    等待成功

    当资源数量>0时(ReleaseSemaphore)

    数量减1

    互斥量

    等待成功

    当未被线程拥有时(ReleaseMutex)

    获取线程所有权

    l 线程和进程创建及运行时都是无信号状态,当结束运行时变为有信号状态。

    l 自动重置的事件(FALSE)对象,当等待成功的时候,会被修改为无信号状态。

    l 信号量对象,当调用ReleaseSemaphore(数量加1),处于有信号状态,WaitForSingleObject会被触发并且立即将信号数量减1.

            用户模式与内核模式的优缺点

    1.   用户模式

    优点:线程同步机制速度快

    缺点:容易陷入死锁状态多个进程之间的线程同步会出现问题。(比如竞争资源、死锁)

    2.   内核模式

    优点:支持多个进程之间的线程同步,防止死锁

    缺点:线程同步机制速度慢,线程必须从用户模式转为内核模式。这个转换需要很大的代价:往返一次需要占用x 8 6平台上的大约1 0 0 0个C P U周期。

    线程间的状态处理

    1.   线程的异步

    因为线程本身就是异步的。

    2.   线程的同步

    线程的同步主要是通过事件(Event)内核对象、信号量(Semaphore)内核对象和互斥量(Mutex)内核对象。因为都是内核对象,所以不仅可以跨线程操作,还可以跨进程同步。

    1.      线程的同步

    线程的同步主要是通过事件(Event)内核对象、信号量(Semaphore)内核对象和互斥量(Mutex)内核对象。因为都是内核对象,所以不仅可以跨线程操作,还可以跨进程同步。

    事件(Event)内核对象

    事件分两种类型:人工重置事件和自动重置事件,前者在触发WaitForSingleObject之后需要手动调用ResetEvent将事件设置为无信号;而后者在触发WaitForSingleObject之后自动将事件设置为无信号状态。

    常用函数:

    CreateEvent,创建事件对象。

    OpenEvent,打开已经创建的事件对象,可以跨进程打开。

    SetEvent,将事件对象设置为有信号状态。

    ResetEvent,将事件对象设置为无信号状态。

    PulseEvent,将事件对象设置为有信号状态,然后又设置为无信号状态,此函数不常用。

    HANDELg_hEvent;
    int Main()
    {
    g_hEvent =CreateEvent(NULL, TRUE, FALSE, NULL);
    _beginthreadex(NULL,0, ThreadFun1, 0);
    _beginthreadex(NULL,0, ThreadFun2, 0);
    SetEvnet(g_hEvent);//
    }
    
    DWORD WINAPIThreadFun1(PVOID pParam)
    {
    WaitForSingleObject(g_hEvent);
    // Todo...
    SetEvent(g_hEvnet);
    return 0;
    }
    
    DWORD WINAPIThreadFun2(PVOID pParam)
    {
    WaitForSingleObject(g_hEvent);
    // Todo...
    SetEvent(g_hEvnet);
    
    return 0;
    }
    

    注意:如果上面创建的是人工重置事件,则两个线程函数都将执行。如果是自动重置事件,则只能执行一个线程,且不能保证哪一个线程先执行。如果要保证一个线程先执行,可以添加事件对象用来确保指定线程已经执行,不能通过代码的先后顺序确保线程已经执行。

    2.      信号量(Semaphore)内核对象

    信号量的使用规则:

    当前信号量资源数大于0,则标记为有信号状态。

    当前信号量资源数为0,则标记为无信号状态。

    信号量资源数不能为负,且最大不能超过指定数量。

    常用函数:

    CreateSemaphore,创建信号量对象。

    OpenSemaphore,打开指定信号量对象,可以跨进程。

    ReleaseSemaphoer,资源计算加1。

    HANDELg_hSema[2];
    
    int Main()
    {
    g_hSema[0] =CreateSemaphore(NULL, 1, 1, NULL);
    
    g_hSema[1] =CreateSemaphore(NULL, 0, 1, NULL);
    
    _beginthreadex(NULL,0, ThreadFun1, 0);
    
    _beginthreadex(NULL,0, ThreadFun2, 0);
    }
    
    DWORD WINAPIThreadFun1(PVOID pParam)
    {
    WaitForSingleObject(g_hSema[0]);
    
    // Todo...
    ReleaseSemaphoer(g_hSema[1]);
    
    return 0;
    }
    
    DWORD WINAPIThreadFun2(PVOID pParam)
    {
    WaitForSingleObject(g_hSema[1]);
    
    // Todo...
    ReleaseSemaphoer(g_hSema[0]);
    
    return 0;
    }

    这样就能够保证ThreadFun1执行完了,再执行ThreadFun2,然后再执行ThreadFun1,并且保证每个线程函数只能被调用一次.

    3.      互斥量(Mutex)内核对象

    互斥量内核对象确保线程拥有单个资源的互斥访问权。在行为特性上,互斥量与临界区的一样。只不过,互斥量是内核对象,使用时需要从用户模式切换到内核模式,比较耗时。但正因为是内核对象,所以互斥量能够跨进程,并且能够设置超时时间,这是它比临界区灵活的地方。

    常用函数:

    CreateMutex,创建互斥量对象。

    OpenMutex,打开指定互斥量对象,可以跨进程。

    ReleaseMutex,释放互斥量,对象被标记为有信号状态,触发WaitForSingleObject。

    互斥量和临界区一样,拥有一个线程拥有权的概念,即当前互斥量和当前临界区的释放只能由当前线程释放,其他线程释放无效。因为互斥量是内核对象,如果线程已经终止,但是其所属的互斥量依然没有释放,内核管理器会自动释放。临界区没有这个功能,因为临界区不是内核对象,所以临界区如果没有正确释放会导致死锁。

    HANDLECreateMutex(  LPSECURITY_ATTRIBUTESlpMutexAttributes,

      BOOL bInitialOwner,  LPCTSTR lpName);

    bInitialOwner标记是否由创建线程拥有线程所有权,TRUE表示创建者拥有,FALSE表示创建者不拥有,则是第一个调用WaitForSingleObject的线程将获得线程所有权。

    HANDELg_hMutex;
    
    int Main()
    {
    g_hMutex =CreateMutex(NULL,FALSE);
    
    _beginthreadex(NULL,0, ThreadFun1, 0);
    
    _beginthreadex(NULL,0, ThreadFun2, 0);
    }
    
    DWORD WINAPIThreadFun1(PVOID pParam)
    {
    WaitForSingleObject(g_hMutex);
    
    // Todo...
    
    ReleaseMutex(g_hMutex);
    
    return 0;
    }
    
    DWORD WINAPIThreadFun2(PVOID pParam)
    {
    WaitForSingleObject(g_hMutex);
    
    // Todo...
    ReleaseMutex(g_hMutex);
    
    return 0;
    }
    

    两个函数谁先调用,谁即获取线程所有权。如果想指定线程先运行,需要判断指定线程已经执行之后再创建新线程,不能依靠线程的代码创建先后顺序。

    3.   线程的互斥

    像互斥量对象同样可以达到互斥的效果,只是互斥量功能更丰富,并且如果是简单的资源互斥,使用临界区的效率更优。

    临界区(Critical Section)是一段供线程独占式访问的代码,也就是说若有一线程正在访问该代码段,其它线程想要访问,只能等待当前线程离开该代码段方可进入,这样保证了线程安全。他工作于用户级(相对于内核级),在Window系统中CRITICAL_SECTION实现临界区相关机制。

    常用函数:

    voidInitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)  // 初始化临界区

    voidEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)       // 进入临界区

    voidLeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)       // 离开临界区

    voidDeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection)      // 释放临界区资源

    因为临界区拥有线程所有权这个概念,即进入临界区的线程才有权释放临界区。因为必须当前线程进入和释放,更多的时候,临界区是在一个函数里使用,为了确保不会由于中间退出函数导致没有释放,我们可以用下列方式来确保释放。

    class Mutex {
    public:
      Mutex()                      {InitializeCriticalSection(section); }
    
      ~Mutex()                     { DeleteCriticalSection(section);}
    
      void Enter()                {EnterCriticalSection(section); }
    
      void Leave()                {LeaveCriticalSection(section); }
    
      struct Lock;
    
    protected:
      Mutex(const Mutex&);
    
      Mutex& operator=(const Mutex&);
    
      CRITICAL_SECTION section;
    };
     
    
    structMutex::Lock {
      Mutex& s;
    
      Lock(Mutex& s) : s(s) { s.Enter(); }
    
      ~Lock()               { s.Leave(); }
    };
    
    
    DWORD WINAPIThreadFun(PVOID pParam)
    {
    Mutex::Locklock(mutex);
    
    // Todo...
    
    return 0;
    }

    注意

    1.      注意所有内核对象在结束时都需要调用closeHandle()。

    2.      跨线程调用MFC对象函数都是不安全的。因为MFC对象的一些函数都与TLS有关联,  所以有些调用会出错。如UpdateData(),最好通过句柄发消息来完成相应的功能。

    展开全文
  • 一、QT 窗口置顶 一般情况下,我们开发QT桌面应用程序如需用到窗口置顶操作时,会这样做: setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); 但经过尝试发现此方法会导致窗口被隐藏,再次调用 show方法...

    一、QT 窗口置顶

    一般情况下,我们开发QT桌面应用程序如需用到窗口置顶操作时,会这样做:

    setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
    

    但经过尝试发现此方法会导致窗口被隐藏,再次调用 show方法又会导致闪烁,查看源码后发现在窗口被创建后(构造函数执行完成后),调用setWindowFlags会导致窗口被 hide,而在构造函数内部调用setWindowFlags则不会引起窗口的hide。

    因此此方法并不适用。

    二、windows 下窗口置顶

    windows 下窗口置顶可采用下面的方法:

      ::SetWindowPos((HWND)winId(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
    

    取消置顶:

      ::SetWindowPos((HWND)winId(), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
    

    SetWindowPos 方法函数原型:

    WINUSERAPI BOOL WINAPI SetWindowPos(HWND hWnd,HWND hWndInsertAfter,int X,int Y,int cx,_In_ int cy, UINT uFlags);
    

    SetWindowPos 方法参数详解:

    序号 参数
    展开全文
  • 03.浅谈在Windows窗口线程使用控件遇到“线程间操作无效: 从不是创建控件“textBox1”的线程访问它”异常的处理方法 在使用C#进行跨线程操作窗体控件时会遇到如下图中的异常: 异常起因: 在程序进程中windows框体...

    03.浅谈在Windows窗口跨线程使用控件遇到“线程间操作无效: 从不是创建控件“textBox1”的线程访问它”异常的处理方法
    在使用C#进行跨线程操作窗体控件时会遇到如下图中的异常:
    跨线程操作控件异常信息
    异常起因:
    在程序进程中windows框体处于一个独立的线程(下文称:主线程),当一个外部线程操作主线程中的控件或其他内容时,由于系统稳定性的限制不能直接进行跨线程操作的,便会出现以上异常信息。
    可以通俗的如下理解:
    有A,B两家公司,如果A公司需要使用B公司的专利B1,一般情况下是不能直接免费的使用B1的。如果A公司未经B公司同意使用B1就会引起B公司的反制
    解决方法(以WinForm为例):
    1):关闭跨线程检查
    直接将窗体的CheckForIllegalCrossThreadCalls(跨线程检查)属性设置为false,默认此设置是true

        CheckForIllegalCrossThreadCalls = false;
    

    2):使用异步上下文的方式进行跨线程操作(需要引用System.Threading;)
    使用步骤:
    a.获取窗体的操作线程的同步上下文
    SynchronizationContext syncontext = SynchronizationContext.Current;
    b.使用 SynchronizationContext 的Post函数进行异步操作
    syncontext.Post(_ => {…TODO…}, null);
    实例代码:

    syncontext.Post(_ => { textBox1.Text = $"当前跨线程检查是否启动:{CheckForIllegalCrossThreadCalls}{Environment.NewLine}使用SynchronizationContext的post方式进行跨线程显示"; }, null);
    
       补充知识->Post和Send的区别:
                Post:将跨线程的进行的操作同步到主线程中,相当于 BeginInvoke
                Send:将同线程的操作同步到控件中,相当于Invoke
              实际使用中要注意区分,否则仍可能会出现跨线程异常
    

    3):使用控件自带的异步方法
    使用步骤:
    a.根据控件的InvokeRequired属性来判断是否是跨线程对控件的操作(以TextBox控件为例)

          bool isCrossThreadCalls= textBox1.InvokeRequired);//是否是跨线程的,true为跨线程
    
    b.使用控件的异步Inovke方法
    
    textBox1.Invoke(new Action(() => { textBox1.Text = $"当前跨线程检查是否启动:{CheckForIllegalCrossThreadCalls}{Environment.NewLine}使用Invoke方式进行跨线程显示"; }));
    

    实例代码:

    if (textBox1.InvokeRequired)
      textBox1.Invoke(new Action(() => { textBox1.Text = $"当前跨线程检查是否启动:{CheckForIllegalCrossThreadCalls}{Environment.NewLine}使用Invoke方式进行跨线程显示"; }));
    
    补充知识:
    如果在主线程中使用控件的In方法会出现如下异常,因此必须要区分主线程操作和跨线程操作:
    

    在主线程中使用控件的Inovke遇到的异常
    水平有限,欢迎大家交流指正!!!
    实例项目代码

    展开全文
  • NtReadFile: // Windows XP 77f5bfa8 b8b7000000 mov eax,0xb7 77f5bfad ba0003fe7f mov edx,0x7ffe0300 77f5bfb2 ffd2 call edx {SharedUserData!SystemCallStub (7ffe0300)} 77f5bfb4 c22400 ret 0x24 77f5bfb7 90...

    根据前面的介绍,NT内核会把操作系统的代码和数据映射到系统中所有进程的内核空间中。这样,每个进程内的应用程序代码便可以很方便地调用内核空间中的系统服务。这里的“很方便”有多层含义,一方面是内核代码和用户代码在一个地址空间中,应用程序调用系统服务时不需要切换地址空间,另一方面是整个系统中内核空间的地址是统一的,编写内核空间的代码时会简单很多。但是,如此设计也带来一个很大的问题,那就是用户空间中的程序指针可以指向内核空间中的数据和代码,因此必须防止用户代码破坏内核空间中的操作系统。怎么做呢?答案是利用权限控制来实现对内核空间的保护。

    2.6.1 访问模式

    Windows定义了两种访问模式(access mode)——用户模式(user mode,也称为用户态)和内核模式(kernel mode,也称为内核态)。应用程序(代码)运行在用户模式下,操作系统代码运行在内核模式下。内核模式对应于处理器的最高权限级别(不考虑虚拟机情况),在内核模式下执行的代码可以访问所有系统资源并具有使用所有特权指令的权利。相对而言,用户模式对应于较低的处理器优先级,在用户模式下执行的代码只可以访问系统允许其访问的内存空间,并且没有使用特权指令的权利。

    本书卷1介绍过,IA-32处理器定义了4种特权级别(privilege level),或者称为环(ring),分别为0、1、2、3,优先级0(环0)的特权级别最高。处理器在硬件一级保证高优先级的数据和代码不会被低优先级的代码破坏。Windows系统使用了IA-32处理器所定义的4种优先级中的两种,优先级3(环3)用于用户模式,优先级0用于内核模式。之所以只使用了其中的两种,主要是因为有些处理器只支持两种优先级,比如Compaq Alpha处理器。值得说明的是,对于x86处理器来说,并没有任何寄存器表明处理器当前处于何种模式(或优先级)下,优先级只是代码或数据所在的内存段或页的一个属性,参见卷1的2.6节和2.7节。

    因为内核模式下的数据和代码具有较高的优先级,所以用户模式下的代码不可以直接访问内核空间中的数据,也不可以直接调用内核空间中的任何函数或例程。任何这样的尝试都会导致保护性错误。也就是说,即使用户空间中的代码指针正确指向了要访问的数据或代码,但一旦访问发生,那么处理器会检测到该访问是违法的,会停止该访问并产生保护性异常(#GP)。

    虽然不可以直接访问,但是用户程序可以通过调用系统服务来间接访问内核空间中的数据或间接调用、执行内核空间中的代码。当调用系统服务时,主调线程会从用户模式切换到内核模式,调用结束后再返回到用户模式,也就是所谓的模式切换。在线程的KTHREAD结构中,定义了UserTime和KernelTime两个字段,分别用来记录这个线程在用户模式和内核模式的运行时间(以时钟中断次数为单位)。模式切换是通过软中断或专门的快速系统调用(fast system call)指令来实现的。下面通过一个例子来分别介绍这两种切换机制。

    2.6.2 使用INT 2E切换到内核模式

    图2-4展示了在Windows 2000中通过INT 2E从应用程序调用ReadFile() API的过程。因为ReadFile() API是从Kernel32.dll导出的,所以我们看到该调用首先转到Kernel32.dll中的ReadFile()函数,ReadFile()函数在对参数进行简单检查后便调用NtDll.dll中的NtReadFile()函数。

    Windows操作系统管理进程和线程:内核模式和用户模式

     

    图2-4 通过INT 2E从应用程序调用ReadFile() API的过程

    通过反汇编可以看到,NtDll.dll中的NtReadFile ()函数非常简短,首先将ReadFile()对应的系统服务号(0xa1,与版本有关)放入EAX寄存器中,将参数指针放入EDX寄存器中,然后便通过INT n指令发出调用。这里要说明的一点是,虽然每个系统服务都具有唯一的号码,但微软公司没有公开这些服务号,也不保证这些号码在不同的Windows版本中会保持一致。

    ntdll!NtReadFile: // Windows 2000
    77f8fb5d b8a1000000    mov      eax,0xa1
    77f8fb62 8d542404      lea      edx,[esp+0x4]
    77f8fb66 cd2e          int      2e
    77f8fb68 c22400        ret      0x24

    在WinDBG下通过!idt 2e命令可以看到2e号向量对应的服务例程是KiSystemService ()。KiSystemService ()是内核态中专门用来分发系统调用的例程。

    lkd> !idt 2e
    Dumping IDT:
    2e:   804db1ed nt!KiSystemService

    Windows将2e号向量专门用于系统调用,在启动早期初始化中断描述符表(Interrupt Descriptor Table,IDT)时(见第11章)便注册好了合适的服务例程。因此当NTDll.DLL中的NtReadFile()发出INT 2E指令后,CPU便会通过IDT找到KiSystemService ()函数。因为KiSystemService ()函数是位于内核空间的,所以CPU在把执行权交给KiSystemService ()函数前,会做好从用户模式切换到内核模式的各种工作,包括:

    (1)权限检查,即检查源位置和目标位置所在的代码段权限,核实是否可以转移;

    (2)准备内核模式使用的栈,为了保证内核安全,所有线程在内核态执行时都必须使用位于内核空间的内核栈(kernel stack),内核栈的大小一般为8KB或12KB。

    KiSystemService ()会根据服务ID从系统服务分发表(System Service Dispatch Table)中查找到要调用的服务函数地址和参数描述,然后将参数从用户态栈复制到该线程的内核栈中,最后KiSystemService ()调用内核中真正的NtReadFile()函数,执行读文件的操作,操作结束后会返回到KiSystemService (),KiSystemService ()会将操作结果复制回该线程用户态栈,最后通过IRET指令将执行权交回给NtDll.dll中的NtReadFile()函数(继续执行INT 2E后面的那条指令)。

    通过INT 2E进行系统调用时,CPU必须从内存中分别加载门描述符和段描述符才能得到KiSystemService ()的地址,即使门描述符和段描述符已经在高速缓存中,CPU也需要通过“内存读(memory read)”操作从高速缓存中读出这些数据,然后进行权限检查。

    2.6.3 快速系统调用

    因为系统调用是非常频繁的操作,所以如果能减少这些开销还是非常有意义的。可以从两个方面来降低开销:一是把系统调用服务例程的地址放到寄存器中以避免读IDT这样的内存操作,因为读寄存器的速度比读内存的速度要快很多;二是避免权限检查,也就是使用特殊的指令让CPU省去那些对系统服务调用来说根本不需要的权限检查。奔腾II处理器引入的SYSENTER/SYSEXIT指令正是按这一思路设计的。AMD K7引入的SYSCALL/SYSRETURN指令也是为这一目的而设计的。相对于INT 2E,使用这些指令可以加快系统调用的速度,因此利用这些指令进行的系统调用称为快速系统调用。

    下面我们介绍Windows系统是如何利用IA-32处理器的SYSENTER/SYSEXIT指令(从奔腾II开始)实现快速系统调用的[2]。首先,Windows 2000或之前的Windows系统不支持快速系统调用,它们只能使用前面介绍的INT 2E方式进行系统调用。Windows XP和Windows Server 2003或更新的版本在启动过程中会通过CPUID指令检测CPU是否支持快速系统调用指令(EDX寄存器的SEP标志位)。如果CPU不支持这些指令,那么仍使用INT 2E方式。如果CPU支持这些指令,那么Windows系统便会决定使用新的方式进行系统调用,并做好如下准备工作。

    (1)在全局描述符表(GDT)中建立4个段描述符,分别用来描述供SYSENTER指令进入内核模式时使用的代码段(CS)和栈段(SS),以及SYSEXIT指令从内核模式返回用户模式时使用的代码段和栈段。这4个段描述符在GDT中的排列应该严格按照以上顺序,只要指定一个段描述符的位置便能计算出其他的。

    (2)设置表2-1中专门用于系统调用的MSR(关于MSR的详细介绍见卷1的2.4.3节),SYSENTER_EIP_MSR用于指定新的程序指针,也就是SYSENTER指令要跳转到的目标例程地址。Windows系统会将其设置为KiFastCallEntry的地址,因为KiFastCallEntry例程是Windows内核中专门用来受理快速系统调用的。SYSENTER_CS_MSR用来指定新的代码段,也就是KiFastCallEntry所在的代码段。SYSENTER_ESP_MSR用于指定新的栈指针(ESP)。新的栈段是由SYSENTER_CS_MSR的值加8得来的。

    (3)将一小段名为SystemCallStub的代码复制到SharedUserData内存区,该内存区会被映射到每个Win32进程的进程空间中。这样当应用程序每次进行系统调用时,NTDll.DLL中的残根(stub)函数便调用这段SystemCallStub代码。SystemCallStub的内容因系统硬件的不同而不同,对于IA-32处理器,该代码使用SYSENTER指令,对于AMD处理器,该代码使用SYSCALL指令。

    表2-1 供SYSENTER指令使用的MSR(略)

    例如在配有Pentium M CPU的Windows XP系统上,以上3个寄存器的值分别为:

    lkd> rdmsr 174
    msr[174] = 00000000`00000008
    lkd> rdmsr 175
    msr[175] = 00000000`bacd8000
    lkd> rdmsr 176
    msr[176] = 00000000`8053cad0

    其中SYSENTER_CS_MSR的值为8,这是Windows系统的内核代码段的选择子,即常量KGDT_R0_CODE的值。WinDBG帮助文件中关于dg命令的说明中列出了这个常量。SYSENTER_EIP_MSR的值是8053cad0,检查nt内核中KiFastCallEntry函数的地址。

    lkd> x nt!KiFastCallEntry
    8053cad0 nt!KiFastCallEntry = <no type information>

    可见,Windows把快速系统调用的目标指向内核代码段中的KiFastCallEntry函数。

    通过反汇编Windows XP下NTDll.DLL中的NtReadFile ()函数,可以看到SystemCallStub被映射到进程的0x7ffe0300位置。与前面Windows 2000下的版本相比,容易看到该服务的系统服务号码在这两个版本间是不同的。

    kd> u ntdll...
    ntdll!NtReadFile: // Windows XP
    77f5bfa8 b8b7000000       mov     eax,0xb7
    77f5bfad ba0003fe7f       mov     edx,0x7ffe0300
    77f5bfb2 ffd2             call edx {SharedUserData!SystemCallStub (7ffe0300)}
    77f5bfb4 c22400           ret     0x24
    77f5bfb7 90               nop

    观察本段下面反汇编SystemCallStub的结果,它只包含3条指令,分别用于将栈指针(ESP寄存器)放入EDX寄存器中、执行sysenter指令和返回。第一条指令有两个用途:一是向内核空间传递参数;二是指定从内核模式返回时的栈地址。因为笔者使用的是英特尔奔腾M处理器,所以此处是sysenter指令,对于AMD处理器,此处应该是syscall指令。

    kd> u...
    SharedUserData!SystemCallStub:
    7ffe0300 8bd4             mov     edx,esp
    7ffe0302 0f34             sysenter
    7ffe0304 c3               ret

    下面让我们看一下KiFastCallEntry例程,其清单如下所示。

    kd> u nt!KiFastCallEntry L20
    nt!KiFastCallEntry:
    804db1bb 368b0d40f0dfff   mov      ecx,ss:[ffdff040]
    804db1c2 368b6104         mov      esp,ss:[ecx+0x4]
    804db1c6 b90403fe7f       mov      ecx,0x7ffe0304
    804db1cb 3b2504f0dfff     cmp      esp,[ffdff004]
    804db1d1 0f84cc030000     je       nt!KiServiceExit2+0x13f (804db5a3)
    804db1d7 6a23             push     0x23
    804db1d9 52               push     edx
    804db1da 83c208           add      edx,0x8
    804db1dd 6802020000       push     0x202
    804db1e2 6a02             push     0x2
    804db1e4 9d               popfd
    804db1e5 6a1b             push     0x1b
    804db1e7 51               push     ecx // Fall Through,自然进入KiSystemService函数
    nt!KiSystemService:
    804db1e8 90               nop
    804db1e9 90               nop
    804db1ea 90               nop
    804db1eb 90               nop
    804db1ec 90                nop
    nt!KiSystemService:
    804db1ed 6a00             push      0x0
    804db1ef 55               push      ebp

    显而易见,KiFastCallEntry在做了些简单操作后,便下落(fall through)到KiSystemService函数了,也就是说,快速系统调用和使用INT 2E进行的系统调用在内核中的处理绝大部分是一样的。另外,请注意ecx寄存器,mov ecx,0x7ffe0304将其值设为0x7ffe0304,也就是SharedUserData内存区里SystemCallStub例程中ret指令的地址(参见上文的SystemCallStub代码)。在进入nt!KiSystemService之前,ecx连同其他一些参数被压入栈中。事实上,ecx用来指定SYSEXIT返回用户模式时的目标地址。当使用INT 2E进行系统调用时,由于INT n指令会自动将中断发生时的CS和EIP寄存器压入栈中,当中断处理例程通过执行iretd返回时,iretd指令会使用栈中保存的CS和EIP值返回合适的位置。因为sysenter指令不会向栈中压入要返回的位置,所以sysexit指令必须通过其他机制知道要返回的位置。这便是压入ECX寄存器的原因。通过反汇编KiSystemCallExit2例程,我们可以看到在执行sysexit指令之前,ecx寄存器的值又从栈中恢复出来了。

    kd> u nt!KiSystemCallExit l20
    nt!KiSystemCallExit:
    804db3b4 cf            iretd
    nt!KiSystemCallExit2:
    804db3b5 5a            pop      edx
    804db3b6 83c408        add      esp,0x8
    804db3b9 59            pop      ecx
    804db3ba fb            sti
    804db3bb 0f35          sysexit
    nt!KiSystemCallExit3:
    804db3bd 59            pop      ecx
    804db3be 83c408        add      esp,0x8
    804db3c1 5c            pop      esp
    804db3c2 0f07          sysret

    以上代码中包含了3个从系统调用返回的例程,即KiSystemCallExit、KiSystemCallExit2和KiSystemCallExit3,它们分别对应于使用INT 2E、sysenter和syscall发起的系统调用,如表2-2所示。

    表2-2 系统调用(略)

    图2-5展示了使用sysenter/sysexit指令对进行系统调用的完整过程(以调用ReadFile服务为例)。

    Windows操作系统管理进程和线程:内核模式和用户模式

     

    图2-5 快速系统调用(针对IA-32处理器)


     

     

    格物

    下面通过一个小的实验来加深大家对系统调用的理解。首先启动WinDBG程序,选择File → Open Crash Dump,然后选择本书实验文件中的dumps\w732cf4.dmp文件。在调试会话建立后,先执行.symfix c:\symbols和.reload加载模块与符号,再执行k命令,便得到清单2-4所示的完美栈回溯。

    第22章将详细讲解栈回溯的原理,现在大家只要知道栈上记录着函数相互调用时的参数和返回地址等信息。栈回溯是从栈上找到这些信息,然后显示出来的过程,是追溯线程执行轨迹的一种便捷方法。

    清单2-4还显示了任务管理器程序(taskmgr)调用NtTerminateProcess系统服务时的执行过程。栈回溯的结果包含4列,第一列是序号,第二列是每个函数的栈帧基地址,第三列是返回地址,第四列是使用“函数名+字节偏移量”形式表达的执行位置。以00栈帧为例,它对应的函数是著名的蓝屏函数KeBugCheckEx,它的栈帧基地址是9796fb9c,它的返回地址是82b1ab51,翻译成符号便是PspCatchCriticalBreak+0x71。

    清单2-4 完美栈回溯

    # ChildEBP RetAddr  
    00 9796fb9c 82b1ab51 nt!KeBugCheckEx+0x1e
    01 9796fbc0 82a6daa8 nt!PspCatchCriticalBreak+0x71
    02 9796fbf0 82a605b6 nt!PspTerminateAllThreads+0x2d
    03 9796fc24 8287c87a nt!NtTerminateProcess+0x1a2
    04 9796fc24 77da7094 nt!KiFastCallEntry+0x12a
    05 001df4dc 77da68d4 ntdll!KiFastSystemCallRet
    06 001df4e0 76193c82 ntdll!NtTerminateProcess+0xc
    07 001df4f0 00bf57b9 KERNELBASE!TerminateProcess+0x2c
    08 001df524 00bf67ec taskmgr!CProcPage::KillProcess+0x116
    09 001df564 00bebc96 taskmgr!CProcPage::HandleWMCOMMAND+0x10f
    0a 001df5d8 76abc4e7 taskmgr!ProcPageProc+0x275
    0b 001df604 76ad5b7c USER32!InternalCallWinProc+0x23
    0c 001df680 76ad59f3 USER32!UserCallDlgProcCheckWow+0x132
    0d 001df6c8 76ad5be3 USER32!DefDlgProcWorker+0xa8
    0e 001df6e4 76abc4e7 USER32!DefDlgProcW+0x22
    0f 001df710 76abc5e7 USER32!InternalCallWinProc+0x23
    10 001df788 76ab5294 USER32!UserCallWinProcCheckWow+0x14b
    11 001df7c8 76ab5582 USER32!SendMessageWorker+0x4d0
    12 001df7e8 74e94601 USER32!SendMessageW+0x7c
    13 001df808 74e94663 COMCTL32!Button_NotifyParent+0x3d
    14 001df824 74e944ed COMCTL32!Button_ReleaseCapture+0x113
    15 001df884 76abc4e7 COMCTL32!Button_WndProc+0xa18
    16 001df8b0 76abc5e7 USER32!InternalCallWinProc+0x23
    17 001df928 76abcc19 USER32!UserCallWinProcCheckWow+0x14b
    18 001df988 76abcc70 USER32!DispatchMessageWorker+0x35e
    19 001df998 76ab41eb USER32!DispatchMessageW+0xf
    1a 001df9bc 00be16fc USER32!IsDialogMessageW+0x588
    1b 001dfdac 00be5384 taskmgr!wWinMain+0x5d1
    1c 001dfe40 76bbed6c taskmgr!_initterm_e+0x1b1
    1d 001dfe4c 77dc377b kernel32!BaseThreadInitThunk+0xe
    1e 001dfe8c 77dc374e ntdll!__RtlUserThreadStart+0x70
    1f 001dfea4 00000000 ntdll!_RtlUserThreadStart+0x1b

    仔细观察清单2-4中的地址部分,很容易看出用户空间和内核空间的分界,也就是在栈帧04和栈帧05之间。栈帧05中的KiFastSystemCallRet函数属于ntdll模块,位于用户空间。栈帧04中的KiFastCallEntry函数属于nt模块,位于内核空间。栈帧04的基地址是9796fc24,属于内核空间;栈帧05的基地址是001df4dc,属于用户空间。它们分别来自这个线程的内核态栈和用户态栈。WinDBG的k命令穿越两个空间,遍历两个栈,显示出线程在用户空间和内核空间执行的完整过程,能产生如此完美的栈回溯显示了WinDBG的强大。


    2.6.4 逆向调用

    前文介绍了从用户模式进入内核模式的两种方法,通过这两种方法,用户模式的代码可以“调用”位于内核模式的系统服务。那么内核模式的代码是否可以主动调用用户模式的代码呢?答案是肯定的,这种调用通常称为逆向调用(reverse call)。

    简单来说,逆向调用的过程是这样的。首先内核代码使用内核函数KiCallUserMode发起调用。接下来的执行过程与从系统调用返回(KiServiceExit)类似,不过进入用户模式时执行的是NTDll.DLL中的KiUserCallbackDispatcher。而后KiUserCallbackDispatcher会调用内核希望调用的用户态函数。当用户模式的工作完成后,执行返回动作的函数会执行INT 2B指令,也就是触发一个0x2B异常。这个异常的处理函数是内核模式的KiCallbackReturn函数。于是,通过INT 2B异常,CPU又跳回内核模式继续执行了。

    lkd> !idt 2b
    Dumping IDT:
    2b:   8053d070 nt!KiCallbackReturn

    以上是使用WinDBG的!idt命令观察到的0x2B异常的处理函数。

    2.6.5 实例分析

    下面通过一个实际例子来进一步展示系统调用和逆向调用的执行过程。清单2-5显示了使用WinDBG的内核调试会话捕捉到的记事本进程发起系统调用进入内核和内核函数执行逆向调用的全过程(栈回溯)。

    清单2-5 记事本进程从发起系统调用进入内核和内核函数逆向调用的全过程

    kd> kn
     # ChildEBP RetAddr  
    00 0006fe94 77fb4da6 USER32!XyCallbackReturn
    01 0006fe94 8050f8ae ntdll!KiUserCallbackDispatcher+0x13
    02 f4fc19b4 80595d2c nt!KiCallUserMode+0x4
    03 f4fc1a10 bf871e98 nt!KeUserModeCallback+0x87
    04 f4fc1a90 bf8748d4 win32k!SfnDWORD+0xa0
    05 f4fc1ad8 bf87148d win32k!xxxSendMessageToClient+0x174
    06 f4fc1b24 bf8714d3 win32k!xxxSendMessageTimeout+0x1a6
    07 f4fc1b44 bf8635f6 win32k!xxxSendMessage+0x1a
    08 f4fc1b74 bf84a620 win32k!xxxMouseActivate+0x22d
    09 f4fc1c98 bf87a0c1 win32k!xxxScanSysQueue+0x828
    0a f4fc1cec bf87a8ad win32k!xxxRealInternalGetMessage+0x32c
    0b f4fc1d4c 804da140 win32k!NtUserGetMessage+0x27
    0c f4fc1d4c 7ffe0304 nt!KiSystemService+0xc4
    0d 0006feb8 77d43a21 SharedUserData!SystemCallStub+0x2
    0e 0006febc 77d43c95 USER32!NtUserGetMessage+0xc
    0f 0006fed8 010028e4 USER32!GetMessageW+0x31
    10 0006ff1c 01006c54 notepad!WinMain+0xe3
    11 0006ffc0 77e814c7 notepad!WinMainCRTStartup+0x174
    12 0006fff0 00000000 kernel32!BaseProcessStart+0x23

    根据执行的先后顺序,最下面一行(帧#12)对应的是进程的启动函数BaseProcessStart,而后是编译器生成的进程启动函数WinMainCRTStartup,以及记事本程序自己的入口函数WinMain。帧#0f表示记事本程序在调用GetMessage API进入消息循环。接下来GetMessage API调用Windows子系统服务的残根函数NtUserGetMessage。从第2列的栈帧基地址都小于0x800000000可以看出,帧#12~#0d都是在用户模式执行的。帧#0d执行我们前面分析过的SystemCallStub,而后(帧#0c)便进入了内核模式的KiSystemService。KiSystemService根据系统服务号码,将调用分发给Windows子系统内核模块win32k中的NtUserGetMessage函数。

    帧#0a~#05表示内核模式的窗口消息函数在工作。帧#07~#05表示要把一个窗口消息发送到用户态。帧#04的SfnDWORD表示在将消息组织好后调用KeUserModeCallback函数,发起逆向调用。帧#02表明在执行KiCallUserMode函数,帧#01表明已经在用户模式下执行,这两行之间的部分过程没有显示出来。同样,帧#01 和帧#00 之间执行用户模式函数的过程没有完全体现出来。XyCallbackReturn函数是用于返回内核模式的,它的代码很简单,只有如下几条指令。

    USER32!XyCallbackReturn:
    001b:77d44168 8b442404     mov   eax,dword ptr [esp+4] ss:0023:0006fe84=00000000
    001b:77d4416c cd2b          int   2Bh
    001b:77d4416e c20400        ret   4

    第1行把用户模式函数的执行结果赋给EAX寄存器,第2行执行INT 2B指令。执行过INT 2B后,CPU便转去执行异常处理程序KiCallbackReturn,回到了内核模式。

    本文摘自《软件调试(第2版)卷2:Windows平台调试(上、下册)》

    Windows操作系统管理进程和线程:内核模式和用户模式

     

    本书是国内当前集中介绍软件调试主题的权威著作。本书第2卷分为5篇,共30章,主要围绕Windows系统展开介绍。第一篇(第1~4章)介绍Windows系统简史、进程和线程、架构和系统部件,以及Windows系统的启动过程,既从空间角度讲述Windows的软件世界,也从时间角度描述Windows世界的搭建过程。第二篇(第5~8章)描述特殊的过程调用、垫片、托管世界和Linux子系统。第三篇(第9~19章)深入探讨用户态调试模型、用户态调试过程、中断和异常管理、未处理异常和JIT调试、硬错误和蓝屏、错误报告、日志、事件追踪、WHEA、内核调试引擎和验证机制。第四篇(第20~25章)从编译和编译期检查、运行时库和运行期检查、栈和函数调用、堆和堆检查、异常处理代码的编译、调试符号等方面概括编译器的调试支持。第五篇(第26~30章)首先纵览调试器的发展历史、工作模型和经典架构,然后分别讨论集成在Visual Studio和Visual Studio(VS)Code中的调试器,最后深度解析WinDBG调试器的历史、结构和用法。

    本书理论与实践结合,不仅涵盖了相关的技术背景知识,还深入研讨了大量具有代表性的技术细节,是学习软件调试技术的珍贵资料。

    展开全文
  • 从这篇文章开始,作者将带着大家来学习《Windows黑客编程技术详解》,其作者是甘迪文老师,推荐大家购买来学习。作者将采用实际编程和图文结合的方式进行分享,并且会进一步补充知识点,希望对您有所帮助。第二篇...
  • 进程和线程的区别(Windows)

    千次阅读 2013-09-07 19:31:56
    简而言之,一个程序至少有一个进程,一个进程至少有一个线程线程的划分尺度小于进程,使得多线程程序的并发性高。 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行...
  • windows线程API 及多线程

    千次阅读 2013-06-05 10:41:38
    原文:...  在Windows的多线程编程中,创建线程的函数主要有CreateThread和_beginthread(及_beginthreadex)。   CreateThread 和 ExitThread    使用API函数CreateThread创
  • 背景介绍一般项目开发完毕就要部署到服务器上,很多公司的服务器采用的是Linux系统,但也有一些公司用的是Windows...运行一段时间后,发现CMD黑窗口卡主不动,日志不再刷新,而系统内存、CPU占用等关键指标并没有异...
  • Qt运行程序弹出异常窗口解释

    千次阅读 2021-04-09 22:39:24
    源于群友的提问。关于程序运行时弹出崩溃窗口提示的问题。
  • 线程访问异常
  • 在Windows应用程序中,窗体是由一种称为“UI线程(User Interface Thread)”的特殊类型的线程创建的。 首先,UI线程是一种“线程”,所以它具有一个线程应该具有的所有特征,比如有一个线程函数和一个线程ID。...
  • 其实Java实现一个软键盘是非常简单的一件事情,但是实现一个软键盘只能在当前这个打开的窗口输入中英文的话只会让你觉得没有一点成就感,所以,我搜集了一些相关的资料,然后封装了一点代码,可以实现向任意窗口发送...
  • Windows的多线程编程 c语言 在Windows的多线程编程中,创建线程的函数主要有CreateThread和_beginthread(及_beginthreadex)。   CreateThread 和 ExitThread    使用API函数CreateThread创建线程时,其中的...
  • GTK+ 2.0 在Windows下的多线程应用

    千次阅读 2015-12-14 09:56:35
    GTK+ 2.0 在Windows下的多线程应用 目录 1. GTK+里面的线程2. GTK+多线程Windows上的应用方法3. GCC内嵌函数给GObject一个闭包的机会4. 结论 1 GTK+里面的线程 大家都知道,GTK+是用GLib来搭建的。那么如果...
  • 线程关闭其他窗口 会报异常: 解决方法: 在你要操作的窗口的类的构造函数中指明允许跨线程操作。 public partial class AlarmInfoWindow : Form { public AlarmInfoWindow() { Initialize...
  • jar包在windows使用启动方式以及假死解决方案参考...java -jar xxx.jar(jar包的路径),最普通简单的方式,通过windows的命令行窗口启动,并在命令行窗口运行,输出。可以在启动的时候增加参数指定启动的端口(se...
  • 最近写调试图开关接口,发现调用...窗口绘制,和销毁函数在一个线程内调用。运行成功。 主线程绘制->主线程销毁。 子线程a绘制->子线程a销毁。 子线程b绘制->子线程b销毁。 另外一种解决...
  • windows 线程

    千次阅读 2016-09-14 00:06:14
    windows中进程只是一个容器,用于装载系统资源,它并不执行代码,它是系统资源分配的最小单元,而在进程中执行代码的是线程线程是轻量级的进程,是代码执行的最小单位。 从系统的内核角度看,进程是一个内核...
  • C#解决多线程窗口UI假死--委托的使用: 1.使用了Thread.Sleep()模拟线程耗时运行; 2.用ParameterizedThreadStart创建了一个带参数的线程,使UI界面上输入的值能传递到线程中; 3.线程运行期间,能够拖动UI窗口;...
  • 在修改单线程MFC程序为多线程时,遇到了CWnd::AssertValid()函数执行出错问题。主要表现是在执行下面代码中绿色语句时出错 点击(此处)折叠或打开 #ifdef _DEBUG void CWnd::AssertValid() ...
  • Windows下多线程编程技术及其实现

    千次阅读 2006-08-24 09:03:00
    Windows下多线程编程技术及其实现本文首先讨论16位Windows下不具备的线程的概念,然后着重讲述在32位Windows 95环境下多线程的编程技术,最后给出利用该技术的一个实例,即基于Windows95下TCP/IP的可视电话的实现。...
  • Windows线程编程总结

    千次阅读 2012-04-12 19:59:00
    线程  线程同步  线程池  内核对象  1 内核对象1 .1内核对象的概念 内核对象是内核分配的一个内存块,这种内存块是一个数据结构,表示内核对象的各种特征。并且只能由内核来访问。应用程序若需要...
  • 就算不出现断言错误,也会导致程序异常退出,这是因为不允许直接操作其他线程中的CWnd对象。 The correct approach in such situations is to work with window handles, not MFC objects. It is safe to ...
  • 如果线程A向线程B传递一个窗口句柄,那么,线程B可以通过发送消息给拥有该句柄的窗口对象。在处理窗口消息时,系统已经切换到线程A。这是验证窗口对象的有效性会成功。』 Description of CWnd derived MFC ...
  • 一个滑动窗口统计主要分为两步: 1. bucket 统计,bucket 的大小决定了滑动窗口滚动时间间隔; 2. window 统计,window 的时长决定了包含的 bucket 的数目。 Hystrix 实现滑动窗口利用了 RxJava 这个响应式函数编程...
  • 细说UI线程Windows消息队列

    千次阅读 2011-03-12 12:17:00
    Windows应用程序中,窗体是由一种称为“UI线程(User Interface Thread)”的特殊类型的线程创建的。  首先,UI线程是一种“线程”,所以它具有一个线程应该具有的所有特征,比如有一个线程函数和一个...
  • Hystrix 1.5 滑动窗口实现原理总结

    千次阅读 2019-01-23 16:51:36
    文章目录总览BucketedCounterStream总览...本文转载自:Hystrix 1.5 滑动窗口实现原理总结 总览 Netflix Hystrix 通过类似滑动窗口的数据结构来统计调用的指标数据。Hystrix 1.5 将滑动窗口设计成了数据流(reacti...
  • 异常异常处理(windows平台)

    千次阅读 2015-10-30 23:07:08
    翻译的不好,莫怪。 原文地址: ... About Exceptions and Exception Handling About Exception ...当程序遇到一个异常或一个严重的错误时,通常意味着它不能继

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 33,746
精华内容 13,498
关键字:

windows7窗口线程异常