欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > 进程的延伸——线程(中)

进程的延伸——线程(中)

2025/2/22 2:22:16 来源:https://blog.csdn.net/ywczuishuai/article/details/145730898  浏览:    关键词:进程的延伸——线程(中)

进程的延伸——线程(上)

POSIX线程

为实现可移植的线程程序,IEEE在IEEE标准1003.1c中定义了线程的标准。它定义的线程包叫作
pthread。大部分UNIX系统都支持该标准。这个标准定义了超过60个函数调用。

出自《现代操作系统》第4版

所有pthread线程都含有一个标识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性。这些属性包括堆栈大小、调度参数以及其他线程需要的项目。 

前面四个线程调用我在上一篇已经提过了。我再提一下最后两个线程调用。

这两个线程调用是处理属性的。pthread_attr_init建立关联一个线程的属性结构并初始化成默认值。这些值(例如优先级)可以通过修改属性结构中的域值来改变。pthread_attr_destroy删除一个线程的属性结构,释放它占用的内存。它不会影响调用它的线程。这些线程会继续存在。

三种实现线程包的主要方式

1)在用户空间中实现线程

知识点拨:这种方法是把整个线程包放在用户空间中,内核对线程包一无所知。从内核的角度考虑,就是按正常的方式管理,即单线程进程。

优点:

1)用户级线程包可以在不支持线程的操作系统上实现。过去所有的操作系统都属于这个范围,即使现在也有一些操作系统还是不支持线程。

1.用函数库实现线程

后续自己手搓操作系统时可以尝试实现一下。

2.线程在一个运行时系统的上层运行,该运行时系统是一个管理线程的过程的集合。就是前面介绍的那些调用过程:pthread_create、pthread_exit、pthread_join...。一般会有更多的过程。

3.在用户空间中管理线程时,每个进程需要有其专用的线程表,用来跟踪该进程中的线程。它仅记录各个线程的属性,包括每个线程的程序计数器、堆栈指针、寄存器和状态等。该线程表由运行时系统管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程所需的信息,与内核在进程表中存放进程的信息完全一样。

4.当某个线程做了一些会引起在本地阻塞的事情之后,例如等待进程中另一个线程完成某项工作,
它调用一个运行时系统的过程(就像进程层面的调度程序一样),这个过程检查该线程是否必须进入阻塞状态。如果是,它在线程表中保存该线程的寄存器(即它本身的),查看表中可运行的就绪线程,并把新线程的保存值重新装入机器的寄存器中。只要堆栈指针和程序计数器一被切换,新的线程就就又自动投入运行。

优点:

2)进行类似于这样的线程切换至少比陷入内核要快一个数量级(或许更多),这是使用用户级线程包的极大的优点。

3)允许每个进程有自己定制的调度算法。

4)用户级线程还具有较好的可扩展性。因为内核级线程(在内核中实现线程)需要一些固定表格空间和堆栈空间,如果内核线程的数量非常大,就会出现问题。

问题:

1)如何实现阻塞系统调用。

让某个线程实际进行阻塞系统调用会停止该进程中的所有线程,我们的目标是:允许每个线程使用阻塞调用,但是还要避免被阻塞的线程影响其他的线程。

思路一:系统调用全部改成非阻塞的,但这需要修改操作系统,不太现实。

思路二:如果某个调用会阻塞,就提前通知。

在某些UNIX版本中,有一个系统调用select可以允许调用者通知预期的read是否会阻塞。若有这个调用,那么库过程read就可以被新的操作替代,首先进行select调用,然后只有在安全的情形下(即不会阻塞)才进行read调用。如果read调用会被阻塞,有关的调用就不进行,代之以运行另一个线程。到了下次有关的运行系统取得控制权之后,就可以再次检查看看现在进行read调用是否安全。这个处理方法需要重写部分系统调用库,所以效率不高也不优雅,不过没有其他的可选方案了。

在系统调用周围从事检查的这类代码称为包装器(jacket或wrapper)。

与阻塞系统调用问题类似的是缺页中断问题,这里不做赘述。

2)如果一个线程开始运行,那么在该进程中的其他线程就不能运行。

在一个单独的进程内部,没有时钟中断(这是进程调度层面的),所以不可能用轮转调度的方式调度线程。

思路一:让运行时系统请求每秒一次的时钟信号(中断),但是这样对程序也是生硬和无序的。不可能总是高频率地发生周期性的时钟中断,即使可能,总的开销也很大。而且,线程可能也需要时钟中断,这就会扰乱运行时系统使用的时钟。

程序员通常在经常发生线程阻塞的应用中才希望使用多个线程。比如多线程Web服务器。而那些基本上是CPU密集型且极少有阻塞的应用程序并不会使用多线程。这是针对用户级线程的最大负面争论意见。

2)在内核中实现线程(需要操作系统支持线程的概念)

1.在内核中支持和管理线程不需要运行时系统。每个进程中没有线程表,在内核中有专门记录系统中所有线程的线程表。

2.当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作。

3.内核中线程表保存的信息和用户级是一样的。这些信息是传统内核所维护的每个单线程进程信息(即进程状态)的子集。另外,内核还维护了传统的进程表,以便跟踪进程的状态。

4.当一个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程(若有一个就绪线程)或者
运行另一个进程中的线程。而在用户级线程中,运行时系统始终运行自己进程中的线程,直到内核剥夺它的CPU(或者没有可运行的线程存在了)为止。

5.在内核中创建或撤销线程的代价比较大,某些系统采取回收线程的方式:当某个线程被撤销时,就把它标志为不可运行的,但是其内核数据结构没有受到影响。稍后,在必须创建一个新线程时,就重新启动某个旧线程,从而节省了一些开销。在用户级线程中线程回收也是可能的,但是由于其线程管理的代价很小,所以没有必要进行这项工作。

内核线程能够解决的问题:

内核线程不需要任何新的、非阻塞系统调用。如果其某个进程中的线程引起了页面故障,内核
可以很方便地检查该进程是否有任何其他可运行的线程,如果有,在等待所需要的页面从磁盘读入时,
就选择一个可运行的线程运行。

这样做的主要缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止等)比较多,就会带来很大的开销。

依旧不能解决的问题(举例):

1)当一个多线程进程创建新的进程时,会发生什么?

新进程是拥有与原进程相同数量的线程,还是只有一个线程?在很多情况下,最好的选择取决于进程计划下一步做什么。如果它要调用exec来启动一个新的程序,或许一个线程是正确的选择;但是如果它继续执行,则最好复制所有的线程。

2)另一个话题是信号。信号是发给进程而不是线程的,至少在经典模型中是这样的。当一个信号到达时,应该由哪一个线程处理它?线程可以"注册"它们感兴趣的某些信号,因此当一个信号到达的时候,可把它交给需要它的线程。但是如果两个或更更多的线程注册了相同的信号,会发生什么?

3)混合实现

将上面两种线程实现的优点结合起来的一种方法是:使用内核级线程,然后将用户级线程与某些或者全部内核线程多路复用起来。

采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。

如同在没有多线程能力操作系统中某个进程中的用户级线程一样,可以创建、撤销和调度这些用户级线程。在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词