深入了解linux系统—— 线程控制
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());// 函数returnreturn (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
:要等待线程的IDvoid **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;elsestd::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/uclibc
的pthread
库接口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
线程栈就是进程地址空间上的栈区域。
到这里本篇文章内容就结束了,感谢支持