linux 线程用户,linux基础知识点

linux下线程属性常用操作有哪些

LinuxThread的线程机制

LinuxThreads是目前Linux平台上使用最为广泛的线程库,由Xavier Leroy(Xavier.Leroy@inria.fr)负责开发完成,并已绑定在GLIBC中发行。它所实现的就是基于核心轻量级进程的"一对一"线程模型,一个线程实体对应一个核心轻量级进程,而线程之间的管理在核外函数库中实现。

1.线程描述数据结构及实现限制

LinuxThreads定义了一个struct _pthread_descr_struct数据结构来描述线程,并使用全局数组变量 __pthread_handles来描述和引用进程所辖线程。在__pthread_handles中的前两项,LinuxThreads定义了两个全局的系统线程:__pthread_initial_thread和__pthread_manager_thread,并用 __pthread_main_thread表征__pthread_manager_thread的父线程(初始为 __pthread_initial_thread)。

struct _pthread_descr_struct是一个双环链表结构,__pthread_manager_thread所在的链表仅包括它一个元素,实际上,__pthread_manager_thread是一个特殊线程,LinuxThreads仅使用了其中的errno、p_pid、 p_priority等三个域。而__pthread_main_thread所在的链则将进程中所有用户线程串在了一起。经过一系列 pthread_create()之后形成的__pthread_handles数组将如下图所示:

图2 __pthread_handles数组结构

新创建的线程将首先在__pthread_handles数组中占据一项,然后通过数据结构中的链指针连入以__pthread_main_thread为首指针的链表中。这个链表的使用在介绍线程的创建和释放的时候将提到。

LinuxThreads遵循POSIX1003.1c标准,其中对线程库的实现进行了一些范围限制,比如进程最大线程数,线程私有数据区大小等等。在 LinuxThreads的实现中,基本遵循这些限制,但也进行了一定的改动,改动的趋势是放松或者说扩大这些限制,使编程更加方便。这些限定宏主要集中在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平台使用的文件位置不同)中,包括如下几个:

每进程的私有数据key数,POSIX定义_POSIX_THREAD_KEYS_MAX为128,LinuxThreads使用 PTHREAD_KEYS_MAX,1024;私有数据释放时允许执行的操作数,LinuxThreads与POSIX一致,定义 PTHREAD_DESTRUCTOR_ITERATIONS为4;每进程的线程数,POSIX定义为64,LinuxThreads增大到1024(PTHREAD_THREADS_MAX);线程运行栈最小空间大小,POSIX未指定,LinuxThreads使用 PTHREAD_STACK_MIN,16384(字节)。

2.管理线程

"一对一"模型的好处之一是线程的调度由核心完成了,而其他诸如线程取消、线程间的同步等工作,都是在核外线程库中完成的。在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会创建(__clone())并启动管理线程。

在一个进程空间内,管理线程与其他线程之间通过一对"管理管道(manager_pipe[2])"来通讯,该管道在创建管理线程之前创建,在成功启动了管理线程之后,管理管道的读端和写端分别赋给两个全局变量__pthread_manager_reader和 __pthread_manager_request,之后,每个用户线程都通过__pthread_manager_request向管理线程发请求,但管理线程本身并没有直接使用__pthread_manager_reader,管道的读端(manager_pipe[0])是作为__clone()的参数之一传给管理线程的,管理线程的工作主要就是监听管道读端,并对从中取出的请求作出反应。

创建管理线程的流程如下所示:

(全局变量pthread_manager_request初值为-1)

图3创建管理线程的流程

初始化结束后,在__pthread_manager_thread中记录了轻量级进程号以及核外分配和管理的线程id, 2*PTHREAD_THREADS_MAX+1这个数值不会与任何常规用户线程id冲突。管理线程作为pthread_create()的调用者线程的子线程运行,而pthread_create()所创建的那个用户线程则是由管理线程来调用clone()创建,因此实际上是管理线程的子线程。(此处子线程的概念应该当作子进程来理解。)

__pthread_manager()就是管理线程的主循环所在,在进行一系列初始化工作后,进入while(1)循环。在循环中,线程以2秒为 timeout查询(__poll())管理管道的读端。在处理请求前,检查其父线程(也就是创建manager的主线程)是否已退出,如果已退出就退出整个进程。如果有退出的子线程需要清理,则调用pthread_reap_children()清理。

然后才是读取管道中的请求,根据请求类型执行相应操作(switch-case)。具体的请求处理,源码中比较清楚,这里就不赘述了。

3.线程栈

在LinuxThreads中,管理线程的栈和用户线程的栈是分离的,管理线程在进程堆中通过malloc()分配一个THREAD_MANAGER_STACK_SIZE字节的区域作为自己的运行栈。

用户线程的栈分配办法随着体系结构的不同而不同,主要根据两个宏定义来区分,一个是NEED_SEPARATE_REGISTER_STACK,这个属性仅在IA64平台上使用;另一个是FLOATING_STACK宏,在i386等少数平台上使用,此时用户线程栈由系统决定具体位置并提供保护。与此同时,用户还可以通过线程属性结构来指定使用用户自定义的栈。因篇幅所限,这里只能分析i386平台所使用的两种栈组织方式:FLOATING_STACK方式和用户自定义方式。

在FLOATING_STACK方式下,LinuxThreads利用mmap()从内核空间中分配8MB空间(i386系统缺省的最大栈空间大小,如果有运行限制(rlimit),则按照运行限制设置),使用mprotect()设置其中第一页为非访问区。该8M空间的功能分配如下图:

图4栈结构示意

低地址被保护的页面用来监测栈溢出。

对于用户指定的栈,在按照指针对界后,设置线程栈顶,并计算出栈底,不做保护,正确性由用户自己保证。

不论哪种组织方式,线程描述结构总是位于栈顶紧邻堆栈的位置。

4.线程id和进程id

每个LinuxThreads线程都同时具有线程id和进程id,其中进程id就是内核所维护的进程号,而线程id则由LinuxThreads分配和维护。

有人能教下我有关linux里面线程的知识吗

.线程的基本介绍

(1)线程的概述

线程与进程类似,也允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程,同一程序中的所有线程共享同一份全局内存区域,线程之间没有真正意义的等级之分。同一个进程中的线程可以并发执行,如果处理器是多核的话线程也可以并行执行,如果一个线程因为等待I/O操作而阻塞,那么其他线程依然可以继续运行

(2)线程优于进程的方面

argv,environ

主线程栈

线程3的栈

线程2的栈

线程1的栈

共享函数库共享的内存

未初始化的数据段

初始化数据段

文本

.进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通讯,在进程之间交换信息

.调用fork()来创建进程代价相对较高

线程很好的解决了上述俩个问题

.线程之间能够方便,快速的共享信息,只需将数据复制到共享(全局或堆)变量中即可

.创建线程比创建线程通常要快10甚至更多,线程创建之所以快,是因为fork创建进程时所需复制多个属性,而在线程中,这些属性是共享的。

(3)创建线程

启动程序时,产生的进程只有单条线程,我们称之为主线程

#include<pthread.h>

int pthread_create(pthread_t*thread,const pthread_attr_t*attr,void*(*start)(void*),void*arg);12

新线程通过调用带有arg的函数开始执行,调用pthread_create()的线程会继续执行该调用之后的语句。

(4)终止线程

可以以如下方式终止线程的运行

.线程调用pthread_exit()

.线程start函数执行return语句并返回指定值

.调用pthread_cancel()取消线程

.任意线程调用了exit(),或者主线程执行了return语句,都会导致进程中的所有线程立即终止

pthread_exit()函数可以终止线程,且其返回值可由另一线程通过调用pthread_join()获得

#include<pthread.h>void pthread_exit(void*retval);12

调用pthread_exit()相当于在线程的start函数中执行return,不同之处在于,pthread_exit()可以在任何地方调用,参数retval指定了线程的返回值

(5)获取线程ID

#include<pthread.h>pthread_t pthread_self(void);12

线程ID在应用程序中主要有如下用途

.不同的pthreads函数利用线程ID来标识要操作目标线程。

.在具体的应用程序中,以特定线程的线程ID作为动态数据结构的标签,这颇有用处,既可用来识别某个数据结构的创建者或属主线程,又可确定随后对该数据结构执行操作的具体线程

函数pthread_equal()可检查俩个线程的ID是否相同

#include<pthread.h>int pthread_equal(pthread_t t1,pthread_t t2);//如果相同返回非0值,否则返回0123

(6)连接已终止的线程

函数pthread_join()等待由thread表识的线程终止

#include<pthread.h>int pthread_join(pthread_t thread,void**retval);//返回0调用成功,否则失败123

如果pthread_join()传入一个之前已然连接过的线程ID,将会导致无法预知的行为,当相同线程ID在参与一次连接后恰好为另一新建线程所重用,再度连接的可能就是这个新线程

若线程未分离,则就应该使用pthread_join()来连接线程,否则会产生僵尸线程

pthrea_join()函数的要点

.线程之间的关系是对等的,所以任意线程都可以调用pthread_join()来连接其他线程

.pthread_join()无法针对任意线程,只能连接单个线程

(6)线程的分离

默认情况下线程都是可连接的,但有时候,我们并不关心线程退出的状态,我们可以调用pthread_detach()并向thread参数传入指定线程的的标识符,将该线程标记为处于分离状态

#include<pthread.h>int pthread_detach(pthread_t thread);//返回0成功,否则失败123

一旦线程处于分离状态,就不能在使用pthread_join()来获取其状态,也无法使其重返可连接状态

(7)在应用程序中如何来选择进程还是线程

.线程之间共享数据很简单,进程间的数据共享需要更多的投入

.创建线程要比创建进程块很多

.多线程编程时,需要确保调用线程安全的函数

.某个线程中的bug可能会危害进程中所有线程

.每个线程都在征用宿主进程中有限的虚拟地址空间

.在多线程应用中,需要小心使用信号

.除了数据,线程还可以共享文件描述符,信号处置,当前工作目录,以及用户ID和组ID

线程的同步

(1)保护共享变量访问:互斥量

线程的主要优势在于能够通过全局变量来共享信息,不过这种共享是有代价的。必须确保多个线程修改同一变量时,不会有其他线程也正在修改此变量,为避免线程更新时共享变量时所出现的问题,必须使用互斥量来确保同时仅有一个线程可以访问某项共享资源

(2)静态分配的互斥锁

互斥锁既可以像静态变量那样分配,也可以在运行时动态分配,互斥量属于pthread_mutex_t类型的变量,在使用之前必须对其初始化。对于静态分配的互斥量而言,可如下例所示,将PTHREAD_MUTEX_INITIALIZER赋给互斥量

pthread_mutex_t= PTHREAD_MUTEX_INITIALIZER;1

1.加锁和解锁互斥量

初始化之后,互斥量处于未锁定状态。函数pthread_mutex_lock()可以锁定某一互斥量

而函数pthread_mutex_unlock()则可以将一个互斥量解锁

#include<pthread.h>int pthread_mutex_lock(pthread_mutex_t*mutex);int pthread_mutex_unlock(pthread_mutex_t*mutex);//返回0成功,其他失败1234

要锁定互斥量,在调用pthread_mutex_lock()时需要指定互斥量,如果互斥量当前处于未锁定状态,则该调用将会立即返回,如果该互斥量已被其他线程锁定,那么该调用将会阻塞,直至互斥量被解锁

函数pthread_mutex_unlock()将解锁之前已遭调用线程锁定的互斥量

2.互斥量的性能

通常情况下,线程会花费更多的时间去做其他工作,对互斥量的加锁解锁相对要少的多,因此使用互斥量对大部分程序来说性能并无显著的影响

3.互斥量的死锁

当一个线程需要同时访问多个共享资源时,没个资源由不同的互斥索管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。如下图所示

线程A

1.pthread_mutex_lock(mutex1);

2.pthread_mutex_lock(mutex2);

线程2

1.pthread_mutex_lock(mutex2);

2.pthread_mutex_lock(mutex1);

每个线程都成功的锁住一个互斥量,接着试图对以为另一线程锁定的互斥量加锁,就会一直等下去

要避免此类死锁问题,最简单的就是定义互斥量的层级关系

Linux用户线程和内核线程区别

Linux用户线程与内核线程的主要区别在于它们的实现方式、调度机制和性能开销。

用户级线程

用户级线程的实现完全在用户空间进行,内核并不直接感知。这种模型的优点包括:调度由应用程序自行管理,开销较小,线程切换速度快。然而,缺点是开发者需要负责调度,并且资源竞争仅限于进程内。

内核级线程

内核级线程由内核直接管理,操作系统内核能感知每个线程,可以全系统范围内调度资源。优点在于线程创建和调度由内核处理,性能稳定,但开销较大,线程切换需进入内核,可能导致性能损失。

用户线程在不支持内核线程的系统中也能工作,创建成本低且调度灵活,但资源竞争受限于进程。内核线程则提供了更直接的并发控制,但创建和调度成本高。

操作系统通常采用不同的线程模型来平衡性能和资源使用,如一对一或多对多模型,各有优缺点。例如,多对一模型适合用户空间效率,一对一模型提供更好的并发性,而多对多模型则在一定程度上兼顾两者。

阅读剩余
THE END