【操作体统】Linux下浅述线程

2019-07-14 11:40发布

1、初识线程

什么是线程?

  • 线程是进程中的一个实体,进程是资源分配的基本单位,线程是调度/执行的基本单位。
  • 简单说来就是在一个程序里的一个执行路线就叫做线程(thread),或者说是:线程是“一个进程内部的控制序列”。
  • 在内核中看到的线程也是以PCB为代表的创建的新的PCB和原PCB共用相同的虚拟地址空间。
  • Linux中使用进程来模拟实现线程,这种线程也称之为轻量级进程(ZWP)。
  • “线程在进程内部运行”—–>线程在进程的地址空间中运行。
  • 一切进程都至少有一个线程。
  • 线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但它可与同属一个进程的其他线程共享进程所拥有的全部资源。
  • 一个线程可以创建和撤销另一个进程。
  • 同一个进程之间的多个线程可以并发执行。
  • 由于线程之间的相互制约,致使线程在运行中也呈现出间断性。
  • 线程也拥有就绪,等待,运行三种基本状态。

线程的属性

  1. 每个线程有唯一的一个标识符和一张线程描述表,线程表描述记录了线程执行的寄存器和栈等现场状态。
  2. 不同的线程可以执行相同的程序,即同一个服务程序被不同用户调用时,操作系统创建不同的线程。
  3. 同一进程中的各个线程共享该进程的内存地址空间。
  4. 线程是处理器的独立调度单位,多个线程是可以并发执行的。在单CPU的计算机系统中,各线程可交替的占用CPU;在多CPU的计算机系统中,各线程可同时占用不同的CPU,若干个CPU同时为一个进程内的各线程服务,则可缩短进程的处理时间。
  5. 一个线程被创建后便开始了他的生命周期,直至终止,线程在生命周期内会经历等待态,就绪态,运行态等各种状态变化。

2、进程和线程

进程线程基本关系:

  • 进程是资源竞争的基本单位。
  • 线程是程序执行的最小单位。
  • 线程共享进程数据,但也拥有自己的一部分数据(线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级)。
进程的多个线程共享:>
  • 同一地址空间,因此Text Segment,Data Segment都是共享的(例如:如果定义一个函数,在各线程都可以调用到;如果定义一个全局变量,在各线程都可以访问到)
  • 文件描述符表
  • 每种信号的处理方式(SIG_IGN,SIG_DFL或自定义的信号处理函数)
  • 当前工作目录
  • 用户ID和组ID
进程和线程的关系: 这里写图片描述

3、线程的优缺点

线程的优点:

  • 创建一个新线程的代价要比创建一个新进程的代价小得多。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少得多。
  • 线程占用的资源要比进程小得多。
  • 线程之间共享数据更容易。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速 I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
【注】:线程并不是越多越好,线程太多频繁使用调度函数也会造成操作系统的负担。

线程的缺点:

  • 性能损失(一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享一个处理器。如果计算密集型线程的数量比可用的处理器多,那么就有可能造成较大的性能损失,这里的性能损失指的是操作系统增加了额外的同步和调度开销,而可用的资源不变);
  • 健壮性降低(编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,也就是说线程之间是缺乏保护的);
  • 缺乏访问控制(进程是访问控制的基本粒度,在一个线程中调用某些函数(OS函数,处理signal函数,调用kill/exit函数)会对整个进程造成影响;一个线程崩溃,会导致整个进程都异常终止);
  • 编程难度提高(时序问题,共享资源的操作等);

4、进程ID和线程ID

在Linux中,在用户态下,目前的线程实现是Native POSIX Thread Libaray ,简称NPTL。每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
  • pthread_create函数会产生一个线程ID(用户态),存放在第一个参数指向的地址中。
  • 内核级线程属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度的最小单位,所以需要一个数值来唯一的表示该线程。
  • pthread_create函数产生并标记在第一个参数指向的地址中的线程ID中,属于NPTL线程库的范畴。线程库就是根据该线程ID来进行对线程的后续操作的。
  • 线程库NPTL提供了pthread_self函数,可以获得线程自身的线程ID(用户态)。

线程组

struct task_struct{ ... pid_t pid; pid_t tgid; ... struct task_struct *group_leader; ... struct list_head thread_group; ... };
  • 多线程的进程又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。
  • 进程描述符结构体中的pid指的是对应的线程ID(内核级线程,类型为pid_t);
  • 进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。
用户态 系统调用 内核进程描述符中对应的结构 线程ID pid_t gettid(void); pid_t pid 进程ID pid_t getpid(void); pid_t tgid 查看一个内核级线程ID的方式: Linux提供了gettid系统调用来返回其线程ID,但是glibc并没有将该系统调用封装起来,在开放接口来供程序员使用。 获取内核级线程ID可以采用如下方式: #define _GNU_SOURCE /* or _BSD_SOURCE or _SVID_SOURCE */ #include #include /* For SYS_xxx definitions */ int syscall(int number, ...); 查看用户态的线程ID方式: 可以借用pthread库中的 pthread_self()函数来实现。

线程组相关概念

  • 线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中被称为group leader。
  • 内核在创建第一个线程时,会将线程组的ID的值设置为第一个线程的线程ID,group_leader指针则指向自身,即主线程的进程描述符。
  • 即线程组内存在一个线程,线程ID等于进程ID,该线程被称为主线程。
  • 在进程中有父进程的概念,但是在线程中所有的线程都是对等关系。
【例】: #include #include #include #include #include #include void *rout(void *arg){ (void)arg; pid_t pid = syscall(SYS_gettid); while(1){ printf("I am thread:所属进程ID为:%d ,内核级线程ID为: %d,用户态下线程ID为: %lx ",getpid(),pid,pthread_self()); sleep(1); } } int main(){ pid_t pid = syscall(SYS_gettid); //获得当前进程的内核级线程ID int ret; pthread_t tid; ret=pthread_create(&tid,NULL,rout,NULL); if(ret!=0){ perror("pthread_create"); exit(1); } while(1){ printf("I am main: 所属进程ID为: %d 内核级线程ID为: %d 用户态线程ID为: %lx ",getpid(),pid,pthread_self()); sleep(1); } return 0; } 【运行结果】: 这里写图片描述 这里写图片描述

进程地址空间布局

pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。 这里写图片描述

5、线程控制

【注】:在操作系统内部,它不管什么进程线程的,它只以PCB为准,只有在用户态里才有线程的概念。一般实现线程会用到一个POSIX线程库,在这里可以通过调用POSIX库里的函数来实现有关线程的各种操作。不过内核中也有一种内核级线程。 线程有两个基本类型: 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。如POSIX线程库。 系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行、撤消线程。

用户级线程:POSIX线程库

由系统库支持。线程的创建和撤销以及线程状态的变化都由库函数控制并在目态(user态)完成,与线程相关的控制结构TCB保存在目态并由系统维护。由于线程对操作不可见(操作系统可见的必然保存在kernel态由系统维护),系统调度仍以进程为单位(同一进程内线程相互竞争),核心栈的个数与进程个数相对性。
  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些库函数,就要引入头文件
  • gcc在链接这些线程函数库时要使用编译器命令的“-lpthread”选项(pthread是共享库文件)。
创建线程: 【功能】:创建一个新的线程 【函数原型】: #include int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg); //返回值:成功返回0,失败返回错误码,如: //EAGAIN 描述: 超出了系统限制,如创建的线程太多。 //EINVAL 描述: tattr 的值无效。 【参数解析】:
  • thread:是一个输出型参数,是一个整数,返回在线程库中使用的线程ID。
  • attr:设置线程的属性,attr为NULL表示使用默认属性。
  • start_routine:是个函数地址,指向线程启动后要执行的函数,即线程入口函数。该函数的返回值类型是void*,接受的参数类型也是void*。如果需要向start_routine函数传递多个参数,就需要把这些参数放到一个结构中,然后把这个结构的地址作为void *传入。
【注意】:
  • 使用具有必要状态行为的attr 调用pthread_create()函数。
  • start_routine是新线程最先 执行的函数。当start_routine 返回时,该线程将退出,其退出状态设置为由 start_routine返回的值。
  • 当pthread_create()成功时,所创建线程的ID 被存储在由tid 指向的位置中。
  • 使用NULL 属性参数或缺省属性调用pthread_create()时,pthread_create() 会创建一 个缺省线程。在对tattr 进行初始化之后,该线程将获得缺省行为。
  • 在进程中只有一个控制线程。
  • 线程创建的时候不能保证哪个先运行。
线程终止: 只终止线程有以下三种方法:
  1. 从线程函数return,这种方法对主线程不适用,从main函数return相当于直接调用exit。
  2. 线程可以调用pthreade_exit终止自己。
  3. 一个线程可以调用pthreade_cancel终止同一进程中的另一线程。
【注】:在这里不推荐方法三。

系统级线程(核心级线程):

通过系统调用由操作系统创建,线程的控制结构TCB保存于操作系统空间,线程是CPU调度的基本单位。由于系统调用以线程为单位,操作系统需要为每个线程保存一个核心栈。