美文网首页
linux用户空间 - 多线程的生命周期

linux用户空间 - 多线程的生命周期

作者: 404Not_Found | 来源:发表于2021-07-06 12:38 被阅读0次
  • 作者: 雪山肥鱼
  • 时间:20210706 22:41z
  • 目的:多线程的生命周期
# 多线程 vs 多进程
# 多线程举例
# 多线程本质
  ## 更高的posix标准
# 线程的资源回收 pthread_join
# 线程的退出引发的问题
  ## 代码举例
  ## 关于pthread_cansle 与 pthread_cleanup_pop/push

多线程 vs 多进程

进程是资源封装的单位。所以一个进程是看不到另一个进程的资源的。在进程之间的通讯中,关注的是,数据的交互。

  • 小量数据 socket
  • 大量数据 走 共享内存
    很明显多进程的好处是进程之间的隔离

在多线程中,线程只是调度单元,同步也好,互斥也好。并不涉及数据的交互,因为在多线程模型中,数据已共享,数据均可被所有线程共享。此时内存本来就是共享的。

多线程的通信开销,是远低于多进程的

那么多线程面对的问题是:

  • 一个线程挂,则进程挂
  • 一个线程造成的 memory leak, 可能死的是另一个线程。定位问题非常困难。

关系亲密的(数据层)用多线程模型,关系一般的用多进程

多线程举例

#define THREAD_NUMBER 2
int retval_hello = 2, retval_hello2 = 3;

void * hello1(void * arg) {
   char * hello_str = (char*arg);
   sleep(1);
   printf("%s\n", hello_str);
   //exit proccess exit
   pthread_exit(&retval_hello1);
}

void * hello2(void * arg) {
    char * hello_str = (char*arg);
    sleep(1);
    printf("%s\n", hello_str);
    //exit proccess exit
    pthread_exit(&retval_hello2);
}

int main(int argc, char ** argv) {
    int i;
    int ret_val;
    int * retval;
    int * retval_hello[2];

    pthread_t pt[THREAD_NUMBER];
    const char * arg[THREAD_NUMBER];
    arg[0] = "hello world from thread1";
    arg[1] = "hello world from thread2";
    printf("Begin to create threads...\n");

    ret_val = pthread_create(&pt[0], NULL, hello1, (void*)arg[0]);
    if(ret_val != 0) {
        printf("pthread_create error!\n");
        exit(1);
    }

    ret_val = pthread_create(&pt[1], NULL, hello2, (void*)arg[1]);
    if(ret_val != 0) {
        printf("pthread_create error!\n");
        exit(1);
    }

    printf("Now, the main thread returns\n");
    printf("Begin to wait for threads..\n");
    for(i =0; i < THREAD_NUMBER; i++) {
        //类似 waitpid()
        ret_val = pthread_join(pt[i], (void **)&retval_hello[i]);
    }
    return 0;
}

exit 是进程级别的api, 调用就整个进程退出了

多线程本质

pthread_create -> clone


资源共享.png

clone 与fork 的本质就是 p1 p2 资源共享,但是有两个不同的pid, 即两个不同的调度单元,tast_struct.

更高的 POSIX 标准

只是用上述从资源的共享角度上描述线程,是不够的。

  1. 查看进程列表的时候,相关的一组task_struct应当被展现为列表的一个节点
    进程开了10个线程,用top命令查看 都只能查看到主线程,即只能看到一个人。tgid的概念。
  2. 发送给这个进程的信号(对应的kill 系统调用),将被对应的这一组task_struct所共享,并且被其中的任意一个线程处理
    ctrl+c 进程的所有线程都退出
  3. 发送给某个线程的信号(对应的 pthread_kill), 将只被对应的一个task_struct接受,并且由它自己来处理
    线程级别的信号,只能打断指定线程
  4. 当进程被停止或继续时(对应 SIGSTOP/SIGCONT) 信号,对应的这一组task_struct状态将改变
  5. 当进程收到一个致命信号(比如由于段错误收到的 SIGSEGV), 对应的这一组task_struct全部退出。同生死,共进退

NPTL模型

LINUX中使用 NPTL模型满足 POSIX标准

TGID

Top中看到的命令都是 TGID,为了让一个进程的所有线程看上去像是一个整体


图片.png
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <signal.h>

struct thread_param
{
    char info;
    int num;
};

#if 0
void segvSignal(int sig)
{
    printf("thread pid:%d, tid:%lu\n",getpid(), pthread_self());
}
#endif

void sighandler1(int sig)
{
    printf("%s got the signal...\n", __func__);
}

void sighandler2(int sig)
{
    printf("%s got the signal...\n", __func__);
}

void* thread_fun(void* param)
{
    struct thread_param* p;
    
    p=(struct thread_param*)param;
    int i;

    if (signal(SIGINT,sighandler2) == SIG_ERR)
        return -1;

    printf("thread pid:%d, tid:%lu\n",getpid(), pthread_self());
    for(i=0;i<p->num;i++){

#if  1 /* change to #if 1 for debugging high cpu-loading issues */
        while(1);
#else
        sleep(1);
#endif
        printf("%i: %c\n",i,p->info);
    }
        
    return NULL;
}

int main(void)
{
    pthread_t tid1,tid2;
    struct thread_param info1;
    struct thread_param info2;
    int ret;

    if (signal(SIGINT,sighandler1) == SIG_ERR)
        return -1;

    info1.info='T';
    info1.num=2000000;

    printf("main pid:%d, tid:%lu\n",getpid(), pthread_self());
    
    ret=pthread_create(&tid1,NULL,thread_fun,&info1);
    if(ret==-1){
        perror("cannot create new thread");
        return 1;
    }

    info2.info='S';
    info2.num=300000;
    
    ret=pthread_create(&tid2,NULL,thread_fun,&info2);
    if(ret==-1){
        perror("cannot create new thread");
        return 1;
    }

    sleep(1);

    //pthread_kill(pthread_self(), SIGINT);

    if(pthread_join(tid1,NULL)!=0){
        perror("call pthread_join function fail");
        return 1;
    }

    if(pthread_join(tid2,NULL)!=0){
        perror("call pthread_join function fail");
        return 1;
    }

    return 0;
}   

代码行为描述:
主进程开了2根线程,所以总共有3个线程
让两个子线程死循环,利用 top 和 htop 查看信息。

  • top 只有 一个pid 进程整体


    top.png
  • top -H 线程视角


    top -H.png
  • htop


    htop.png

现象总结:
top 只会看到tgid 的信息。
我们去 /proc/1606/ 或者 /proc/1607/ 两个文件夹中进行操作

cat /proc/1606/status
status.png pthread_self().png

这一大串数字是 libc中维护的数字,和内核没什么关系。
用户态的c库所维护的tid, 和内核没半毛钱关系,只是用户态用来标识线程。

线程级和进程级的信号

posix标准规定,线程和线程之间是可以互发信号的,所以每根线程会有自己的signalpending, 对于进程来说,会有进程级别的signalpending

  • kill 是 进程级别的
  • pthread_kill 是 线程级别的

但是 信号的行为 是进程级别的。比如
signal(SIGINT, func1);
signal(SIGINT, func2);
singal(SIGINT, func3);
对于所有线程来说,最终 SIGINT 的行为 是 func3

代码示例,依旧使用上述代码,打开 pthread_kill()

    sleep(1);
    return 0;
    pthread_kill(pthread_self(), SIGINT);

    if(pthread_join(tid1,NULL)!=0){
        perror("call pthread_join function fail");
        return 1;
    }

    if(pthread_join(tid2,NULL)!=0){
        perror("call pthread_join function fail");
        return 1;
    }

    return 0;

杀掉自己,但是 信号原来注册的 signal handler1, 但是在线程函数中 signal(SIGINT, sighandler2) 则会刷出来 signalhandler 2:


1s 后的结果.png 主线程状态.png

exit_group 的系统调用

  • 依旧是上述代码, 在主线程中,开好线程后,sleep 1s, 紧跟着 exit( ).
    ret=pthread_create(&tid2,NULL,thread_fun,&info2);
    if(ret==-1){
        perror("cannot create new thread");
        return 1;
    }
    sleep(1);
    exit(0); 
exit_group(0).png

exit(0) 转化成了 exit_group(0)
整个进程退出,代表所有线程退出

  • 当没有join子线程,直接在主线程中调用return 0; 效果和原理 同上
    ret=pthread_create(&tid2,NULL,thread_fun,&info2);
    if(ret==-1){
        perror("cannot create new thread");
        return 1;
    }

    sleep(1);
    return 0;
#if 0
    pthread_kill(pthread_self(), SIGINT);

    if(pthread_join(tid1,NULL)!=0){
        perror("call pthread_join function fail");
        return 1;
    }

    if(pthread_join(tid2,NULL)!=0){
        perror("call pthread_join function fail");
        return 1;
    }

    return 0;
#endif
}   
return 0.png
主线程的 main函数 返回,clib 会 主动调用一个 exit_grou(0) 所有线程均退出。
所以 main函数的返回 意味着整个进程的返回
  • 那么尝试在子线程中调用 exit(0)
    结果: 整个进程退出


    exitd with 0.png
  • 那么尝试在子线程中调用 return 0
    子线程退出。main函数继续 running

总结:

  1. 主线程中调用 return 0 (libc 根据返回值0,会调用 exit_group 0)和 exit(0) 最终都会走到 exit_group 0
  2. 在子线程中 调用 exit(0), 整个进程退出 不会调用 exit_group
  3. 在子线程中 调用 return 0 子线程退出。 2根线程 短暂的存留在
/proc/1939/task 下
  • 只让某个线程退出
    pthread_exit(0); 即可
    最终走的是 exit(0) 自己退出 与他人无关

线程的资源回收 pthread_join

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

void* worker (void* unusued)
{
   // pthread_detach(pthread_self());
    // Do nothing
}

int main()
{
    pthread_t thread_id;

    for(int i=0; i < 200000; i++)
    {
            if (pthread_create(&thread_id, NULL, &worker, NULL) != 0) {
            fprintf(stderr, "pthread_create failed at %d\n", i);
            perror("pthread_create");
            break;
            }
    }
    sleep(1000);
    return 0;
}

创建线程的极限,用如下命令编写,用32位编译,死的更快


死亡.png

pthread_join 会把 线程分配的tcb 等资源全部释放。

pthread_detach 不用手动调用pthread_join, 目的是让线程游离了。自己工作完全不care 其他线程。
比如 项目中 有一个debug的线程,把进程某些数据结构和状态周期性的通过socket 发给另外的syslog. 又不怎么和其他线程工作,完全游离状态,这个时候用detach.
释放的动作 都是 libc的行为。即ntpl中的行为,与内核关系不大。
工程中很少使用 detach。因为线程的进退,要我们自己搞清楚。

pthread_join 调用的是 user space 中 futex(了解就好,遇到再说)。

线程的退出可能会引起leak

不pthread_join 可能会引起leak。
众所周知,如果非detach的线程,在线程结束时,并不主动join,是会造成内存呢泄露。但我在线程中申请资源,却在线程结束时,不主动free,且主动join,依然存在mem leak。
线程死的时候,是不会释放其申请的内存资源的!因为 heap 是追随整个进程的

代码举例

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void * test() {
  //char*p = (char*)malloc(1000*sizeof(char));
  printf("ok\n");
  //free(p);
  return;
}

int main(int argc, char ** argv) {
  pthread_t tid;
  pthread_create(&tid, NULL, test, NULL);
  //pthread_join(tid, NMU
}

用 valgrind 进行内存检查

gcc -g test.c -o test -lpthread

valgrind --tool=memcheck --leak-check=full ./test
未手动phread_join.png

手动join:


手动 pthread_join.png

手动join, 但未释放heap 空间:


手动join, 但未释放heap.png

手动join, 手动释放heap:


join+free.png

所以,pthread_join 只回收与线程相关的tcb,而申请的heap空间则不管 ,因为heap追随的是整个进程

关于pthread_cancel 与 pthread_cleanup_pop/push

void pthread_cleanup_push(void(*routine)(void*), void * arg);
void pthread_cleanup_pop(int execute)

pthread_cancel 这种api就非常不靠谱。
T1 看 T2 不顺眼,可以用pthread_cancel 把T2 停掉。但是T2 被干掉之前持有 mutex,sem 和malloc一段内存,打开一个文件等。这些都没来得及回收.
所以pthread_cancel 会与 pthread_cleanup_pop/push , 以及pthread_testcancel 联合使用:
解读 man pthread_cleanup_pop/push 函数

       #include <pthread.h>
       #include <sys/types.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <errno.h>

       #define handle_error_en(en, msg) \
               do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

       static int done = 0;
       static int cleanup_pop_arg = 0;
       static int cnt = 0;

       static void
       cleanup_handler1(void *arg)
       {
           printf("Called clean-up handler 1\n");
           cnt = 0;
       }

       static void
       cleanup_handler2(void *arg)
       {
           printf("Called clean-up handler 2\n");
           cnt = 0;
       }

       static void *
       thread_start(void *arg)
       {
           time_t start, curr;

           printf("New thread started\n");

           pthread_cleanup_push(cleanup_handler, NULL);

           curr = start = time(NULL);

           while (!done) {
               pthread_testcancel();           /* A cancellation point */
               if (curr < time(NULL)) {
                   curr = time(NULL);
                   printf("cnt = %d\n", cnt);  /* A cancellation point */
                   cnt++;
                   //pthread_testcancel();           /* A cancellation point */
               }
           }

           pthread_cleanup_pop(cleanup_pop_arg);

           pthread_cleanup_push(cleanup_handler2);
           pthread_testcancel();//可有可无
           pthread_cleanup_push(1);
           return NULL;
       }

       int
       main(int argc, char *argv[])
       {
           pthread_t thr;
           int s;
           void *res;

           s = pthread_create(&thr, NULL, thread_start, NULL);
           if (s != 0)
               handle_error_en(s, "pthread_create");

           sleep(2);           /* Allow new thread to run a while */

           if (argc > 1) {
               if (argc > 2)
                   cleanup_pop_arg = atoi(argv[2]);
               done = 1;

           } else {
               printf("Canceling thread\n");
               s = pthread_cancel(thr);
               if (s != 0)
                   handle_error_en(s, "pthread_cancel");
           }

           s = pthread_join(thr, &res);
           if (s != 0)
               handle_error_en(s, "pthread_join");

           if (res == PTHREAD_CANCELED)
               printf("Thread was canceled; cnt = %d\n", cnt);
           else
               printf("Thread terminated normally; cnt = %d\n", cnt);
           exit(EXIT_SUCCESS);
       }

  • 注意点:
  1. pthread_cleanup_pop
    pthread_cleanup_push
    成对出现,如果不成对出现,编译就报错。所以只要传入参数,pop执行的函数必定会全部弹出来。
  2. pthread_cleanup_pop(int execute)
    如果execute 非零 则会弹出来一个栈(收集了所有push传入的函数)顶函数。
    execute为 0 , 则不会弹出栈顶函数,即便栈中存了多个函数
  3. pthread_testcancel
    设置取消点。pthread_cancel 只有在线程执行到 pthread_test_cancel 这个取消点,线程才会退出。有些c库函数天生有pthread_testcancel 线程取消点的
    可以设置 pthread_testcancel 不同位置,来观察cnt的值
./a.out
./a.out 1
./a.out 1 0
./a.out 1 1

试试看结果如何?同时 栈里可以增加一个 cleanup_handler2 试一试

  1. pop 和 push 保护的是这两个函数之间的函数

退出线程,再释放资源,本来就不太合理,是有风险的。push 和pop google Andorid 已经不支持了。

  • 可能会引起memory leak
  • lock no release
  • other issues

相关文章

  • OC多线程

    iOS多线程方案 技术方案简介语言线程生命周期使用频率pthread通用多线程API 适用于unix/linux/...

  • 多线程

    常用的多线程 技术方案简介语言线程生命周期使用频率pthread一套通用的多线程API,适用于Unix\Linux...

  • 中级Android开发应该了解的Binder原理

    一、基础概念 Linux的进程空间是相互隔离的。 Linux将内存空间在逻辑上划分为内核空间与用户空间。Linux...

  • Binder机制小结

    by hzwusibo 20190504 1.Linux内核基础知识: (1)用户空间/内核空间: 用户空间指的是...

  • JavaNIO-MappedByteBuffer

    内核空间与用户空间 Kernel space 是 Linux 内核的运行空间,User space 是用户程序的运...

  • Binder原理

    Linux进程划分 用户空间内核空间用户空间是不共享的空间,内核空间是共享的空间,所以两个用户空间传递数据就需要内...

  • 转载---Binder

    知识储备 Linux进程空间划分 一个进程空间分为 用户空间 & 内核空间(Kernel),即把进程内 用户 & ...

  • 专题3-嵌入式linux内核制作

    一、linux体系结构 linux由内核空间与用户空间两部分组成,用户空间主要是应用程序和C库,内核空间包含了像系...

  • Linux 下创建用户表空间.md

    linux下创建oracle用户表空间 操作步骤如下: 登录linux,以oracle用户登录(如果是root用户...

  • 从Kafka到NIO

    在谈NIO之前,简单回顾下内核态和用户态 内核空间是Linux内核运行的空间,而用户空间是用户程序的运行空间,为了...

网友评论

      本文标题:linux用户空间 - 多线程的生命周期

      本文链接:https://www.haomeiwen.com/subject/amenultx.html