linux与线程

1,961 阅读8分钟

1.1.1 进程

​ 在早期,人们都是为特定机器编写程序,并在其上运行计算任务(task)。渐渐的人们发现CPU与IO设备之间速度差太多了,往往CPU都在空转,是不是可以在CPU空闲的时候做些其他事呢?于是,就有了多任务(每个任务就是一个进程),有了资源调度,有了操作系统...

​ 进程是资源管理的最小单位,操作系统在分配资源(内存,文件等)时是以进程为单位划分的。在单CPU(单核)的时代,通过对多个进程的调度(分配CPU时间片),我们已经可以边听音乐,边打游戏了!

在以前进程被描述为资源分配和执行调度的最小单位,但现在都不这么说了,因为引入了线程的概念。不管怎样,这些都只是一个名称而已,本质上还是要看资源是怎么分配和管理的。

​ 本文不会讨论进程的调度算法,如FCFS,SJF,时间片轮转等。在这里,具体看一下Linux进程的数据结构,以及状态转换:

  • 进程的数据结构

linux进程是一个双端链表结构,其主要内容包括代码段,数据段,堆,栈,内存映射表等

  • 进程的状态转换

进程状态主要在就绪,执行,等待,每一次切换伴随着一次上下文切换。

  • 上下文切换

    进程的上下文包括进程在执行时,CPU所有寄存器中的值、进程的状态以及堆栈中的内容。所谓进程切换即一个进程获得或者丢失CPU时间片,这个过程由内核负责保存进程的状态快照(上下文),由此发生了上下文切换。可以看到这个过程本身就需要消耗很多CPU时间片。

总结: 虽然通过多进程调度,可以并发的处理任务,但可以看到进程的切换很频繁。一个进程刚得到CPU资源就又可能因为发生了IO阻塞而转入等待状态。一个进程在得到CPU时间片后如何充分利用它呢?于是又引入了线程的概念。

1.1.2 线程

​ 为了最大效率的利用CPU,防止IO操作阻塞整个进程运行,降低进程上下文切换的开销,于是又引入了线程的概念,将线程作为CPU调度执行的基本单位。如果一个进程包含多个线程,则这多个线程可以并发或并行执行,并且线程不会导致进程的阻塞(理想情况或者理论层面来讲)。

​ 为什么线程可以降低开销呢?对照上面进程内存结构图,进程的所有线程共享进程的数据结构,除了线程私有的像程序计数器,栈空间,寄存器之外。一个进程的线程之间切换不会发生系统调用,还有采用多线程可以更好的利用多处理器并行计算,线程直接通信更方便等等。

​ 尽管线程有很多优点,但这都只是概念性的。并不是所有的操作系统都支持线程。windows原生支持了线程的实现,但linux中并没有线程的概念,所以只能通过在内核外实现多线程,根据线程的支持是在内核还是内核外,把线程划分为内核线程和用户级线程。

1.1.3 Posix线程标准

​ 在讲Linux下线程实现之前,有必要先介绍一下posix线程标准。POSIX(Portable Operating System Interface)是一套接口API规范,有C语言描述,使用posix API编写的代码在遵循posix规范的平台间是可以移植的,JVM在linux系统上使用的就是pthread线程库作为底层实现。其中关于线程的API被称作pthread,该标准定义了从线程创建,通信,退出全部相关API(以pthread_开头)及其行为约束。

主要API:

函数前缀 功能
pthread_ 线程本身及相关函数
pthread_attr_ 线程属性对象
pthread_mutex_ 互斥锁
pthread_mutexattr_ 互斥锁属性对象
pthread_cond_ 条件变量
pthread_condattr_ 条件变量属性对象
pthread_key_ 线程私有数据
pthread_rwlock_ 读写锁
pthread_rwlockattr_ 读写锁属性对象
pthread_barrier_ 屏障
pthread_barrierattr_ 屏障属性对象
pthread_spin_ 自旋锁

使用时引入头文件#include <pthread.h>:

//--------------------线程相关API--------------------//

/** 创建线程 */
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    void *(*start_routine)(void*), void *arg);
/** 等待线程结束 */
int pthread_join (pthread_t thread, void**value_ptr);
/** 退出线程 */
void pthread_exit(void *value_ptr);
/** 脱离线程: 将线程属性的分离状态设置为 detached,等待结束时回收资源 */
int pthread_detach (pthread_t thread);
/** 结束线程: 给线程发送中止信号 */
int pthread_kill(pthread_t thread, int sig);
/** 获取线程ID */
pthread_t pthread_self(void);

//----------------线程属性相关API-------------------//

/** 设置线程属性,pthread_create会用到 */
int pthread_attr_init(pthread_attr_t *attr);
/** 销毁线程属性 */
int pthread_attr_destroy(pthread_attr_t *attr);

//----------------互斥锁相关API--------------------//

/** 初始化互斥锁对象 */
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
/** 销毁互斥锁对象 */
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
/** 获取互斥锁(阻塞方式) */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/** 获取互斥锁(非阻塞方式) */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/** 释放互斥锁 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//----------------条件变量相关API------------------//

/** 初始化条件变量 */
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
/** 销毁条件变量 */
int pthread_cond_destroy(pthread_cond_t *cond);
/** 在条件变量上阻塞等待 */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/** 在条件变量上有时限等待 */
int pthread_cond_timedwait(pthread_cond_t *cond, 
    pthread_mutex_t *mutex, const struct timespec *abstime);
/** 唤醒一个在条件变量上等待的线程 */
int pthread_cond_signal(pthread_cond_t *cond);
/** 唤醒全部在条件变量上等待的线程 */
int pthread_cond_broadcast(pthread_cond_t *cond);

//----------------线程私有数据相关API------------------//

/** 设置线程私有数据 */
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
/** 删除线程私有数据 */
int pthread_key_delete(pthread_key_t key);

//----------------读写锁相关API------------------//

/** 初始化一个读写锁 */
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
/** 销毁读写锁 */
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/** 读锁定(阻塞) */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
/** 读锁定(非阻塞) */
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
/** 写锁定(阻塞) */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
/** 写锁定(非阻塞) */
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/** 释放读写锁 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

//----------------屏障相关API------------------//

/** 初始化一个屏障(栅栏) */
int pthread_barrier_init(pthread_barrier_t *barrier,const pthread_barrierattr_t *attr,        unsigned count);
/** 销毁屏障 */
int pthread_barrier_destroy(pthread_barrier_t *barrier);
/** 在屏障上等待 */
int pthread_barrier_wait(pthread_barrier_t *barrier);

这里只列出了部分API,更多API可以使用man命令查看手册,熟悉pthreads api对理解java线程机制也很有帮助。

1.1.4 Linux线程支持

​ Linux中没有内核级线程的实现,所以只能在用户级别实现线程功能。比较著名的有早期的LinuxThreads,后来的NGPT以及NPTL。这些实现都利用了Linux提供的轻量级进程功能。

​ 轻量级进程(LWP)就是对一个进程的拷贝(clone()系统调用),不过在进行拷贝时,可以有选择的只拷贝部分,clone底层调用内核do_fork方法:

int do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, 	unsigned long stack_size)

clone_flags就是要拷贝的内容,如LinuxThreads创建线程时,用CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND指定拷贝进程内存空间,文件目录,打开的文件表,信号处理器,注意,这里的拷贝表示在新的进程task_struct中将相关部分地址设置为与原进程相应地址相同,其实是共享相同内存,这个消耗相对普通进程会低一些。可以看到轻量级进程与线程非常相似,共享进程内存空间,打开的文件列表,信号等,也有自己私有的寄存器,栈空间等,但不能就此将轻量级进程与线程等同。

  • LinuxThreads

    LinuxThreads通过创建一个轻量级进程来创建一个线程,即一对一的线程模型,这样,线程的调度有os负责,而LinuxThreads通过一个管理线程(用户级)来管理像线程取消、线程间的同步的工作。通过这种方式模拟了一个进程包含一组线程的定义,但毕竟是模拟的,必然存在很多问题,如管理线程增加了线程创建的开销,线程数受到os进程数限制(后来linux版本有改进),无法利用SMP,线程间通信需要通过信号量的方式等等,以及与posix严重不兼容问题,正是由于种种问题,出现了一些其他新的线程库实现。

  • NGPT

    NGPT(Next-Generation POSIX Threads)是由IBM开发的一套新的用于取代LinuxThreads的线程库,不过并没有被广泛使用,现在已经不在维护了。

  • NPTL

    NPTL(Native POSIX Thread Library)是由Red Hat开发的另一套用于取代LinuxThreads的线程库。NPTL 不在使用管理线程,使用内核支持的进程共享信号及信号处理器,通过共享内存上实现futex功能来做线程同步,以及可以利用SMP特性等,理论上提高了多线程的性能,还有一个重要的点,基本支持posix标准。

    现在大部分平台线程库都是NPTL,可以通过getconf GNU_LIBPTHREAD_VERSION命令查看。

总结: 因为Linux没有原生语义的线程支持,所以在linux平台的线程都是使用轻量级进程来实现线程,这种方式是即有核外也有核内参与,在核内通过轻量级进程模拟,在核外实现线程语义(线程组,线程通信等)在一些地方称之为“混合式线程实现”。总而言之,可以知道,在linux平台一个线程就是一个轻量级进程。

此篇为java多线程系列文章第一篇,更多内容关注以后更新