Linux系统编程8(守护进程,线程)
01. 终端的概念(了解)
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。
默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl+C表示SIGINT,Ctrl+\表示SIGQUIT。
函数说明:
#include <unistd.h>
char *ttyname(int fd);
功能:由文件描述符查出对应的文件名
参数:
fd:文件描述符
返回值:
成功:终端名
失败:NULL
示例函数:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("fd 0: %s\n",ttyname(0));
printf("fd 1: %s\n",ttyname(1));
printf("fd 2: %s\n",ttyname(2));
return 0;
}
示例函数运行结果:
02. 进程组概念(理解)
2.1 进程组概述
进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。
每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID为第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID为其进程ID
可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死:
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
一个进程可以为自己或子进程设置进程组ID。
2.2 相关函数说明
#include <unistd.h>
pid_t getpgrp(void); /* POSIX.1 version */
功能:获取当前进程的进程组ID
参数:无
返回值:总是返回调用者的进程组ID
pid_t getpgid(pid_t pid);
功能:获取指定进程的进程组ID
参数:
pid:进程号,如果pid = 0,那么该函数作用和getpgrp一样
返回值:
成功:进程组ID
失败:-1
int setpgid(pid_t pid, pid_t pgid);
功能:
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
参数:
将参1对应的进程,加入参2对应的进程组中
返回值:
成功:0
失败:-1
03. 会话(了解)
3.1 会话概念
会话是一个或多个进程组的集合。
一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;
建立与控制终端连接的会话首进程被称为控制进程;
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;
如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组;
如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。
3.2 创建会话注意事项
-
调用进程不能是进程组组长,该进程变成新会话首进程(session header)
-
该调用进程是组长进程,则出错返回
-
该进程成为一个新进程组的组长进程
-
需有root权限(ubuntu不需要)
-
新会话丢弃原有的控制终端,该会话没有控制终端
-
建立新会话时,先调用fork, 父进程终止,子进程调用setsid
3.3 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
04. 守护进程(重点)
4.1 守护进程介绍
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。
Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
4.2 守护进程模型
- 创建子进程,父进程退出(必须)
所有工作在子进程中进行形式上脱离了控制终端
- 在子进程中创建新会话(必须)
setsid()函数
使子进程完全独立出来,脱离控制
- 改变当前目录为根目录(不是必须)
chdir()函数
防止占用可卸载的文件系统
也可以换成其它路径
- 重设文件权限掩码(不是必须)
umask()函数
防止继承的文件创建屏蔽字拒绝某些权限
增加守护进程灵活性
- 关闭文件描述符(不是必须)
继承的打开文件不会用到,浪费系统资源,无法卸载
- 开始执行守护进程核心工作(必须)
守护进程退出处理程序模型
4.3 守护进程参考代码
写一个守护进程, 每隔10s获取一次系统时间, 将这个时间写入到磁盘文件:
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <stdlib.h>
/*
*time_t rawtime;
*time(&rawtime);--------获取时间,以秒计算,从1970年1月1日,存于rawtime
*localtime(&rawtime);---转换为当地时间,tm时间结构
*asctime()--------------转换为标准ASCII时间格式
* */
void write_time(int num)
{
time_t rawtime;
struct tm* timeinfo;
//获取时间
time(&rawtime);
#if 0
//转换为本地时间
timeinfo = localtime(&rawtime);
//转换为标准ASCII时间格式
char *cur = asctime(timeinfo);
#else
char *cur = ctime(&rawtime);
#endif
//将得到的时间写入文件中
int fd = open("./test.txt", O_RDWR | O_CREAT | O_APPEND, 0644);
if(-1 == fd)
{
perror("open error\n");
exit(1);
}
//写文件
int ret = write(fd, cur, strlen(cur) + 1);
if(-1 == ret)
{
perror("write error\n");
exit(1);
}
//close file
close(fd);
}
int main()
{
pid_t pid = fork();
if(-1 == pid)
{
perror("fork error\n");
exit(1);
}
if(pid > 0)
{
//father prossess exit
exit(1);
}
else if(pid == 0)
{
//sub prossess,提升为会长,同时也是新进程组的组长
setsid();
//更改进程的执行目录
chdir("./test1.txt");
//更改掩码
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 = 10;
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;
}
运行结果(当前目录下会自动新建一个名为test.txt文件):
运行结果在test.txt中。
05. 线程简介
5.1 线程概念
在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。
所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。
为了让进程完成一定的工作,进程必须至少包含一个线程。
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位。
线程存在与进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。
进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。
进程是操作系统分配资源的最小单位
线程是操作系统调度的最小单位
7.2 线程函数列表安装
命令:
ubuntu下
sudo apt-get install manpages-posix-dev
centos下
sudo yum install manpages-posix-dev
【说明】manpages-posix-dev 包含 POSIX 的 header files 和 library calls 的用法
查看:
man -k pthread
5.3 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
5.4 线程的特点
类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。
因此在这类系统中,进程和线程关系密切:
- 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
- 从内核里看进程和线程是一样的,都有各自不同的PCB.
- 进程可以蜕变成线程
- 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
查看指定进程的LWP号:
ps -Lf pid
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。
Ø 如果复制对方的地址空间,那么就产出一个“进程”;
Ø 如果共享对方的地址空间,就产生一个“线程”。
Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_xxx是库函数,而非系统调用。
5.5 线程共享资源
文件描述符表
每种信号的处理方式
当前工作目录
用户ID和组ID
内存地址空间 (.text/.data/.bss/heap/共享库)
5.6 线程非共享资源
线程id
处理器现场和栈指针(内核栈)
独立的栈空间(用户空间栈)
errno变量
信号屏蔽字
调度优先级
5.7 线程的优缺点
优点:
Ø 提高程序并发性
Ø 开销小
Ø 数据通信、共享数据方便
缺点:
Ø 库函数,不稳定
Ø 调试、编写困难、gdb不支持
Ø 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
06. 线程常用操作
6.1 线程号
就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。
进程号用 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
示例代码:
#include <stdio.h>
#include <pthread.h>
int main()
{
pthread_t thread_id;
thread_id = pthread_self(); //return ID of used thread
printf("Thread ID = %1u \n", thread_id);
if(0 != pthread_equal(thread_id, pthread_self()))
{
printf("Equal!!!\n");
}
else
{
printf("Not equal!!!\n");
}
return 0;
}
~
运行结果:
6.2 线程的创建
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 <pthread.h>
#include <unistd.h>
#include <string.h>
//return function
void *thread_fun(void *arg)
{
sleep(1);
int num = *((int *)arg);
printf("int the new thread: num = %d\n", num);
return NULL;
}
int main()
{
pthread_t tid;
int test = 100;
//return num of error
int ret = pthread_create(&tid, NULL, thread_fun, (void *)&test);
if(ret != 0)
{
printf("error number: %d\n",ret);
//according to num of error print information
printf("error information: %s\n",strerror(ret));
}
while(1);
return 0;
}
运行结果(编译需要连接时需要使用库libpthread.a,加上 -lpthread ):
6.3 线程资源回收
pthread_join函数:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:
等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:
thread:被等待的线程号。
retval:用来存储线程退出状态的指针的地址。
返回值:
成功:0
失败:非 0
示例程序:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
//return function
void *thread(void *arg)
{
static int num = 123;
printf("after 2 secenod,thread will return!!!\n");
sleep(2);
return #
}
int main()
{
pthread_t tid;
int ret = 0;
void *value = NULL;
//create thread
pthread_create(&tid, NULL, thread, NULL);
//Wait thread of id is tid
pthread_join(tid, &value);
printf("value = %d\n", *((int *)value));
return 0;
}
运行结果:
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
-
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
-
如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
-
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
6.4 线程分离
一般情况下,线程终止后,其终止状态一直保留到其它线程调用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
6.5 线程退出
在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。
线程从执行函数中返回。
线程调用pthread_exit退出线程。
线程可以被同一进程中的其它线程取消。
pthread_exit函数:
#include <pthread.h>
void pthread_exit(void *retval);
功能:
退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
参数:
retval:存储线程退出状态的指针。
返回值:无
示例代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
//return function
void *thread(void *arg)
{
static int num = 123;
int i = 0;
while(1)
{
printf("I am runing\n");
sleep(1);
i++;
if(i == 3)
{
pthread_exit((void *)&num);
}
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = 0;
void *value = NULL;
//create thread
pthread_create(&tid, NULL, thread, NULL);
//Wait thread of id is tid
pthread_join(tid, &value);
printf("value = %d\n", *((int *)value));
return 0;
}
运行结果:
6.6 线程取消
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:
杀死(取消)线程
参数:
thread : 目标线程ID。
返回值:
成功:0
失败:出错编号
注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。
杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
可粗略认为一个系统调用(进入内核)即为一个取消点。
示例程序:
void *thread_cancel(void *arg)
{
while (1)
{
pthread_testcancel(); //设置取消点
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_cancel, NULL); //创建线程
sleep(3); //3秒后
pthread_cancel(tid); //取消tid线程
pthread_join(tid, NULL);
return 0;
07. 线程属性(了解)
7.1 概述
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;
主要结构体成员:
-
线程分离状态
-
线程栈大小(默认平均分配)
-
线程栈警戒缓冲区大小(位于栈末尾)
-
线程栈最低地址
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。
线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
7.2 线程属性初始化和销毁
#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
失败:错误号
7.3 线程分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当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()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
7.4 线程栈地址
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两个函数分别设置和获取线程的栈地址。
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
功能:设置线程的栈地址
参数:
attr:指向一个线程属性的指针
stackaddr:内存首地址
stacksize:返回线程的堆栈大小
返回值:
成功:0
失败:错误号
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
功能:获取线程的栈地址
参数:
attr:指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小
返回值:
成功:0
失败:错误号
7.5 线程栈大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
#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
失败:错误号
7.6 综合参考程序
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define SIZE 0x100000
//return function
void *th_fun(void *arg)
{
while(1)
{
sleep(1);
}
}
int main()
{
pthread_t tid;
int err, detachstate, i =1;
pthread_attr_t attr;
size_t stacksize;
void * stackaddr;
pthread_attr_init(&attr); //线程属性初始化
pthread_attr_getstack(&attr, &stackaddr, &stacksize);//获取线程的栈地址
pthread_attr_getdetachstate(&attr, &detachstate); //获取线程分离状态
if(detachstate == PTHREAD_CREATE_DETACHED)
{
printf("thread detached!!\n");
}
else if(detachstate == PTHREAD_CREATE_JOINABLE)
{
printf("thread join\n");
}
else
{
printf("thread unknown\n");
}
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
while(1)
{
stackaddr = malloc(SIZE);
if(stackaddr == NULL)
{
perror("malloc");
exit(1);
}
stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize);
err = pthread_create(&tid, &attr, th_fun, NULL);
if(err != 0)
{
printf("%s\n", strerror(err));
exit(1);
}
printf("%d\n", i++);
}
pthread_attr_destroy(&attr);
return 0;
}
运行结果:
7.7 线程使用注意事项
-
主线程退出其他线程不退出,主线程应调用pthread_exit
-
避免僵尸线程
a) pthread_join
b) pthread_detach
c) pthread_create指定分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
-
malloc和mmap申请的内存可以被其他线程释放
-
应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程t在子进程中均pthread_exit
-
信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制