UNIX线程

来源:互联网 发布:振动检测软件 编辑:程序博客网 时间:2024/05/02 18:50

一、进程原语与线程原语的区别

线程包括的内容有:线程ID、寄存器、栈、调度优先级、信号屏蔽字、errno、线程私有数据。

线程共享的进程数据有:程序文本、程序全局内存和堆内存、栈、文件描述符。

==pthread_create==

#include <pthread.h>int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,                   void *(*start_rtn)(void), void *restrict arg)
成功时返回0,tidp指向的内存单元被置位新线程ID,start_rtn是线程的入口函数。

==pthread_exit==

#include <pthread.h> void pthread_exit(void *rval_ptr);

注意:rval_ptr指向的内存在线程结束后必须仍然有效,否则可能会出现非法访问的情况。看下面的例子:

#include "apue.h" #include <pthread.h>struct foo { int a, b, c, d;};void printfoo(const char *s, const struct foo *fp) {printf(s); printf(" structure at 0x%x\n", (unsigned)fp); printf(" foo.a = %d\n", fp->a); printf(" foo.b = %d\n", fp->b); printf(" foo.c = %d\n", fp->c); printf(" foo.d = %d\n", fp->d);}void * thr_fn1(void *arg) {struct foo foo = {1, 2, 3, 4};printfoo("thread 1:\n", &foo); pthread_exit((void *)&foo);//①}void * thr_fn2(void *arg) {printf("thread 2: ID is %d\n", pthread_self()); pthread_exit((void *)0);} int main(void) {pthread_t tid1, tid2; struct foo *fp;int err;//CREATE ONE THREADerr = pthread_create(&tid1, NULL, thr_fn1, NULL); if (err != 0)err_quit("can't create thread 1: %s\n", strerror(err)); err = pthread_join(tid1, (void *)&fp);//② if (err != 0)err_quit("can't join with thread 1: %s\n", strerror(err)); sleep(1);printf("parent starting second thread\n"); //CREATE TWO THEADerr = pthread_create(&tid2, NULL, thr_fn2, NULL); if (err != 0)err_quit("can't create thread 2: %s\n", strerror(err)); sleep(1);printfoo("parent:\n", fp); exit(0);//③}
THREAD ONE:将函数thr_fn1作为入口,执行到①的适合,调用phread_exit正常返回,但返回地址指向自己的栈,执行到②时,&foo所指向的内存不受保护了,会被释放。

THREAD TWO:将函数thr_fn2作为入口,没什么特殊操作,但线程二很有可能使用线程一之前用过的那段内存。所以在③出再次执行printfoo使,传入的fp参数所指向的内存已经不再是线程一执行时的那样了。

结果中的一种:

$ ./a.out 

thread 1:

structure at 0xbfafefc0

foo.a = 1 

foo.b = 2 

foo.c = 3 

foo.d = 4

parent starting second thread 

thread 2: ID is 134534144 

parent:

structure at 0xbfafefc0 

foo.a = 0 

foo.b = 134534144 

foo.c = 3

foo.d = 671642590


单个进程在不终止整个进程的情况下,可以通过以下三种方式正常退出:

1、线程只是从启动历程中返回,返回值是线程的退出码

2、线程可以被同一进程中的其他线程取消

3、线程调用ptread_exit()

==pthread_join==

# include <pthread.h> int pthread_join(pthread_t thread, void **rval_ptr);

调用线程将一直阻塞,直到相应进程发生上述的三种情况:

1、如果相应进程从启动例程返回,rval_ptr为返回码

2、被取消,为PTHREAD_CANCLED

3、为exit的值

==pthread_cleanup_push==

#include <pthread.h> void pthread_cleanup_push(void (*rtn)(void *), void *arg); void pthread_cleanup_pop(int execute);

当进程执行以下动作时会调用清理函数:

1、pthread_exit()

2、相应取消请求

3、用非零参数调用pthread_cleanup_pop()

注意:如果从启动例程中return,是不会调用清理函数的

==pthread_self==

#include <pthread.h> int pthread_equal(pthread_t tid1, pthread_t tid2);pthread_t pthread_self(void);

==pthread_cancel==

#include <pthread.h> int pthread_cancel(pthread_t tid);

线程可以通过调用此函数来请求取消同一进程中的其他线程。

二、进程和线程的表示

==单线程进程内存布局==


进程控制块:一般可以分为进程描述信息、进程控制信息、进程相关的资源信息和CPU现场保护机构。

用户地址空间:包括相关数据集和程序段。


图2-5中,一个进程中的所有线程共享其所属进程的一切资源,他们驻留在相同的地址空间,可以存取相同的数据。线程自己基本上不拥有系统资源,只拥有在运行中必不可少的资源,如线程状态、寄存器上下文和栈。

==用户态和内核态==

先了解什么是用户态和内核态,看一段简单的代码:

void testfork(){  if(fork() == 0){  printf(“create new process success!\n”) }  printf(“testfork ok\n”);  }  

fork的工作实际上是以系统调用的方式完成相应功能的,具体的工作是由sys_fork负责实施,对于任何操作系统来说,创建一个新的进程都是属于核心功能,因为它要做很多底层细致地工作,消耗系统的物理资源,比如分配物理内存,从父进程拷贝相关信息,拷贝设置页目录页表等等,这些显然不能随便让哪个程序就能去做,于是就自然引出特权级别的概念,显然,最关键性的权力必须由高特权级的程序来执行,这样才可以做到集中管理,减少有限资源的访问和使用冲突。

特权级显然是非常有效的管理和控制程序执行的手段,因此在硬件上对特权级做了很多支持,就Intel x86架构的CPU来说一共有0~3四个特权级,级最高,3级最低。

程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态,他们不能直接访问操作系统的程序和结构;反之,当程序运行在级特权级上时,就可以称之为运行在内核态。

==从用户态到内核态的方法==

a. 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

b. 异常             

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

c. 外围设备的中断

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

==用户栈和内核栈==

现在就好理解什么是内核栈、什么是用户栈了,用户堆栈的空间指向用户地址空间,内核堆栈的空间指向内核地址空间。当进程在用户态运行时,CPU堆栈指针寄存器指向的 用户堆栈地址,使用用户堆栈,当进程运行在内核态时,CPU堆栈指针寄存器指向的是内核栈空间地址,使用的是内核栈。

三、线程同步

==互斥量==

通过锁机制实现同步,同一时刻只能有一个线程执行关键部分的代码。

include <pthread.h>int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);int pthread_mutex_lock(pthread_mutex *mutex);int pthread_mutex_trylock(pthread_mutex *mutex);int pthread_mutex_destroy(pthread_mutex *mutex);int pthread_mutex_unlock(pthread_mutex *mutex);
1、初始化有两种方式

pthread_mutex mutex;mutex = PTHREAD_MUTEX_INITIALIZER;//静态赋值pthread_mutex_init(mutex, NULL);//采用初始化函数,属性设置NULL,具体会在12章讲解
2、加锁解锁

如果互斥量已经加锁了,再次调用lock的线程会被阻塞,若不希望被阻塞可以使用trylock

3、释放空间

如果使用malloc动态分配互斥量,需要使用destory销毁空间。注意,当多个线程需要访问动态分配的对象时,可以建立一个数据结构,存放锁和和计数器,确保在所有使用该锁的线程完成访问数据后,再进行destory。

==读写锁==

类似互斥锁,但有更高的并发性,读写锁共有三个状态:

1、读模式加锁。当读写锁以读模式加锁后,所有试图以读模式加锁的其他线程都可以得到访问权限,但是想以写模式加锁的线程将被阻塞。注意,为了防止写加锁的线程被饿死,当有一个想以写模式加锁的线程在等待后,不允许新的线程读加锁了。

2、写模式加锁。将会阻塞所有企图对读写锁加锁的线程。

3、不加锁

#include <pthread.h>int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
==条件变量==

条件变量受互斥量的保护,提供了一种机制,允许线程以无竞争的方式等待特定的条件发生。

#include <pthread.h>int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,                           const struct timespec *restrict timeout);int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);

1、等待信号
线程使用wait等待条件变为真,互斥量mutex对条件cond进行保护。如果条件不满足,wait函数会将调用线程放在等待条件的线程队列中,然后对互斥量解锁,注意这个解锁是wait自带的;如果条件满足,就可以执行临界区代码,手动解锁。

timewait只是多了个计时器而已。

2、发送信号

在改变条件变量后,signal唤醒等待该条件的某个线程,broadcast唤醒等待该条件的所有线程。

原创粉丝点击