6. 线程和fork
6.1 多线程下的fork
(1)历史包袱
①fork与多线程的协作性很差,这是POSIX系统操作系统的历史包袱。
②长期以来程序都是单线程的,fork运行正常,但引入线程这后,fork的适用范围大大缩小。
(2)多线程下的fork
①在多线程执行的情况下调用fork函数,仅会将发起调用的线程复制到子进程中去(线程ID与父进程发起fork调用的线程ID一样)。也就是说,不能同时创建出与父进程一样多的子线程。
②其他线程均在子进程中立即停止并消失,并且不会为这些线程调用清理函数以及针对线程局部存储变量的析构函数,这可能会造成子进程的内存泄漏。
③虽然只有fork调用线程被复制到子进程,但子进程继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。这意味着,如果某个线程锁定了某个互斥锁,那么在子进程中这个互斥锁可能因得不到释放而造成死锁现象的发生。
(3)多线程中fork的使用方法
①方法1:调用fork以后,立即调用exec()函数执行另一个程序,彻底隔断子进程与父进程的关系。由新的进程覆盖掉原有的内存,使用子进程中所有的线程相关的对象消失。
②方法2:使用pthread_atfork() + fork来创建子进程。
【编程实验】“死锁”现象的产生
//pthread_fork.c
#include <pthread.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> //线程函数 void* th_func(void* arg) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; printf("process(%d)_thread(%x) lock mutex\n", getpid(), pthread_self()); pthread_mutex_lock(&mutex); struct timespec ts = {10, 0}; nanosleep(&ts, 0); //休眠10秒 pthread_mutex_unlock(&mutex); printf("process(%d)_thread(%x) unlock mutex\n", getpid(), pthread_self()); } int main(void) { pthread_t th; int err = 0; //创建子线程(注意,这个子线程只会存在于父进程中,子进程没有!) if((err = pthread_create(&th, NULL, th_func, NULL)) !=0){ perror("pthread_create error"); } sleep(1); pid_t pid; pid = fork(); //刚fork出来的子进程只能是单线程的,它只保留了调用fork //的线程,而不保留父进程的其它线程。) if(pid <0){ perror("fork error"); exit(1); }else if(pid > 0){ //parent process printf("parent process id = %d thread id = %x\n", getpid(), pthread_self()); pthread_join(th, 0); //等待子线程结束 }else{ //child process printf("child process id = %d thread id = %x\n", getpid(), pthread_self()); th_func(0); } wait(pid); //等待子进程结束 return 0; } /*输出结果 process(1710)_thread(b77cab70) lock mutex //父进程中的子线程获得mutex,此时mutex为被占用状态 parent process id = 1711 thread id = b77cb6c0 //父进程调用fork后,mutex的状态会被子进程继承! child process id = 1713 thread id = b77cb6c0 //创建子进程 process(1713)_thread(b77cb6c0) lock mutex //子进程的主线程试图获得mutex,因复制了父进程的mutex锁 //的状态。所以子进程的主线程(也是唯一的线程)会一直被阻塞! process(1711)_thread(b77cab70) unlock mutex //父进程释放mutex ^C //子进程会因其主线程被阻塞而无法退出! */
6.2 pthread_atfork函数
(1)pthread_atfork()函数
头文件 |
#include <pthread.h> |
函数 |
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); |
功能 |
安装fork处理函数 |
返回值 |
成功返回0,否则返回错误编号 |
备注 |
①prepare处理函数数在父进程调用fork之后,创建子进程前调用。 ②parent处理函数在创建子进程之后,但fork返回之前在父进程环境中执行。 ③child处理函数创建子进程之后,fork返回之前被调用。在子进程环境中调用。 ④可以多次调用pthread_atfork来安装多个fork处理函数。其中parent和child的处理程序是以它们注册的顺序调用的。而prepare处理程序的调用是与其注册时的顺序相反。 |
【编程实验】子进程中清理互斥锁
//pthread_atfork.c
#include <pthread.h> #include <stdio.h> #include <stdlib.h> /*清理mutex锁 1、定义了两个互斥锁。 2、prepare处理函数中获取这两个锁 3、child处理函数在子进程环境中释放锁 4、parent处理函数在父进程环境中释放锁 */ pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER; //在fork出子进程之前调用,因此只会在父进程中执行! void prepare(void) { printf("process(%d) preparing locks...\n", getpid()); pthread_mutex_lock(&lock1); pthread_mutex_lock(&lock2); } void parent(void) { printf("parent unlocking locks...\n"); pthread_mutex_unlock(&lock1); pthread_mutex_unlock(&lock2); } void child(void) { printf("child unlocking locks...\n"); pthread_mutex_unlock(&lock1); pthread_mutex_unlock(&lock2); } void* th_func(void* arg) { printf("thread started...\n"); sleep(10); return 0; } int main(void) { int err = 0; pid_t pid; pthread_t tid; //可以多次调用pthread_atfork来安装多个fork处理函数. //为简单起见,这里只调用一次。 if((err = pthread_atfork(prepare, parent, child)) != 0){ perror("can't install fork handlers"); } //创建子线程 if((err = pthread_create(&tid, NULL, th_func, NULL)) !=0 ){ perror("can't create thread"); } sleep(2);//让出CPU,让子线程去运行! printf("parent about to fork...\n"); //创建子进程 pid = fork(); //子进程只继承父进程中调用fork的线程(即主线程) if(pid < 0){ perror("fork error"); exit(1); }else if(pid == 0){ //child process printf("child returned from fork\n"); }else{ printf("parent returned from fork\n"); } wait(pid); //等待子进程结束 return 0; } /* 输出结果 thread started... parent about to fork... process(1786) preparing locks... //父进程获得两个锁 parent unlocking locks... parent returned from fork child unlocking locks... //子进程复制父进程的两个锁状态,这里成功释放锁 child returned from fork */
7. 线程和I/O
(1)进程中的所有线程共享相同的文件描述符。
(2)文件的定位(lseek)和读写(read/write)是两个操作,如果两个线程同时对同一个文件进行并发读写操作时,可能会造成不安全。
(3)而pread/pwrite将定位和读/写操作变成一个原子操作。这极大方便多线程下对文件的操作。如以下读取文件是线程安全的。
①pread(fd, buf1, 100, 300); //确保线程A读取偏移量为300处的记录
②pread(fd, buf2, 100, 700); //确保线程B读取偏移量为700处的记录