您的位置:首页 > 运维架构 > Linux

Linux线程简说

2015-11-26 16:00 766 查看
Linux线程简说
尹德位 2015 西安
本文内容臃肿,作者尽力在描述自己对线程的感悟,表达能力有限,敬请读者去其糟粕。

系统环境: RedHat Enterprise Linux 7.0 x86_64

内核版本: linux-4.1.0

Glibc版本: glibc-2.22

IT名言:进程是资源分配的基本单位。线程是程序调度的基本单位。

目录:
1.线程概念
2.线程创建
3.线程特性
4.线程控制
5.线程同步
6.线程应用
<<正文>>
1.线程概念

众所周知,进程就是一个关于某数据集合的运行过程。现代操作系统理论体系是建立在进程基础上的。一切功能性程序,http服务,nginx服务,Oracle数据库等等都是以进程为单位运行的。进程的运行除了需要分配内存装载代码段(.txt段)外,还需要分配必要的资源才能运行。
有时候我们需要利用CPU多核的优势实现代码的并行执行来提高效率,而恰巧这些多份并行执行的代码是可以共享资源,这种情况下采用进程技术来实现多任务程序就显得臃肿笨拙了。于是线程技术就在千呼万唤中诞生了。
线程,是轻量级进程。除了必要的栈、上下文等资源外,线程不需要其他太多额外资源便能运行。其精巧高效的特性是软件设计中常采用线程来实现多任务的主要原因。比起进程,线程更便于灵活调度运行。因而如今的操作系统都以线程为单位来调度,以极大提高系统的运行效率。
尽管线程有许多优势,但并非可以取代进程。下面讲述二者的区别,以便读者理解其应用场景。
区别1:独立性
进程是完全独立的,进程之间互不干扰对方的内存空间(也称进程空间),亲属进程创建的多个子进程与其父进程之间依然保持独立性,换言之,若任一进程出现故障,不影响其他进程运行。而线程与之相比则不同,处于同一进程空间中的多个子线程之间共享父线程(父进程)的资源,任一线程若出现故障导致系统信号触发,则整个进程空间都受牵连。比如某一线程访问非法内存地址,则SIGSEGV信号将终结整个进程。
区别2:作用范围
进程是全系统范围内可见的,通过ps命令可以看到任何正在运行的进程信息。当然线程也可以看到,但不同的是,线程只能与同一进程空间的其他线程之间保持联系,跨越进程的线程之间没有建立通信的必要。因为线程只是共享父进程数据资源,跨越进程的线程因为加工数据资源的对象处于不同空间而失去意义。
区别3:正文段区间
子进程的正文段与父进程共享,且拥有全部执行区间,若没有严格逻辑限制,则子进程将从其创建开始,与父进程执行同一代码副本直到结束。而线程的运行期间相较而言短了许多,只是从线程入口函数开始,到其函数结束线程自动终止。
区别4:项目代码体系
进程创建的系统调用fork由内核提供;线程库函数由glibc(运行于内核之上的库)提供。

2.线程创建

几乎所有的Linux操作系统都提供线程技术,因为线程的实现由Linux内核提供。
线程创建函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
该函数功能是创建一个新的线程,其函数体在标准glibc库中的代码可以看到:
http://ftp.gnu.org/pub/gnu/glibc/glibc-2.22.tar.gz
nptl/pthread_create.c :
int __pthread_create_2_1 (newthread, attr, start_routine, arg)
pthread_t *newthread;
const pthread_attr_t *attr;
void *(*start_routine) (void *);
从源码得知,线程真身的创建是调用了create_thread函数:
/* Create the thread. We always create the thread stopped
so that it does not get far before we tell the debugger. */
retval = create_thread (pd, iattr, true, STACK_VARIABLES_ARGS,
&thread_ran);
继续追踪create_thread:
sysdeps/unix/sysv/linux/createthread.c :
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);
由此发现,线程的创建实际上还是clone技术的封装。换句话说,内核提供的clone系统调用可以创建线程,而POSIX线程库也是通过clone技术实现的线程。Linux的NPTL线程技术兼容了POSIX标准,所以在描述上不做区分。
可通过Linux的man手册页来查看 : man clone
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
综上所述,无论是调用clone创建线程,还是调用POSIX线程库的pthread_create创建一个线程,其本质都是一样。

3.线程特性

许多教科书在描述线程和进程的时候都不会刻意区分,因为在执行单位上二者没有区别。内核对线程和进程的调度,实际上是统一的。我们知道,每个进程都有一个进程控制信息结构,维护在内核数据区。该结构为 struct task_struct ,每一个进程都会有一个唯一的结构体与之对应。
该结构体的原型定义在内核代码的 include/linux/sched.h中。

线程,同样具有该结构体。也即每个线程也都有一个完全独立的结构体节点。
那么,问题就出现了:

进程的管理都是以其标识进程ID(即PID)来实现的,莫非线程也具有PID ?

答案是肯定的。

接下来我们就详细讨论下线程的标识ID。

首先,我们来看看,命令行里看看多进程和多线程的显示结果。

通过ps auxH 或者 pstree –ap 来查看(读者可自己编写一个创建线程的程序):



进程号35985的进程有一个ID为35986的子进程。还发现它的名字还有一对“{ }”,其实35986就是一个线程。普通进程身份的子进程的名字不带大括号。

再通过ps命令我们再看看:



可以看出,两个线程的PID居然相同。

这就是线程的ID特性,每一个线程对外而言都表达其父进程的ID。换句话说,一个进程创建的所有子线程,都是以同一进程身份(即父进程)运行,而其内部依然是并发运行。

ps命令的H参数可以看到子线程,并且其显示效果上与子进程并无太多区别,但,若用kill命令终止其中任何一个子线程,则整个进程的全部线程(包括父线程/父进程)都将终结。这就是与多进程程序不同的地方,如果是用kill终止一个子进程,则其他进程不受任何影响。从这点来说,多线程程序的独立性就差一些,如果某个线程资源访问失误(访问空指针)导致SIGSEGV信号触发的话,牵连甚广。

好了,上面说了线程的具有进程特性的PID,下面说说线程特有的TID。
每一个线程除了拥有被调度控制用的PID外,还有一个pthread_t类型的TID,该ID实际是一个long类型的数据区,存储着每个线程的内部标识ID。这个TID只在同一进程空间的内有意义,离开进程空间该ID没有任何作用。
换言之,TID是父进程用来控制子线程时采用的线程标识。



多线程程序的在线调试也比多进程程序简单,因其共享同一进程空间。

可通过info thread查看各线程的编号,并通过thread +编号 直接切换到该线程。

(bt命令可以查看线程的栈数据。)

事实上,从线程的栈可以看出,线程是clone产生的,如下图:



4.线程控制

线程控制一般指父线程对子线程的控制,命令行实际不能对子线程直接进行终止操作(会波及整个进程)。所以线程的控制技术此处就不再赘述,提供几个关键词供读者查阅学习:分离属性,线程取消点,线程回收,线程信号。

5.线程同步

线程大量被采用的一大原因是其精巧高效,运行调度很方便。那么,若多个线程间有数据互斥操纵的场合下,其同步技术是不是一样优越于进程呢?
答案是肯定的。
线程的同步技术大体有这样几种(注:线程间独有,进程间不可采用):
互斥量(即线程锁),读写锁,自旋锁,条件变量。
线程锁只用于同一进程空间内的全部子线程中,跨越进程的线程间一般不采用线程锁来同步。我们知道,进程间的同步技术有PV操作(信号量技术),那线程锁是不是就比信号量快呢?
答案还是肯定的。
信号量是一种特殊系统资源,它是全系统可见的。因此要实现互斥,在进程P、V操作时就必须实现原子操作。对于内核而言,原子操作本身是一种负担较重的处理,并且原子操作会降低内核的并发性能。因而,P、V操作都需要内核提供操作方法,即系统调用。用户程序若频繁的采用系统调用,则会降低程序性能,还会增加内核上下文切换开销,总的来说,既损失了自身效率,还加重了内核负担。
与之相比,线程锁则优越许多。因为线程锁的作用空间很小,只限于单个进程内,因而线程锁的实现就进化到了全局变量。即,线程锁就是一个全局控制变量,这样在对线程锁进行操作的时候就无需内核帮助,也避免访问内核数据区提高了性能。再者,作用域变小了,参与资源竞争的个体(进程/线程)就减少了,使得多任务程序可以快速得到互斥资源。
总的一句话,线程的同步技术快于进程。

6.线程应用

说了这么多的线程优势,那是不是所有的多任务环境都可以采用线程来替代进程设计呢?
答案是不可以。
进程和线程,二者各有优势,应在不同的场景采用不同方式。
线程虽然运行时无需过多资源,但在需要独立控制的任务里,它又变成了缺陷。比如当今流行的nginx服务,它采用的是进程池技术,之所以是进程而不是线程,就是考虑了任务独立性的要求。对于数据独立性要求低,或者说是专门处理共享数据,只是为了提高代码并发执行效率的话,采用线程就是优选之举了。
限于篇幅,本文就此停笔,宏观上的线程特性请参考:

http://my.oschina.net/cnyinlinux/blog/367910

<<END>>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: