Linux进程和线程

进程与线程

(1)基本概念

进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

进程——资源分配的最小单位,线程——程序执行的最小单位,进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

(2)使用线程的理由

  • 理由1:多线程是一种非常“节俭”的多任务操作方式。启动进程要分配地址空间、建立众多数据表来维护代码段、堆栈段、数据段,所以开销大。并且进程之间切换时间要大于线程的切换。

  • 理由2:线程之间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。

Linux线程模型

实际上,posix线程和一般的线程不同,在概念上没有主线程和子线程之分(虽然在实际实现上还是有一些区分),如果仔细观察apue或者unp等书会发现基本看不到“主线程”或者“子线程”等词语,在csapp中甚至都是用“对等线程”一词来描述线程间的关系。

在Linux 2.6以后的posix线程都是由用户态的pthread库来实现的。在使用pthread库以后,在用户视角看来,每一个tast_struct就对应一个线程(tast_struct原本是内核对应一个进程的结构),而一组线程以及他们所共同引用的一组资源就是进程。

从Linux 2.6开始,内核有了线程组的概念,tast_struct结构中增加了一个tgid(thread group id)字段。getpid(获取进程号)通过系统调用返回的也是tast_struct中的tgid,所以tgid其实就是进程号。而tast_struct中的线程号pid字段则由系统调用syscall(SYS_gettid)来获取。

当线程收到一个kill致命信号时,内核会将处理动作施加到整个线程组上。为了应付“发送给进程的信号”和“发送给线程的信号”,tast_struct里面维护了两套signal_pending,一套是线程组共用的,一套是线程独有的。通过kill发送的致命信号被放在线程组共享的signal_pending中,可以任意由一个线程来处理。而通过pthread_kill发送的信号被放在线程独有的signal_pending中,只能由本线程来处理。

如果信号的默认处理动作是终止该进程,那么把信号传递给某个线程仍然会杀掉整个进程。

另外,系统调用syscall(SYS_ gettid)获取的线程号与pthread_ self获取的线程号是不同的,pthread_self获取的线程号仅仅在线程所依赖的进程内部唯一。当一个线程由另一个进程join等待退出、或者一个分离(detached)的线程退出,则这个线程id还是可以被重复利用的。所以,在内核中唯一标识线程ID的线程号只能通过系统调用syscall(SYS_ gettid)获取。

有关线程的函数:

(1)创建线程

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);

pthread_create用于创建一个线程,成功返回0,否则返回Exxx(为正数)。
在主程序运行的时候,它是以单进程中的单个控制线程启动的,该线程成为主线程,由它调用pthread_create来创建其他的线程。

pthread_t *tid:线程id的类型为pthread_t,通常为无符号整型,当调用pthread_create成功时,通过*tid指针返回(在C中如果是返回常常使用指针来返回)。

const pthread_attr_t *attr:指定创建线程的属性,如线程优先级、初始栈大小、是否为守护进程等。可以使用NULL来使用默认值,通常情况下我们都是使用默认值。

void *(*func) (void *):函数指针func,指定当新的线程创建之后,将执行的函数。函数的参数是void*,这样可以接收任意参数。

void *arg:线程将执行的函数的参数。如果想传递多个参数,请将它们封装在一个结构体中,如果没有就设为NULL

(2)等待一个线程退出

int pthread_join (pthread_t tid, void ** status);

pthread_join用于等待某个线程退出,成功返回0,否则返回Exxx(为正数)。
pthread_t tid:指定要等待的线程ID

void ** status:如果不为NULL,那么线程的返回值存储在status指向的空间中(这就是为什么status是二级指针的原因!这种参数也称为“值-结果”参数,即一级指针指向这些值对应的地址,二级指针指向一系列地址,可以想象成是数组,第一层指针指向每一行中的元素,二级指针指向的是每一行)。

###(3) 返回当前线程ID
pthread_t pthread_self (void);
里面不用传入参数,它会返回调用该函数的线程id。

(4)使线程变成分离状态

int pthread_detach (pthread_t tid);

pthread_detach用于是指定线程变为分离状态,就像进程脱离终端而变为后台进程类似。成功返回0,否则返回Exxx(为正数)。变为分离状态的线程,如果线程退出,它的所有资源将全部释放。而如果不是分离状态,线程必须保留它的线程ID,退出状态直到其它线程对它调用了pthread_join

(5)线程退出

void pthread_exit (void *status);

pthread_exit用于终止线程,可以指定返回值,以便其他线程通过pthread_join函数获取该线程的返回值。

void *status:指针线程终止的返回值。

如果程序中使用到了pthread库中的函数,除了要#include<pthread.h>,在编译的时候还要加上-lpthread选项。

注意
pthread_exit()用于线程退出,可以指定返回值,以便其他线程通过pthread_join()函数获取该线程的返回值。

return是函数返回,只有线程函数return,线程才会退出,即一个线程里面可能会调用多个函数,而只有当线程里面的函数调用了return,线程才退出。
exit是进程退出,如果在线程函数中调用exit,进程中的所有函数都会退出!即后面的程序不再执行。

(6)线程之间的互斥

互斥锁:使用互斥锁(互斥)可以使线程按顺序执行。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程。互斥锁还可以保护单线程代码。

int pthread_mutex_lock(pthread_mutex_t * mptr); 
int pthread_mutex_unlock(pthread_mutex_t * mptr); 

两个函数都是成功返回0,失败返回Exxx(是正值)

在对临界资源进行操作之前需要pthread_mutex_lock先加锁,操作完之后pthread_mutex_unlock再解锁。而且在这之前需要声明一个pthread_mutex_t类型的变量,用作前面两个函数的参数。

###(7)线程之间的同步
线程同步需要条件变量。使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用。对条件的测试是在互斥锁(互斥)的保护下进行的(条件变量函数参数中有一个参数是互斥量)。

如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作:

  • 唤醒
  • 再次获取互斥锁
  • 重新评估条件

在以下情况下,条件变量可用于在进程之间同步线程:

  • 线程是在可以写入的内存中分配的
  • 内存由协作进程共享

所以,如果要求函数阻塞等待…直到…,此时可以用条件变量来实现。

条件变量相关函数:

int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr); 
int pthread_cond_signal(pthread_cond_t *cptr); 

两个函数也都是成功返回0,失败返回Exxx(是正值)

pthread_cond_wait用于等待某个特定的条件为真,pthread_cond_signal用于通知阻塞的线程某个特定的条件为真了。在调用者两个函数之前需要声明一个pthread_cond_t类型的变量,用于这两个函数的参数。

为什么条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的呢?因为“某个特性条件”通常是在多个线程之间共享的某个变量。互斥锁允许这个变量可以在不同的线程中设置和检测。

通常,pthread_cond_wait只是唤醒等待某个条件变量的一个线程。如果需要唤醒所有等待某个条件变量的线程,需要调用:

int pthread_cond_broadcast (pthread_cond_t * cptr);

默认情况下面,阻塞的线程会一直等待,知道某个条件变量为真。如果想设置最大的阻塞时间可以调用:

int pthread_cond_timedwait (pthread_cond_t * cptr, pthread_mutex_t *mptr, const struct timespec *abstime);

如果时间到了,条件变量还没有为真,仍然返回,返回值为ETIME