会话(了解)
会话概念
会话是一个或多个进程组的集合。
- 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;
- 建立与控制终端连接的会话首进程被称为控制进程;
- 一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;
- 如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组;
- 如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。
创建会话注意事项
1) 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2) 该调用进程是组长进程,则出错返回
3) 该进程成为一个新进程组的组长进程
4) 需有root权限(ubuntu不需要)
5) 新会话丢弃原有的控制终端,该会话没有控制终端
6) 建立新会话时,先调用fork, 父进程终止,子进程调用setsid
API函数介绍
getsid函数:
#include <unistd.h>
pid_t getsid(pid_t pid);
功能:获取进程所属的会话ID
参数:
pid:进程号,pid为0表示查看当前进程session ID
返回值:
成功:返回调用进程的会话ID
失败:-1
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
setsid函数:
#include <unistd.h>
pid_t setsid(void);
功能:
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。调用了setsid函数的进程,既是新的会长,也是新的组长。
参数:无
返回值:
成功:返回调用进程的会话ID
失败:-1
守护进程(重点)
守护进程介绍
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。
Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
守护进程模型
1) 创建子进程,父进程退出(必须)
- 所有工作在子进程中进行形式上脱离了控制终端
2) 在子进程中创建新会话(必须)
- setsid()函数
- 使子进程完全独立出来,脱离控制
3) 改变当前目录为根目录(不是必须)
- chdir()函数
- 防止占用可卸载的文件系统
- 也可以换成其它路径
4) 重设文件权限掩码(不是必须)
- umask()函数
- 防止继承的文件创建屏蔽字拒绝某些权限
- 增加守护进程灵活性
5) 关闭文件描述符(不是必须)
- 继承的打开文件不会用到,浪费系统资源,无法卸载
6) 开始执行守护进程核心工作(必须)
守护进程退出处理程序模型
为什么必须在子进程中创建新会话?
1. 脱离原控制终端
- 守护进程的核心要求:必须完全脱离任何终端,避免因终端关闭(如 SSH 断开)收到
SIGHUP
等信号而意外退出。 -
setsid()
的作用:创建新会话后,子进程与原终端解绑,成为独立的后台进程。
2. 规避会话组长限制
- 系统强制规则:只有非会话组长的进程能调用
setsid()
。 - 父进程通常是会话组长(如 Shell 启动的进程),直接调用
setsid()
会失败(errno=EPERM
)。 -
fork()
后的子进程:默认继承父进程的会话 ID,但不是会话组长**,因此可以成功调用setsid()
。
3. 实现真正的进程独立性
- 新会话的特性:
- 子进程成为新会话的首进程(Session Leader)。
- 拥有独立的进程组 ID(PGID),与原进程组隔离。
- 不再受原终端的作业控制(如
Ctrl+C
、SIGHUP
影响)。
一句话总结:
必须在子进程中调用 setsid()
,因为只有非会话组长的进程才能成功创建新会话,从而彻底脱离原终端和会话,实现守护进程的完全后台独立性。
守护进程参考代码
写一个守护进程, 每隔2s获取一次系统时间, 将这个时间写入到磁盘文件:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#include<sys/types.h>
#include<time.h>
//写一个守护进程, 每隔2s获取一次系统时间, 将这个时间写入到磁盘文件
void write_time(int num)
{
time_t rawtime;
struct tm * timeinfo;
//获取时间
time(&rawtime);
#if 1
//转为本地时间
timeinfo = localtime(&rawtime);
//转为标准ASCII时间格式
char *cur = asctime(timeinfo);
#else
char* cur = ctime(&rawtime);
#endif
//将得到的时间写入文件中
int fd = open("/home/little-hu/timelog.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
if (fd == -1)
{
perror("open error");
exit(1);
}
//写文件
int ret = write(fd, cur, strlen(cur) + 1);
if (ret == -1)
{
perror("write error");
exit(1);
}
//关闭文件
close(fd);
}
int main(int argc, const char* argv[])
{
pid_t pid = fork();
if (pid == -1)
{
perror("fork error");
exit(1);
}
if (pid > 0)
{
//父进程退出
exit(1);
}
else if (pid == 0)
{
//子进程
//提升为会长,同时也是新进程组的组长
setsid();
//更改进程的执行目录
chdir("/home/little-hu");
//更改掩码
umask(0022);
//关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//注册信号捕捉函数
//先注册,再定时
struct sigaction sigact;
sigact.sa_flags = 0;
sigemptyset(&sigact.sa_mask);
sigact.sa_handler = write_time;
sigaction(SIGALRM, &sigact, NULL);
//设置定时器
struct itimerval act;
//定时周期
act.it_interval.tv_sec = 2;
act.it_interval.tv_usec = 0;
//设置第一次触发定时器时间
act.it_value.tv_sec = 2;
act.it_value.tv_usec = 0;
//开始计时
setitimer(ITIMER_REAL, &act, NULL);
//防止子进程退出
while (1);
}
return 0;
}
结果:
线程简介
线程概念
在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。
所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。
为了让进程完成一定的工作,进程必须至少包含一个线程。
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位。
线程存在与进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。
进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。
进程是操作系统分配资源的最小单位
线程是操作系统调度的最小单位
线程函数列表安装
命令:
sudo apt-get install manpages-posix-dev
【说明】manpages-posix-dev 包含 POSIX 的 header files 和 library calls 的用法
查看:
man -k pthread
NPTL
当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合 POSIX 的要求。
要改进 LinuxThreads,非常明显我们需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation POSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了 NPTL。
NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。
查看当前pthread库版本:getconf GNU_LIBPTHREAD_VERSION
线程的特点
类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。
因此在这类系统中,进程和线程关系密切:
1) 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
2) 从内核里看进程和线程是一样的,都有各自不同的PCB.
3) 进程可以蜕变成线程
4) 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
查看指定进程的LWP号:
ps -Lf pid
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。
- 如果复制对方的地址空间,那么就产出一个“进程”;
- 如果共享对方的地址空间,就产生一个“线程”。
Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
线程共享资源*
1) 文件描述符表
2) 每种信号的处理方式
3) 当前工作目录
4) 用户ID和组ID
内存地址空间 (.text/.data/.bss/heap/共享库)
线程非共享资源*
1) 线程id
2) 处理器现场和栈指针(内核栈)
3) 独立的栈空间(用户空间栈)
4) errno变量
5) 信号屏蔽字
6) 调度优先级
线程的优缺点*
优点:
- 提高程序并发性
- 开销小
- 数据通信、共享数据方便
缺点:
- 库函数,不稳定
- 调试、编写困难、gdb不支持
- 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
线程常用操作
线程号
就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。
进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。
有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。
pthread_self函数:
#include <pthread.h>
pthread_t pthread_self(void);
功能:
获取线程号。
参数:
无
返回值:
调用线程的线程 ID 。
pthread_equal函数:
int pthread_equal(pthread_t t1, pthread_t t2);
功能:
判断线程号 t1 和 t2 是否相等。为了方便移植,尽量使用函数来比较线程 ID。
参数:
t1,t2:待判断的线程号。
返回值:
相等: 非 0
不相等:0
【注意】线程函数的程序在 pthread 库中,故链接时要加上参数 -lpthread。
线程的创建
pthread_create函数:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg );
功能:
创建一个线程。
参数:
thread:线程标识符地址。
attr:线程属性结构体地址,通常设置为 NULL。
start_routine:线程函数的入口地址。
arg:传给线程函数的参数。
返回值:
成功:0
失败:非 0
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。
由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。
参考程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
void *tfn(void *arg){
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[]){
pthread_t tid;
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
perror("pthread_create error");
}
sleep(1);
return 0;
}
结果:
循环创建多个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
//循环创建多个子线程
//函数错误码返回
void sys_err(const char *str){
perror(str);
exit(1);
}
//线程创建的回调函数,打印进程id和线程id
void *tfn(void *arg){
int i = (int)arg;
sleep(i);
printf("--I'm %dth thread: pid = %d, tid = %lu\n",i+1, getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[]){
int i;
int ret;
pthread_t tid;
//循环创建五个线程
for(i=0;i<5;i++){
ret = pthread_create(&tid, NULL, tfn, (void *)i);//这里传参采用值传递,借助强转
if (ret != 0) {
sys_err("pthread_create error");
}
}
sleep(i);
//获取主函数进程号和线程号
printf("I'm main, pid = %d, tid = %lu\n", getpid(), pthread_self());
return 0;
}
结果:
线程资源回收
pthread_join函数:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:
等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:
thread:被等待的线程号。
retval:用来存储线程退出状态的指针的地址。
返回值:
成功:0
失败:非 0
参考程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
//回收线程并获取子线程返回值的小例子
struct thrd {
int var;
char str[256];
};
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn(void *arg)
{
struct thrd *tval;
tval = malloc(sizeof(tval));
tval->var = 100;
strcpy(tval->str, "hello thread");
return (void *)tval;
}
int main(int argc, char *argv[])
{
pthread_t tid;
struct thrd *retval;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0)
sys_err("pthread_create error");
//int pthread_join(pthread_t thread, void **retval);
//retval:用来存储线程退出状态的指针的地址。
ret = pthread_join(tid, (void **)&retval);
if (ret != 0)
sys_err("pthread_join error");
printf("child thread exit with var= %d, str= %s\n", retval->var, retval->str);
pthread_exit(NULL);
}
结果:
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1) 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2) 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
3) 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
线程分离
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。
不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
pthread_detach函数:
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:
使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:
thread:线程号。
返回值:
成功:0
失败:非0
参考程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *tfn(void *arg)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(1);
}
ret = pthread_detach(tid); // 设置线程分离` 线程终止,会自动清理pcb,无需回收
if (ret != 0) {
fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
exit(1);
}
sleep(1);
ret = pthread_join(tid, NULL);
printf("join ret = %d\n", ret);
if (ret != 0) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void *)0);
}
结果:
线程退出
在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。
-
线程从执行函数中返回。
-
线程调用pthread_exit退出线程。
-
线程可以被同一进程中的其它线程取消。
pthread_exit函数:
#include <pthread.h>
void pthread_exit(void *retval);
功能:
退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
参数:
retval:存储线程退出状态的指针。
返回值:无
线程取消
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:
杀死(取消)线程
参数:
thread : 目标线程ID。
返回值:
成功:0
失败:出错编号
注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。
杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
可粗略认为一个系统调用(进入内核)即为一个取消点。
参考程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
//主线程调用pthread_cancel杀死子线程
void *tfn(void *arg){
while (1) {
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(1);
}
return NULL;
}
int main(int argc, char *argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(5);
ret = pthread_cancel(tid); // 终止线程,但回收资源需使用pthread_join和 detach才行
if (ret != 0) {
fprintf(stderr, "pthread_cancel error:%s\n", strerror(ret));
exit(1);
}
while (1);
pthread_exit((void *)0);
}
结果:
线程属性(了解)
概述
Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。
如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
} pthread_attr_t;
主要结构体成员:
1) 线程分离状态
2) 线程栈大小(默认平均分配)
3) 线程栈警戒缓冲区大小(位于栈末尾)
4) 线程栈最低地址
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。
线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
线程属性初始化和销毁
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
功能:
初始化线程属性函数,注意:应先初始化线程属性,再pthread_create创建线程
参数:
attr:线程属性结构体
返回值:
成功:0
失败:错误号
销毁线程属性所占用的资源函数:
int pthread_attr_destroy(pthread_attr_t *attr);
功能:
销毁线程属性所占用的资源函数
参数:
attr:线程属性结构体
返回值:
成功:0
失败:错误号
线程分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
-
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
-
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
相关函数:
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
功能:设置线程分离状态
参数:
attr:已初始化的线程属性
detachstate: 分离状态
PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD_CREATE_JOINABLE(非分离线程)
返回值:
成功:0
失败:非0
获取线程分离状态:
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
功能:获取线程分离状态
参数:
attr:已初始化的线程属性
detachstate: 分离状态
PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD _CREATE_JOINABLE(非分离线程)
返回值:
成功:0
失败:非0
这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。
要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。
设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
线程栈地址
POSIX.1定义了两个常量来检测系统是否支持栈属性:
-
_POSIX_THREAD_ATTR_STACKADDR
-
_POSIX_THREAD_ATTR_STACKSIZE
也可以给sysconf函数传递来进行检测:
-
_SC_THREAD_ATTR_STACKADDR
-
_SC_THREAD_ATTR_STACKSIZE
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。
pthread_attr_setstack:
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
功能:设置线程的栈地址
参数:
attr:指向一个线程属性的指针
stackaddr:内存首地址
stacksize:返回线程的堆栈大小
返回值:
成功:0
失败:错误号
pthread_attr_getstack:
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
功能:获取线程的栈地址
参数:
attr:指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小
返回值:
成功:0
失败:错误号
线程栈大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
设置线程的栈大小:
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
功能:设置线程的栈大小
参数:
attr:指向一个线程属性的指针
stacksize:线程的堆栈大小
返回值:
成功:0
失败:错误号
获取线程的栈大小:
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
功能:获取线程的栈大小
参数:
attr:指向一个线程属性的指针
stacksize:返回线程的堆栈大小
返回值:
成功:0
失败:错误号
综合参考程序
应用场景,如果你创建了500个线程,那么是否要手动回收500次呢,这样就显得很繁琐,此时就可以设置分离属性,使线程创建出来就是分离态,由系统自动回收
线程分离代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *tfn(void *arg)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
pthread_attr_t attr;//创建一个线程属性结构体变量
int ret = pthread_attr_init(&attr);//初始化线程属性
if (ret != 0) {
fprintf(stderr, "attr_init error:%s\n", strerror(ret));
exit(1);
}
// 设置线程属性为 分离属性
ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (ret != 0) {
fprintf(stderr, "attr_setdetachstate error:%s\n", strerror(ret));
exit(1);
}
ret = pthread_create(&tid, &attr, tfn, NULL);//借助修改后的 设置线程属性 创建为分离态的新线程
if (ret != 0) {
perror("pthread_create error");
}
ret = pthread_attr_destroy(&attr);//销毁线程属性
if (ret != 0) {
fprintf(stderr, "attr_destroy error:%s\n", strerror(ret));
exit(1);
}
ret = pthread_join(tid, NULL);
if (ret != 0) {
//pthread_join报错,说明线程已经自动回收,设置分离成功。
fprintf(stderr, "pthread_join error:%s\n", strerror(ret));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void *)0);
}
结果:
线程使用注意事项
1) 主线程退出其他线程不退出,主线程应调用pthread_exit
2) 避免僵尸线程
a) pthread_join
b) pthread_detach
c) pthread_create指定分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
3) malloc和mmap申请的内存可以被其他线程释放
4) 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程t在子进程中均pthread_exit
5) 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制