内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行(实际上,也并行于内核自身的执行)。内核线程经常称之为
守护进程。它们用于执行下列任务。

转自:

  • 程序和进程
    程序是计算机指令的集合,它以文件的形式存储在磁盘上。
    进程通常被定义为一个正在运行的程序的实例。是一个程序在其自身的地址空间中的一次执行活动。
    一个程序可以对应多个进程。

  • 进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源。
    程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位。因此它不占用系统的运行资源。
    真正完成代码执行的是线程,而进程只是线程的容器,或者说是线程的执行环境。

  • 主线程,也就是执行main函数或者WinMain函数的线程,可以把main函数或者WinMain函数看做是主线程的进入点函数。此后,主线程可以创建其他线程。
    系统赋予每个进程独立的虚拟地址空间。32位进程,4GB。

  • 周期性地将修改的内存页与页来源块设备同步(例如,使用mmap的文件映射)。
  • 如果内存页很少使用,则写入交换区。
  • 管理延时动作(deferred action)。
  • 实现文件系统的事务日志。
    基本上,有两种类型的内核线程。
  • 类型1:线程启动后一直等待,直至内核请求线程执行某一特定操作。
  • 类型2:线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制值时采取行动。内核使用这类线程用于连续监测任务
日期 内核版本 架构 作者 GitHub CSDN
2016-06-02 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度-之-进程的描述

磁盘缓存、虚拟内存、页面文件和物理内存的关系
磁盘缓存分为读缓存和写缓存。读缓存是指,操作系统为已读取的文件数据,在内存较空闲的情况下留在内存空间中(这个内存空间被称之为“内存池”),当下次软件或用户再次读取同一文件时就不必重新从磁盘上读取,从而提高速度。写缓存实际上就是将要写入磁盘的数据先保存于系统为写缓存分配的内存空间中,当保存到内存池中的数据达到一个程度时,便将数据保存到硬盘中。这样可以减少实际的磁盘操作,有效的保护磁盘免于重复的读写操作而导致的损坏,也能减少写入所需的时间。
虚拟内存是用硬盘空间做内存来弥补计算机RAM空间的缺乏。当实际RAM满时(实际上,在RAM满之前),虚拟内存就在硬盘上创建了。当物理内存用完后,虚拟内存管理器选择最近没有用过的,低优先级的内存部分写到交换文件上。这个过程对应用是隐藏的,应用把虚拟内存和实际内存看作是一样的。虚拟内存文件也就是页面文件。

内核线程与普通进程之间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的MM指针被设置为NULL)。

内核线程


线程有两个部分组成

为什么需要内核线程


Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。

内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。

内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。

这与用户线程是不一样的。因为内核线程只运行在内核态

因此,它只能使用大于PAGE_OFFSET(传统的x86_32上是3G)的地址空间。

  1. 线程的内核对象。操作系统用它来对线程实施管理。关于线程的统计信息组成的一个小型数据结构。
  2. 线程栈。它用于维护线程在执行代码时需要的所有函数参数和局部变量。
    新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。

内核线程概述


内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程

他们执行下列任务

  • 周期性地将修改的内存页与页来源块设备同步

  • 如果内存页很少使用,则写入交换区

  • 管理延时动作, 如2号进程接手内核进程的创建

  • 实现文件系统的事务日志

内核线程主要有两种类型

  1. 线程启动后一直等待,直至内核请求线程执行某一特定操作。

  2. 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。

内核线程由内核自身生成,其特点在于

  1. 它们在CPU的管态执行,而不是用户态。

  2. 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间

进程、线程及内核对象

内核对象
每个内核对象只是内核分配的一个内存块,并且只能由该内核访问,这个内存块是一种数据结构,他的成员负责维护该对象的各种信息,如进程对象有一个进程ID、一个基本优先级和一个退出代码。

由于内核对象的数据结构只能被内核访问,so应用程序是无法在内存中找到这些数据结构的并直接改变其内容的。Windows提出这个限制为了确保内核对象结构保持状态的一致,也是为了保证Microsoft能够在不破坏应用程序的情况下在这些内核对象的结构中添加、删除、修改这些数据成员;

内核对象使用引用计数
内核对象由内核所有,而不是进程所有,举例说明,在做单进程限制时,我们一般会CreateMutex来创建一个命名的Mutex,再另外一个进程中再来创建或者打开相同命名的Mutex来检验有相同进程被创建。也可以这么说进程调用一个创建的内核对象函数,进程终止了但是内核对象不一定被撤销;

每个线程都有自己的一组cpu寄存器和堆栈,每个进程至少有一个线程,来执行进程的地址空间中的代码。如果没有线程来执行进程地址空间中的代码,那么这个进程就没有存在的理由,系统会自动撤销该进程;

进程的实例句柄
加载到进程地址空间的每个可执行文件或者dll均被赋予了一个独一无二的实例句柄。可执行的文件的实例作为(w)WinMain的第一个参数hinstExe来传递;

  • WinMain的hinstExe参数的实际值是系统将可执行文件的映像加载到进程的地址空间时使用的基本地址空间。例如,如果系统打开了可执行文件并将它的内容加载到地址的0x00400000,那么WinMain的hinstExe的参数值就是0x00400000;

  • 可执行文件的映像加载到的基地址是由连接程序决定的,不同的链接程序可以使用不同的默认基地址。VC++链接程序使用的默认基地址是0x00400000;只是运行在windows98时可执行文件的映像可以加载到的最低地址。

  • 进程的前一个实例句柄
    C/C++运行期启动代码总是将NULL传递给WinMain的hinstExePrev参数,该参数使用在16位windows中的,并且保留了WinMain的一个参数,目的仅仅是为了能够容易的转用16位windows应用程序,so绝不应该在代码中引用这个参数。

  • 线程有两部分组成:
    一个是线程的内核对象,操作系统用他来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方;
    一个是线程堆栈,用于维护线程在执行代码时需要的所有函数参数和局部变量;

进程是不活泼的,进程从不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。那么这意味着线程在他的进程地址空间中执行代码,并在进程的地址空间中对数据进行操作。so在单进程中,如果存在多个线程的运行,那么这些线程将共享单个地址空间。这些线程能够执行相同的代码对相同数据进行操作。而且他们还能共享内核对象句柄,因为句柄表依赖于每个进程而非每个线程存在的。

进程使用的系统资源要比线程对很多,原因是他需要更多的地址空间。为进程创建一个虚拟的地址空间需要系统很多的资源。系统中要保留大量的记录,这需要占用大量的内存。另外,由于exe和dll文件需要加载到一个地址空间,因此也需要文件资源,而线程使用的系统资源就小很多了,他只需要一个内核对象和一个堆栈就ok,保留的记录也很少,so只需要很少的内存。

So能用线程解决的问题,要避免创建新进程来解决。

注:CreateThread函数是用来创建线程的windows函数,如果你正在编写C/C++代码,绝不应该调用CreateThread,相反,应该使用VC++运行库函数_beginthreadex。原因是,标准C/C++运行库在最初设计的时候并没有考虑到多线程的问题。若要使多线程C/C++程序能够正常的运行起来,必须创建一个数据结构,并将它与使用的C/C++运行库函数的每一个线程关联起来,当你调用C/C++运行库时,这些函数必须知道查看调用线程的数据块,这样就不会对别的线程产生不良影响。那么系统是否知道在创建新线程时分配该数据块呢?no,系统是无法知道你的应用程序使用C/C++编写的,自然不知道你调用的线程本身不安全,所以不要调用操作系统的CreateThread函数,而需要调用_beginthreadex.

线程的运行时间
有时要计算线程执行某个任务需要多长时间,很多时候我们会采用如下代码:

//start time

DWORD starttime = GetTickCount();

//complex algorithm here;

//subtract start time from current time to get duration;

DWORD dwElapsedTime = GetTickCount() – dwStartTime;

这个代码有个简单的假设:运行不会被中断;但是在抢占式的windows操作系统是不可能存在的,so我们需要用另外一个方式来计算,该函数为GetThreadTime;

内存映射文件

Windows提供了3种进行内存管理的方法:

  • 虚拟内存,最适合用来管理大型对象或结构数组。

  • 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。

  • 内存堆栈,最适合用来管理大量的小对象。

这里将先讲述一下关于内存映射文件的使用情况。

与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,并将物理存储器提交给该区域,他们之间的差别是物理存储器来自一个已经位于磁盘上的文件,而不是系统的页文件。一旦该文件被映射,就可以访问它,就像是整个文件已经加载到内存中一样。

内存映射文件的3个目的:

  • 系统使用内存映射文件,以便快速加载和执行exe和dll文件。

  • 可以使用内存映射文件快速访问磁盘上的数据文件,这样使得你不必对文件执行I/O操作,并且不必对文件内容进行缓存。

  • 可以使用内存映射文件来进行进程间的数据共享。

内存映射文件的步骤:

若要使用内存映射文件,必须执行下列操作步骤:

  1. 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。(CreateFile)

  2. 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。(CreateFileMapping)

  3. 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。(MapViewOfFile)

当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:

  1. 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。(UnmapViewOfFile)

  2. 关闭文件映射内核对象和文件内核对象。(CloseHandle)

CreateThread,该函数将创建一个线程。

很多程序在创建线程都这样写的:
ThreadHandle = CreateThread(NULL,0,…..);
CloseHandle(ThreadHandle );
1,线程和线程句柄(Handle)不是一个东西,线程句柄是一个内核对象。我们可以通过句柄来操作线程,但是线程的生命周期和线程句柄的生命周期不一样的。线程的生命周期就是线程函数从开始执行到return,线程句柄的生命周期是从CreateThread返回到你CloseHandle()。
2,线程句柄是一种内核对象,系统维护着每一个内核对象,当每个内核对象引用记数为0时,系统就从内存中释放该对象,CloseHandle就是将该线程对象的引用记数减1。所有的内核对象(包括线程Handle)都是系统资源,用了要还的,也就是说用完后一定要closehandle关闭之,如果不这么做,你系统的句柄资源很快就用光了。
只是关闭了一个线程句柄对象,表示我不再使用该句柄,即不对这个句柄对应的线程做任何干预了。并没有结束线程.

  • 如果线程需要访问共享资源,就需要进行线程之间的同步处理。

  • 利用互斥对象实现线程同步
    互斥对象mutex属于内核对象。它能确保线程拥有对单个资源的互斥访问权。
    互斥对象包含一个使用数量,一个线程ID和一个计数器。
    ID用于标识系统中的哪个对象拥有互斥对象。
    计数器用于指明该线程拥有互斥对象的次数。

HANDLE hMutex; 互斥对象的句柄
hMutex=CreateMutex(NULL,FALSE,NULL);

WaitForSingleObject(hMutex,INFINITE);
ReleaseMutex(hMutex);
对互斥对象来说,谁拥有谁释放。

如果多次在同一线程中请求同一个互斥对象,那么就需要相应的多次调用
ReleaseMutex函数释放该互斥对象。

注:主线程拥有该互斥对象时,该对象就处于未通知状态了。主线程通过WaitForSingleObject函数再次请求该互斥对象的所有权时,因为ID相同,所以仍然能够请求到这个互斥对象。操作系统通过互斥对象内部的计数器来维护同一个线程请求到该互斥对象的次数。

  • 保证程序只有一个实例运行
    对这种同时只能有应用程序的一个实例运行的功能,可以通过命名的互斥对象来实现。
    CreateMutex
    GetLastError 返回值 ERROR_ALREADY_EXISTS 或其他

  • LPVOID,是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候再转换回来。

  • 不能使用全局函数和全局变量,我们就可以采用静态成员函数和静态成员变量的方法来解决上述问题。

  • PVOID是void*的别名。

  • 在windef.h中,LPVOID是这么定义的:typedef void far *LPVOID。

  • 和void*的区别是远指针,因为win32编程中,经常要调用外部DLL堆变量。但现在的大部分平台已经无所谓了,因为寻址方式成flat了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注