深入了解linux系统—— 线程控制

发布于:2025-08-18 ⋅ 阅读:(17) ⋅ 点赞:(0)

POSIX线程库

在了解过进程概念之后,我们知道在Linux操作系统中,线程是用进程来模拟实现的;

内核中的task_struct结构体对象被称为轻量级进程,所以操作系统所提供的系统调用接口都是关于轻量级进程的。

而我们想要创建线程,就要使用到pthread库。

使用pthread库,要包含头文件<pthread.h>

pthread是第三方库,在使用g++/gcc编译时要带-lpthread选项。

pthread库中,绝大多数函数接口都是以pthread_开头的;例如pthread_create创建线程、pthread_join等待线程。

线程控制

线程创建

要创建一个线程,就要调用pthread_create函数

在这里插入图片描述

可以看到pthread_create函数有4个参数:

  • 第一个参数是一个输出型参数,传递pthread_t*类型的指针,创建线程成功之后将线程ID带出来。
  • 第二个参数attr用来设置线程相关属性的,传nullptr默认设置属性。
  • 第三个参数,start_routine表示要创建的线程的入口函数(该函数的返回值和参数类型都是void*类型)
  • 第四个参数,arg表示线程在执行自己的入口函数时,要传递的参数。

返回值:

如果调用pthread_create创建线程成功,就返回0;如果创建失败,就返回对应的错误码。

在这里插入图片描述

使用pthread_create创建一个线程:

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
void *func(void *arg)
{
    std::string name = static_cast<char *>(arg);
    int cnt = 3;
    while (cnt--)
    {
        std::cout << name << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, func, (void *)"thread-1");
    // 创建线程执行func函数 , func函数的参数(void *)"thread-1"
    if (n != 0)
    {
        std::cerr << "pthread_create : " << std::endl;
        return 1;
    }
    return 0;
}

线程终止

我们能够使用pthread_create创建一个线程,那一个线程如何退出呢?

return函数返回

线程要执行对应的函数,当函数执行完成时,线程就退出了;

注意:这里调用exit是让进程退出,线程如果调用exit,其进程就会退出

pthread_exit线程退出

线程在执行时不能调用exit来终止;可以调用pthread_exit来终止进程

在这里插入图片描述

其参数void* retval,就是指返回值;当我们调用pthread_exit终止线程时就可以通过参数将退出信息返回。

pthread_cancel取消线程

上述的return返回、pthread_exit终止线程都是线程自己终止;此外,我们也可以调用pthread_cancel来主动取消线程。

在这里插入图片描述

参数:pthrad_t thread表示要取消线程的id(pthread_create拿到的线程id)

返回值:如果调用pthread_cancel取消线程成功,返回0;失败则返回错误码

补充:pthread_self获取当前线程ID。

主线程(main)调用pthread_create创建新线程,可以获得新线程ID;main线程可以调用pthread_self获取自己的线程ID;

新线程也可以调用pthread_cancel来取消自己。(可以调用pthread_self来获取当前线程的ID)

void *func(void *arg)
{
    std::string name = static_cast<char *>(arg);
    int cnt = 3;
    while (cnt--)
    {
        std::cout << name << std::endl;
        sleep(1);
    }
    // 线程退出
    pthread_exit((void *)"pthread_exit");
    // 线程调用pthread_cancel取消
    pthread_cancel(pthread_self());
    // 函数return
    return (void *)"return";
}
int main()
{
    pthread_t tid;
    //创建新线程
    int n = pthread_create(&tid, nullptr, func, (void *)"thread1");
    if (n != 0)
    {
        std::cerr << "pthread_create" << std::endl;
        return -1;
    }
    sleep(1);
    // main线程取消新线程
    pthread_cancel(tid);
    return 0;
}

线程等待

pthread_join

我们创建出来线程,让线程执行指定的函数;

就和创建子进程一样,创建出来要去执行某种任务,那我们要知道任务执行的结果吧;并且如果不处理子进程退出还会造成僵尸进程,那线程呢?

线程也是如此,我们需要获取新线程执行的结果,并且回收线程防止资源泄露问题。

pthread_join等待某个线程。

在这里插入图片描述

int pthread_join(pthread_t thread, void **retval);

参数pthread_join存在两个参数

  • pthread_t thread:要等待线程的ID
  • void **retval:输出型参数,获取线程退出时的返回值(return或者pthread_exit的参数)

返回值:如果等待线程成功就返回0,失败则返回对应的错误码

Linux系统中,ps指令-L选项显示线程的关键参数、-f选项显示完整信息、-T选项显示线程

在这里插入图片描述

对于void **retval可以获取线程退出时的返回值,获取return返回值:

void* func(void* msg)
{
    std::string name = static_cast<char*>(msg);
    int cnt = 3;
    while(cnt--)
    {
        std::cout<<name<<std::endl;
        sleep(1);
    }
    return (void*)"thread return";
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,func,(void*)"thread-1");
    char* ret = nullptr;
    pthread_join(tid, (void**)&ret);
    std::cout<<ret<<std::endl;
    return 0;
}

在这里插入图片描述

此外,如果线程是调用pthread_exit退出的,pthread_join获取的就是pthread_exit的返回值

void *func(void *msg)
{
    std::string name = static_cast<char *>(msg);
    int cnt = 3;
    while (cnt--)
    {
        std::cout << name << std::endl;
        sleep(1);
    }
    pthread_exit((void *)"thread pthread_exit");
    // return (void*)"thread return";
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func, (void *)"thread-1");
    sleep(5);
    char *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    std::cout << ret << std::endl;
    return 0;
}

在这里插入图片描述

线程分离

pthread_detach

在创建子进程时,就要通过父进程调用wait/waitpid来回收子进程;

如果我们不需要子进程的退出信息,就可以将进程对SIGCHLD(17号信号)的处理方式设置为SIGCHLD,这样子进程在退出时,系统给父进程发送SIGCHLD号信号,父进程忽视处理,就不需要调用wait/waitpid子进程就会被回收。

那线程呢?如果我们不需要线程的退出信息,也不想要调用pthread_join来回收新线程,那我们就可以调用pthread_detach设置线程分离状态;分离状态下的线程,退出后不能获取其退出信息(内核数据结构在线程退出后就回收了)

void *func(void *msg)
{
    std::string name = static_cast<char *>(msg);
    int cnt = 3;
    while (cnt--)
    {
        std::cout << name << std::endl;
        sleep(1);
    }
    pthread_exit((void *)"thread pthread_exit");
    // return (void*)"thread return";
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func, (void *)"thread-1");
    pthread_detach(tid);
    sleep(5);
    char *ret = nullptr;
    int n = pthread_join(tid, (void **)&ret);
    if (n == 0)
        std::cout << ret << std::endl;
    else
        std::cout << "thread detach" << std::endl;
    return 0;
}

在这里插入图片描述

线程ID

在上述使用pthread库,进行线程控制的过程中,貌似都是通过线程ID来对线程进行相关的操作;但是线程ID是什么呢?在Linux系统中不是没有线程这一概念吗,线程ID是轻量级进程ID吗?ps -aL显示的线程属性中LWP又是什么呢?

在这里插入图片描述

很显然,ps -aL显示的线程属性中,PID指的是进程ID,LWP指的是内核轻量级进程ID;

那线程ID是LWP吗?

void *func(void *msg)
{
    std::cout << "new thread id : " << pthread_self() << std::endl;
    sleep(1);
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func, nullptr);
    sleep(3);
    std::cout << "main thread id : " << pthread_self() << std::endl;
    pthread_join(tid, nullptr);
    return 0;
}

这里输出线程ID 看是否和LWP相等

在这里插入图片描述

可以看到线程ID和内核中LWP是不一样的。

输出出来的线程ID是一个很大的数,对于目前Linux实现的NPTL而言,pthread_t类型的的线程ID,本质上就是一个进程地址空间上的一个地址。

在上述创建线程的过程中,我们可以发现当线程运行结束,无论main线程是否调用pthread_join等待新线程,新线程对应的轻量级进程都会被回收;那不等待新线程造成内存泄露,丢失的是哪一部分内存呢?线程的返回值又是如何拿到的呢?

我们程序可以调用pthread_create创建线程,其他程序也可以;那在内存中就存在非常多的线程,有的刚刚创建、有的正在运行、有的即将释放,那肯定要将这些线程管理起来;管理:先描述再组织

所以说,在内存中也存在描述线程相关的数据结构,这些数据结构被管理起来。

那这些数据结构在哪里呢?

mmap动态映射区/共享区。这里就像C语言文件操作那样,在库中构建了一个FILE类型返回给上层。

在这里插入图片描述

线程栈

对于线程,虽然说在linux系统中,进程和线程的统一使用task_struct,但是访问进程地址空间还是存在区别的。

我们知道进程地址中,只存在一个栈区,线程要执行自己的代码,那肯定是要有自己对应的栈的。

对于Linux进程(主线程),就是main函数的栈;在fork时,本质上就是复制了父进程的stack空间地址,然后进行写时拷贝以及动态增长;通过扩充超出该上限就会栈溢出,(段错误)发送段错误信号给进程。

对于子线程 ,其stack不再是向下增长的,是事先固定下来的;线程栈一般是调用glibc/uclibcpthread库接口pthread_create创建的线程,在共享区。

mem = mmap (NULL, size, prot, 
	MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); 

这里mmap调用中的size参数比较复杂,可以自己传入stack的大小,也可以使用默认的,一般默认就是8M.

这种stack不能动态增长,用尽之后就没有了;glibc中调用mmap获得栈后,底层会调用sys_clone

int sys_clone(struct pt_regs *regs)
{
    unsigned long clone_flags;
    unsigned long newsp;
    int __user *parent_tidptr, *child_tidptr;

    clone_flags = regs->bx;
    // 获取了mmap得到的线程的stack指针
    newsp = regs->cx;
    parent_tidptr = (int __user *)regs->dx;
    child_tidptr = (int __user *)regs->di;
    if (!newsp)
        newsp = regs->sp;
    return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}

所以,对于子线程的栈,本质上是在进程地址空间中map出的一块内存区域;而main线程栈就是进程地址空间上的栈区域。

到这里本篇文章内容就结束了,感谢支持


网站公告

今日签到

点亮在社区的每一天
去签到