Linux内核中的线程及多线程
时间:2019-05-28 15:31 来源:linux.it.net.cn 作者:IT
一、线程的概念、理解及特点
1.线程的概念:
至今为止,Linux下还是没有“真正的线程”。谈到线程就不得不提到进程这概念,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段,代码段和堆栈段,这就造成了进程在进行切换等操作时都需要有比较负责的上下文切换等动作.为了进一步减少处理机的空转时间支持多个处理器和减少上下文切换开销,进程在演化中出现了另一个概念--线程。它是一个进程内的基本调度单位,也可以称为轻量级进程。线程是在供享内存空间中并发的多道执行路径,它们共享一个进程的资源,如文件描述和信号处理。因此,大大减少了上下文切换的开销。
2.线程的理解:
(1).线程有的只是一组共享内存/PID/执行环境的进程,这被称为1:1的线程模型,因为每个“线程”都要有一个核心调度单元与之对应(因为在OS核心中,进程这个概念并不是很清晰,所以权且称之为调度单元吧)。这样做最大的好处(也是linux本人坚持这样做的原因)就是简单,但实际上“线程”基本变成了“进程”,要知道进程上下文切换的代价比较高(这也是多线程模型被引入的原因之一),所以在新城数量很多时,系统的线程表(实际上是进程表)会很大,切换的效率会急剧下降,造成执行效率较低。
(2).另外还有两种线程模型:
a.一种是m:1模型,也就是多个用户线程对应一个核心调度单元,换言之就是用户完全自己做调度,核心不为此提供任何支持,在早期Unix系统上,因为系统本身不支持多线程,多已出现了很多这样的库,这样做系统开销比较小,因为用户线程的切换完全不涉及进程上下文的切换,但是这样做最大的问题就是“线程”切换问题很多。
b.另一种是m:n模型,这种模型比较复杂,简单说就是内核准备n个调度单元,由这些调度单元轮换执行m个用户“线程”,这样做最大的好处就是调度由内核处理,而且由于切换的是“线程”,而不是“进程”,所以切换上下文时的开销较小,另外n的个数可以由系统决定,以充分利用硬件的多处理能力。
3.线程的特点:
(1).用户级线程:主要解决的是上下文切换的问题,它的调度算法和调度过程会全部由用户自行选择解决,在运行时不需要特定的内核支持。操作系统往往会提供一个用户空间的线程库,该用户程序库提供了线程的创建、调度和撤销等功能,而内核仍然对进程进行管理。如果一个进程中的某一个线程调用了一个阻塞的系统调用函数,那么该进程包括该进程中的线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势。
(2).轻量级的进程:轻量进程是内核支持的用户线程,是内核线程的一种抽象对象。每个线程拥有一个或多个轻量级线程,而每个轻量级线程分别被绑定在一个内核线上。
(3).内核线程:这个线程不允许不同进程中的线程按照同一相对优先调度方法进行调度,这样就可以发挥多处理器并发优势。
注意:a.进程是系统中程序执行和资源分配的基本单位。
b.每个进程都拥有自己的数据段、代码段和堆栈段。
二、进程与线程的区别
1.定义方面:进程是程序在某个数据集合上的一次运行活动;线程是进程中的一个执行路径。
2.角色方面:在支持线程机制的系统中,进程是系统资源分配的单位,线程是系统调度的单位。
3.资源共享方面:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈和栈指针,程序计数器等寄存器。
4.独立性方面:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存。
三、线程的创建、等待及终止
进程在各自在自己的地址空间中运行,进程间通信要通过进程间通信机制实现,但是一个进程的地址空间中可以执行多个线程,这些线程除了共享数据段还共享文件描述符表,用户id组id,和当前工作目录,errno变量。但同一进程中的线程还有其所独有的:线程id、上下文(寄存器、程序计数器、栈指针)、调度优先级、等等。
1.线程的创建函数:
[cpp] view plain copy
int pthread_create(pthread_t*thread,const pthread_att_t*attr,void*(*start_routine)(void*),void*arg);
若成功返回0,错误返回错误号,当一个线程调用此函数继续往下执行,新的线程所执行的代码由函数指针start_routine决定,start_routine接受一个通过pthread_creat函数传进来的void*类型参数arg,start_routine返回时此线程结束。别的线程调用pthread_join得到start_routine的返回值。
2.线程的等待:
[cpp] view plain copy
int pthread_join(pthread_t thread,void **retval)
调用该函数的线程将挂起等待,直到id为thread的线程终止。
3.线程的终止:
(1).从线程函数返回(main函数除外)
(2).一个线程调用pthread_cancel终止同一进程中的另一线程
(3).线程调用pthread_exit(void *retval)终止自己
查看线程不同终止方式返回,value_ptr所指向的内存的值:
运行结果:
四、线程的分离、同步与互斥
1. 线程的分离:
线程终止后,线程状态一直保留到其他线程调用pthread_join获取状态为止,但线程也可以被置detach状态,这样一旦终止就立刻回收它所占有的资源,而不保留终止状态。
2.分离函数:
int pthread_detach(pthread_t thread);
可结合的线程能被其它线程收回其资源和杀死,被其他线程回收之前,存储器资源不释放,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在终止时由系统自动释放。
调用pthread_join后,如果线程没有运行结束,调用者会被阻塞,但有时当主线程创建多个子线程进行处理,并不像希望调用pthread_join阻塞,这时就可以置线程为分离状态,这样一来线程运行结束后会自动释放资源。
3.线程的同步:
多线程的程序,访问冲突很普遍,可以引入互斥锁(Mutex),获得锁的线程可以进行读写修改操作,然后释放锁给其它线程。
A:实现互斥锁的操作:
lock:
movb $0,%al
xchgb %al,mutex
if(al寄存器的内容>0){
return 0;
}else
挂起等待;
goto lock;
unlock:
movb $1 ,mutex
唤醒等待Mutex的线程;
return 0;
其中“挂起等待”和“唤醒等待线程”的实现:每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先要将自己加入到等待队列中,然后置线程为睡眠状态,然后调用调度器函数切换到别的线程,一个线程要唤醒等待队列中的其他线程,只需从等待队列中取出一项,将睡眠状态改为就绪,加入就绪队列。
引起死锁的两种典型情况:
如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。也可以使用pthread_mutex_trylock调用替代pthread_mutex_lock
B:条件变量
线程间的同步还有这样这种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条
件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执
行
C:Semaphore(信号量)
信号量和Mutex类似,表示可用资源的数量,信号量的数量大于1。
int sem_init(sem_t *sem,int pshared,unsigned int value);value:可用资源数量;pshared=0,表示同一进程的线程同步
int sem_wait(sem_t *sem);semaphore减1,如果调用时已经为0,则挂起等待
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);释放资源,semaphore加1
int sem_destory(sem_t *sem);
D:读写锁
一个读写锁只能有一个写者或多个读者,但不能即有读者又有写者。
五,、线程的分离与结合属性
线程属性标识符:
pthread_attr_t 包含在 pthread.h 头文件中。
[cpp] view plain copy
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
structsched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。 为了避免存储器泄漏,每个可结合线程都应该要 么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。 由于调用pthread_join后,如果该线程没有运行结束,调用者会被阻塞。因此,我们就可以在子线程中加入pthread_detach(pthread_self())或者父线程调用pthread_detach(thread_id)(非阻塞,可立即返回) 。 这样将子线程的状态设置为分离的(detached),如此一来,该线程运行结束后会自动释放所有资源。
(责任编辑:IT)
一、线程的概念、理解及特点
1.线程的概念:
至今为止,Linux下还是没有“真正的线程”。谈到线程就不得不提到进程这概念,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段,代码段和堆栈段,这就造成了进程在进行切换等操作时都需要有比较负责的上下文切换等动作.为了进一步减少处理机的空转时间支持多个处理器和减少上下文切换开销,进程在演化中出现了另一个概念--线程。它是一个进程内的基本调度单位,也可以称为轻量级进程。线程是在供享内存空间中并发的多道执行路径,它们共享一个进程的资源,如文件描述和信号处理。因此,大大减少了上下文切换的开销。
2.线程的理解:
(1).线程有的只是一组共享内存/PID/执行环境的进程,这被称为1:1的线程模型,因为每个“线程”都要有一个核心调度单元与之对应(因为在OS核心中,进程这个概念并不是很清晰,所以权且称之为调度单元吧)。这样做最大的好处(也是linux本人坚持这样做的原因)就是简单,但实际上“线程”基本变成了“进程”,要知道进程上下文切换的代价比较高(这也是多线程模型被引入的原因之一),所以在新城数量很多时,系统的线程表(实际上是进程表)会很大,切换的效率会急剧下降,造成执行效率较低。
(2).另外还有两种线程模型:
a.一种是m:1模型,也就是多个用户线程对应一个核心调度单元,换言之就是用户完全自己做调度,核心不为此提供任何支持,在早期Unix系统上,因为系统本身不支持多线程,多已出现了很多这样的库,这样做系统开销比较小,因为用户线程的切换完全不涉及进程上下文的切换,但是这样做最大的问题就是“线程”切换问题很多。
b.另一种是m:n模型,这种模型比较复杂,简单说就是内核准备n个调度单元,由这些调度单元轮换执行m个用户“线程”,这样做最大的好处就是调度由内核处理,而且由于切换的是“线程”,而不是“进程”,所以切换上下文时的开销较小,另外n的个数可以由系统决定,以充分利用硬件的多处理能力。
3.线程的特点:
(1).用户级线程:主要解决的是上下文切换的问题,它的调度算法和调度过程会全部由用户自行选择解决,在运行时不需要特定的内核支持。操作系统往往会提供一个用户空间的线程库,该用户程序库提供了线程的创建、调度和撤销等功能,而内核仍然对进程进行管理。如果一个进程中的某一个线程调用了一个阻塞的系统调用函数,那么该进程包括该进程中的线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势。
(2).轻量级的进程:轻量进程是内核支持的用户线程,是内核线程的一种抽象对象。每个线程拥有一个或多个轻量级线程,而每个轻量级线程分别被绑定在一个内核线上。
(3).内核线程:这个线程不允许不同进程中的线程按照同一相对优先调度方法进行调度,这样就可以发挥多处理器并发优势。
注意:a.进程是系统中程序执行和资源分配的基本单位。
b.每个进程都拥有自己的数据段、代码段和堆栈段。
二、进程与线程的区别
1.定义方面:进程是程序在某个数据集合上的一次运行活动;线程是进程中的一个执行路径。
2.角色方面:在支持线程机制的系统中,进程是系统资源分配的单位,线程是系统调度的单位。
3.资源共享方面:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈和栈指针,程序计数器等寄存器。
4.独立性方面:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存。
三、线程的创建、等待及终止
进程在各自在自己的地址空间中运行,进程间通信要通过进程间通信机制实现,但是一个进程的地址空间中可以执行多个线程,这些线程除了共享数据段还共享文件描述符表,用户id组id,和当前工作目录,errno变量。但同一进程中的线程还有其所独有的:线程id、上下文(寄存器、程序计数器、栈指针)、调度优先级、等等。
1.线程的创建函数:
[cpp] view plain copy
int pthread_create(pthread_t*thread,const pthread_att_t*attr,void*(*start_routine)(void*),void*arg);
若成功返回0,错误返回错误号,当一个线程调用此函数继续往下执行,新的线程所执行的代码由函数指针start_routine决定,start_routine接受一个通过pthread_creat函数传进来的void*类型参数arg,start_routine返回时此线程结束。别的线程调用pthread_join得到start_routine的返回值。
2.线程的等待:
[cpp] view plain copy
int pthread_join(pthread_t thread,void **retval)
调用该函数的线程将挂起等待,直到id为thread的线程终止。
3.线程的终止:
(1).从线程函数返回(main函数除外)
(2).一个线程调用pthread_cancel终止同一进程中的另一线程
(3).线程调用pthread_exit(void *retval)终止自己
查看线程不同终止方式返回,value_ptr所指向的内存的值:
运行结果:
四、线程的分离、同步与互斥
1. 线程的分离:
线程终止后,线程状态一直保留到其他线程调用pthread_join获取状态为止,但线程也可以被置detach状态,这样一旦终止就立刻回收它所占有的资源,而不保留终止状态。
2.分离函数:
int pthread_detach(pthread_t thread);
可结合的线程能被其它线程收回其资源和杀死,被其他线程回收之前,存储器资源不释放,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在终止时由系统自动释放。
调用pthread_join后,如果线程没有运行结束,调用者会被阻塞,但有时当主线程创建多个子线程进行处理,并不像希望调用pthread_join阻塞,这时就可以置线程为分离状态,这样一来线程运行结束后会自动释放资源。
3.线程的同步:
多线程的程序,访问冲突很普遍,可以引入互斥锁(Mutex),获得锁的线程可以进行读写修改操作,然后释放锁给其它线程。
A:实现互斥锁的操作:
lock:
movb $0,%al
xchgb %al,mutex
if(al寄存器的内容>0){
return 0;
}else
挂起等待;
goto lock;
unlock:
movb $1 ,mutex
唤醒等待Mutex的线程;
return 0;
其中“挂起等待”和“唤醒等待线程”的实现:每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先要将自己加入到等待队列中,然后置线程为睡眠状态,然后调用调度器函数切换到别的线程,一个线程要唤醒等待队列中的其他线程,只需从等待队列中取出一项,将睡眠状态改为就绪,加入就绪队列。
引起死锁的两种典型情况:
如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。也可以使用pthread_mutex_trylock调用替代pthread_mutex_lock
B:条件变量
线程间的同步还有这样这种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条
件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执
行
C:Semaphore(信号量)
信号量和Mutex类似,表示可用资源的数量,信号量的数量大于1。
int sem_init(sem_t *sem,int pshared,unsigned int value);value:可用资源数量;pshared=0,表示同一进程的线程同步
int sem_wait(sem_t *sem);semaphore减1,如果调用时已经为0,则挂起等待
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);释放资源,semaphore加1
int sem_destory(sem_t *sem);
D:读写锁
一个读写锁只能有一个写者或多个读者,但不能即有读者又有写者。
五,、线程的分离与结合属性
线程属性标识符:
pthread_attr_t 包含在 pthread.h 头文件中。
[cpp] view plain copy
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
structsched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。 为了避免存储器泄漏,每个可结合线程都应该要 么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。 由于调用pthread_join后,如果该线程没有运行结束,调用者会被阻塞。因此,我们就可以在子线程中加入pthread_detach(pthread_self())或者父线程调用pthread_detach(thread_id)(非阻塞,可立即返回) 。 这样将子线程的状态设置为分离的(detached),如此一来,该线程运行结束后会自动释放所有资源。
(责任编辑:IT) |