问题背景
起因:笔者在开发一个统计Linux系统各个进程、线程loading的软件(精度高于top命令),目前是间隔500ms采样一次,在内核遍历所有线程时,发现如果有的线程在采样间隔中间退出的话,for_each_process不能遍历到该线程,导致该周期内的线程runtime丢失。为了解决该问题,在线程退出路径的最后一步do_exit()函数中,记录最后一周期该线程的runtime,理论上这样线程的runtime就不会丢失了。但实际操作中遇到了一个新的问题:do_exit中统计了该线程的运行时间,在do_exit()的do_task_dead()函数中进行了调度,该线程确实再也不会运行了,一切看起来很nice,但是其task_struct并没有在第一时间被回收(原因未明确),导致在for_each_process依然可以遍历到该进程,这就导致了该退出线程的runtime被重复记录。
笔者在开发的过程中产生了几个问题:一个进程由一个主线程与若干个子线程构成,如果主线程退出了,那么子线程的行为是什么样的呢?是退出还是继续运行?如果子线程继续运行的话,其tgid是谁?如果是父进程fork了子进程,然后父进程退出呢?做一下关于孤儿进程和僵尸进程的实验
主线程退出后子线程的状态
背景知识
return和pthread_exit的区别
main函数中return和pthread_exit的区别:return之后,子线程也会退出。pthread_exit() 函数只会终止当前线程(并不退出),不会影响进程中其它线程的执行。
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
void *ThreadFun(void *arg)
{
while (1) {
printf("child thread pid=%d\n", gettid());
sleep(1);
}
}
int main()
{
printf("father thread pid=%d\n", gettid());
int res;
pthread_t myThread;
res = pthread_create(&myThread, NULL, ThreadFun, NULL);
sleep(3);
printf("father thread reach trace point\n");
return 0;
}
输出:
chen@ubuntu:~/Desktop/test_fork$ ./test_fork
father thread pid=6534
child thread pid=6535
child thread pid=6535
child thread pid=6535
子线程也直接挂了,如果把return 0换成pthread_exit的话
chen@ubuntu:~/Desktop/test_fork$ ./test_fork
father thread pid=6472
child thread pid=6473
child thread pid=6473
child thread pid=6473
father thread reach trace point
child thread pid=6473
child thread pid=6473
可以发现子线程还是在继续跑
join和detach的区别
区别在于两者是否阻塞主调线程
- 当使用join()函数时,主调线程阻塞,等待被调线程终止,然后主调线程回收被调线程资源,并继续运行;
- 当使用detach()函数时,主调线程继续运行,被调线程驻留后台运行,主调线程无法再取得该被调线程的控制权。当主调线程结束时,由运行时库负责清理与被调线程相关的资源;
- 如果什么都不加,默认是join,join的位置在主函数的最下面,也就是pthread_exit()之前,相当于pthread_exit会自动等待子线程都运行完。这种情况相当于主线程、子线程可以一起并行,主线程也会等待子线程结束。
主线程pthread_join()
int main()
{
printf("father thread pid=%d\n", gettid());
int res;
pthread_t myThread;
res = pthread_create(&myThread, NULL, ThreadFun, NULL);
sleep(3);
printf("father thread reach trace point\n");
pthread_join(myThread, NULL);
pthread_exit(NULL);
return 0;
}
输出:
chen@ubuntu:~/Desktop/test_fork$ ./test_fork
father thread pid=6705
child thread pid=6706
child thread pid=6706
child thread pid=6706
father thread reach trace point
child thread pid=6706
child thread pid=6706
查看子线程的tgid是多少
chen@ubuntu:~/Desktop/test_fork$ readlink -f /proc/*/task/6706 | cut -d/ -f3
6705
主线程pthread_detach()
int main()
{
printf("father thread pid=%d\n", gettid());
int res;
pthread_t myThread;
res = pthread_create(&myThread, NULL, ThreadFun, NULL);
sleep(10);
printf("father thread reach trace point\n");
pthread_detach(myThread);
pthread_exit(NULL);
return 0;
}
如果删除pthread_exit(NULL)
的话,主线程和子线程会一块退出,这个不讨论
输出:
chen@ubuntu:~/Desktop/test_fork$ ./test_fork
father thread pid=7897
child thread pid=7898
child thread pid=7898
child thread pid=7898
child thread pid=7898
child thread pid=7898
child thread pid=7898
child thread pid=7898
child thread pid=7898
child thread pid=7898
child thread pid=7898
father thread reach trace point
child thread pid=7898
child thread pid=7898
查看pid下面的tid、tgid
chen@ubuntu:~/Desktop/test_fork$ ps --pid 7897 -O tgid,tid,lwp,nlwp -L
PID TGID TID LWP NLWP S TTY TIME COMMAND
7897 7897 7897 7897 2 S pts/0 00:00:00 ./test_fork
7897 7897 7898 7898 2 S pts/0 00:00:00 ./test_fork
在输出“father thread reach trace point”之后,再查看
chen@ubuntu:~/Desktop/test_fork$ ps --pid 7897 -O tgid,tid,lwp,nlwp -L
PID TGID TID LWP NLWP S TTY TIME COMMAND
7897 7897 7897 7897 2 Z pts/0 00:00:00 [test_fork] <defunct>
7897 7897 7898 7898 2 S pts/0 00:00:00 ./test_fork
主线程后面多了一个<defunct>标志(僵尸进程)
如何理解这个僵尸进程呢?
- 先说僵尸进程的定义:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程
- 那么明明7898是7897 fork出来的,怎么7897反而成了僵尸呢?这么理解:bash作为ppid创建了主线程,然后主线程创建了子线程,尽管主线程运行完了,但是子线程还在运行,所以主线程的task_struct等资源不能回收。所以主线程处在既不能回收又不能被父进程waitpid()的尴尬境地,按照定义,运行完又没被父进程处理,那这就属于僵尸进程了。
参考文章
pid、tgid、ppid的关系: https://www.baeldung.com/linux/pid-tid-ppid
return和pthread_exit的区别: http://c.biancheng.net/view/8608.html
孤儿进程、僵尸进程: https://www.cnblogs.com/anker/p/3271773.html