Appearance
守护进程, 线程
进程组和会话
概念和特性
进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
一个进程组是一组相关的进程,它们共享同一个终端或控制台,并且可以通过进程组ID(PGID)来标识。一个进程可以创建一个新的进程组,并将其子进程分配给该组。进程组的一个常见用途是在Shell中将一组进程一起启动或停止。
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID==第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID==其进程ID
可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。
使用命令
cat | cat | cat | wc -l
可以使用
kill - 9 -3991
杀死这一个组这一个使用正的3991也可以, 这是因为这一个管道关闭, 读不到数据, 子进程结束
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
一个进程可以为自己或子进程设置进程组ID
会话
Linux session(会话) - sparkdev - 博客园 (cnblogs.com)
见的 Linux session 一般是指 shell session。Shell session 是终端中当前的状态,在终端中只能有一个 session。当我们打开一个新的终端时,总会创建一个新的 shell session。
Session 中的每个进程组被称为一个 job,有一个 job 会成为 session 的前台 job(foreground),其它的 job 则是后台 job(background)。每个 session 连接一个控制终端(control terminal),控制终端中的输入被发送给前台 job,从前台 job 产生的输出也被发送到控制终端上。同时由控制终端产生的信号,比如 ctrl + z 等都会传递给前台 job。
一般情况下 session 和终端是一对一的关系,当我们打开多个终端窗口时,实际上就创建了多个 session。
Session 的意义在于多个工作(job)在一个终端中运行,其中的一个为前台 job,它直接接收该终端的输入并把结果输出到该终端。其它的 job 则在后台运行。
会话是多个进程组的集合,
这时候会出现一个问题, 如果你希望一个进程一直在后台执行, 但是因为这一个进程的父进程(或者父进程的父进程等)是shell终端, 一旦这一个终端关闭, 会导致这一个终端里面的所有一个组里面的进程退出
解决方法: 守护进程
创建会话
创建一个会话需要注意以下6点注意事项:
- 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
- 该进程成为一个新进程组的组长进程。
- 需有root权限 (ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 该调用进程是组长进程,则出错返回
- 建立新会话时,先调用fork, 父进程终止,子进程调用setsid
getsid获取当前的sid
获取进程所属的会话ID
pid_t getsid(pid_t pid); 成功:返回调用进程的会话ID;失败:-1,设置errno
pid为0表示察看当前进程session ID
ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
setsid创建一个会话
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。
pid_t setsid(void); 成功:返回调用进程的会话ID;失败:-1,设置errno
调用了setsid函数的进程,既是新的会长,也是新的组长。
练习:fork一个子进程,并使其创建一个新会话。查看进程组ID、会话ID前后变化
守护进程daemon
Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。
创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。
创建守护进程模型
- 创建子进程,父进程退出所有工作在子进程中进行形式上脱离了控制终端
- 在子进程中创建新会话
setsid()函数
使子进程完全独立出来,脱离控制
- 改变当前目录为根目录
chdir()函数
防止占用可卸载的文件系统
也可以换成其它路径
如果这一个进程处于的目录是一个U盘, 这一个U盘被拔出来会导致这一个程序出问题, 改为根目录这一个程序就不会出错
- 重设文件权限掩码
umask()函数
防止继承的文件创建屏蔽字拒绝某些权限
增加守护进程灵活性
- 关闭文件描述符(主要是标准输入输出)
继承的打开文件不会用到,浪费系统资源,无法卸载
开始执行守护进程核心工作守护进程退出处理程序模型
一般可以把这三个文件重定向到/dev/null里面,用于符合一般的编程习惯
示例
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void){
pid_t pid;
pid = fork(); //1.创建子进程
int ret;
if(pid > 0){
exit(0);
}else{
pid = setsid(); //2.创建新的会话
if(pid == -1){
perror("setid error");
exit(0);
}
ret = chdir("/home/jiao");//3.改变文件的目录
if(ret < 0){
perror("chdir error");
exit(0);
}
umask(0022);//4.改变文件的掩码
close(STDIN_FILENO);
int fd;
fd = open("/dev/null", O_RDWR);//5.重定向文件的输出
if(fd == -1){
perror("open error");
exit(0);
}
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
while(1);
}
return 0;
}
线程原理
LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)
进程:独立地址空间,拥有PCB
线程:有独立的PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间。 独居(进程);合租(线程)。
Linux下: 线程:最小的执行单位
进程:最小分配资源单位,可看成是只有一个线程的进程。
可以使用
ps -Lf pid
查看一个进程的线程
LWP: 执行的最小单位, 轻量级线程, 实际等于tid
Linux内核线程实现原理
类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。
- 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
- 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
- 进程可以蜕变成线程
- 线程可看做寄存器和栈的集合
- 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
察看LWP号:ps –Lf pid 查看指定线程的lwp号。
三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元
x86使用页表实现虚拟内存原理分析---使用代码分析-CSDN博客
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。
如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
线程共享资源
- 文件描述符表
- 每种信号的处理方式 , 这一个信号的处理方式这几个线程都一样, 实际处理的是获取到的那一个, 但是线程的mask不一样, 可以改变mask使得某一个线程处理这一个信号
不推荐线程以及信号混合使用!!!
- 当前工作目录
- 用户ID和组ID
- 内存地址空间 (.text/.data/.bss/heap/共享库), 这一个里面没有栈
线程非共享资源
- 线程id
- 处理器现场和栈指针(内核栈, 用于处理高特权级时候使用)
- 独立的栈空间(用户空间栈, 普通用户使用)
- errno变量, 这是一个全局变量
- 信号屏蔽字
- 调度优先级
线程优、缺点
优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便(全局变量, 堆区, 以及rodata共享, 可以直接使用)
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
线程控制
以下的函数是使用pthread 库进行线程级别的操作。
pthread_self获取线程ID
获取的线程ID是一个posix标准的ID, 只用于这一个进程里面的区分, 这是这一个线程库使用的变量
对应gettid, 这一个tid是进程里面标示身份的, 一个多线程的任务的所有线程的PID是一样的, tid用于区分不同的线程(内核用于时间片轮转), 这一个是系统级的(lwp:轻量级线程, 实际代表的也是这一个)
gettid() returns the caller's thread ID (TID). In a single-threaded process, the thread ID is equal to the process ID (PID, as returned by getpid(2)). In a multithreaded process, all threads have the same PID, but each one has a unique TID. For further details, see the discussion of CLONE_THREAD in clone(2).
可以使用这一个命令获取TID
The pthread_self() function returns the ID of the calling thread. This is the same value that is returned in *thread in the pthread_create(3) call that created this thread.
Thread IDs are guaranteed to be unique only within a process. A thread ID may be reused after a terminated thread has been joined, or a detached thread has terminated.
使用这一个命令获取这一个线程在这一个库里面的编号
pthread_create函数
创建一个新线程。 其作用,对应进程中fork() 函数。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功:0; 失败:错误号 -----Linux环境下,所有线程特点,失败均直接返回错误号。
参数:
pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。设置这一个线程的优先级, 线程的栈空间大小
The attr argument points to a pthread_attr_t structure whose contents are used at thread creation time to determine attributes for the new thread; this structure is initialized using pthread_attr_init(3) and related functions. If attr is NULL, then the thread is created with default attributes.
这一个参数使用pthread_attr_init这一个函数
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束
参数4:线程主函数执行期间所使用的参数
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。
start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。
pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。
attr参数表示线程属性,本节不深入讨论线程属性,所有代码例子都传NULL给attr参数,表示线程属性取缺省值,感兴趣的读者可以参考APUE。
示例
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <string.h>
void *thread_handler(void *arg){
printf("-----------thread------------\n");
printf("pid = %d, swl = %ld\n",
getpid(), pthread_self());
return NULL;
}
int main(void){
pthread_t tid;
tid = pthread_self();
printf("main pid = %d swl = %ld \n", getpid(), tid);
int ret = pthread_create(&tid, NULL, thread_handler, NULL);
if(ret != 0){
printf("pthreat creat error, %d = %s\n", ret, strerror(ret));
exit(0);
}
printf("tid now = %ld\n", tid);
sleep(1);
return 0;
}
由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。
如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止
创建的线程的return实际效果是执行pthread_exit返回这一个返回值, main返回实际会结束这一个线程组, 相当于exit
线程属性
linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
c
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;
这是一个比较早的版本
主要结构体成员:
- 线程分离状态
- 线程栈大小(默认平均分配)
- 线程栈警戒缓冲区大小(位于栈末尾) 参 APUE.12.3 线程属性
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。
线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
线程属性初始化
注意:应先初始化线程属性,再pthread_create创建线程
初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
成功:0;失败:错误号
销毁线程属性所占用的资源
int pthread_attr_destroy(pthread_attr_t *attr);
成功:0;失败:错误号
线程的分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
线程分离状态的函数:
设置线程属性,分离or非分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
获取程属性,分离or非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
参数: attr:已初始化的线程属性
detachstate: PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD _CREATE_JOINABLE(非分离线程)
这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
示例1: 使用属性设置是否分离
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>
void *pthread_func(void *arg){
printf("pthread_func\n");
return (void *)0x1234;
}
int main(void){
pthread_t tid;
pthread_attr_t attr;
int ret;
ret = pthread_attr_init(&attr);
if(ret != 0){
printf("pthread_attr_init %s\n", strerror(ret));
exit(1);
}
ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if(ret != 0){
printf("pthread_attr_setdetachstate %s\n", strerror(ret));
exit(1);
}
ret = pthread_create(&tid, &attr, (void *)pthread_func, NULL);
if(ret != 0){
printf("pthread_create %s\n", strerror(ret));
exit(1);
}
....
}
设置一个分离属性
示例2: 使用属性设置栈大小
c
#include <pthread.h>
#define SIZE 0x100000
void *th_fun(void *arg)
{
while (1)
sleep(1);
}
int main(void)
{
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;
}
pthread_exit线程的退出
将单个线程退出
void pthread_exit(void *retval);
参数:retval表示线程退出状态,通常传NULL
结论:线程中,禁止使用exit函数,会导致进程内所有线程全部退出。
在不添加sleep控制输出顺序的情况下。pthread_create在循环中,几乎瞬间创建5个线程,但只有第1个线程有机会输出(或者第2个也有,也可能没有,取决于内核调度)如果第3个线程执行了exit,将整个进程退出了,所以全部线程退出了。
所以,多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。
注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <string.h>
void *thread_handler(void *arg){
if((long)arg == 3){
pthread_exit((void *)0);//可以单独退出某一个线程
}
printf("-----------thread%ld------------\n", (long)arg);
printf("pid = %d, swl = %ld\n",
getpid(), pthread_self());
return NULL;
}
int main(void){
pthread_t tid;
tid = pthread_self();
printf("main pid = %d swl = %ld \n", getpid(), tid);
for(long i = 0 ; i < 5 ;i ++){
int ret = pthread_create(&tid, NULL, thread_handler, (void *)i);
if(ret != 0){
printf("pthreat creat error, %d = %s\n", ret, strerror(ret));
exit(0);
}
}
printf("parent pid = %d tid now = %ld\n",getpid(), tid);
//return 0;
pthread_exit((void *)0);//这个时候不需要sleep了
}
pthread_join获取返回值
阻塞等待线程退出,获取线程退出状态 其作用,对应进程中 waitpid() 函数。
int pthread_join(pthread_t thread, void **retval); 成功:0;失败:错误号
参数:thread:线程ID (【注意】:不是指针);retval:存储线程结束状态。
c
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
struct thrd {
int val;
char str[256];
};
void * tfn(void *arg){
struct thrd *tval;//初始化一块结构体
tval = malloc(sizeof(struct thrd));
tval->val = 100;
strcpy(tval->str, "hello world");
return (void *)tval;
}
int main(void){
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0){
printf("pthread creat error %d :%s", ret, strerror(ret));
exit(1);
}
void *retval;
ret = pthread_join(tid, &retval);//获取这一个的信息
if(ret != 0){
printf("pthread join error %d :%s", ret, strerror(ret));
exit(1);
}
printf("get thread ret : %d %s\n", ((struct thrd*)retval)->val,
((struct thrd*)retval)->str);//打印信息
free(retval);
pthread_exit(NULL);
return 0;
}
回收多个进程的时候需要使用数组
pthread_detach线程的分离
int pthread_detach(pthread_t thread); 成功:0;
失败:错误号
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。
也可使用 pthread_create函数参2(线程属性)来设置线程分离。
c
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
struct thrd {
int val;
char str[256];
};
void * tfn(void *arg){
struct thrd *tval;
tval = malloc(sizeof(struct thrd));
tval->val = 100;
strcpy(tval->str, "hello world");
return (void *)tval;
}
int main(void){
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0){
printf("pthread creat error %d :%s", ret, strerror(ret));
exit(1);
}
ret = pthread_detach(tid); //进行分离
if(ret != 0){
printf("pthread detach error %d :%s", ret, strerror(ret));
exit(1);
}
void *retval;
ret = pthread_join(tid, &retval); //获取这一个线程的返回值, 这个时候已经分离了, 会失败
if(ret != 0){
printf("pthread join error %d :%s", ret, strerror(ret));
exit(1);
}
printf("get thread ret : %d %s\n", ((struct thrd*)retval)->val,
((struct thrd*)retval)->str);
pthread_exit(NULL);
free(retval);
return 0;
}
pthread_cancel函数
杀死(取消)线程 其作用,对应进程中 kill() 函数。
int pthread_cancel(pthread_t thread); 成功:0;失败:错误号
【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点。
被取消的线程, 退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h中找到它的定义:**#define PTHREAD_CANCELED ((void *) -1)****。**因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1。
c
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void * tfn(void *arg){
while(1){
printf("thread pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(1);
}
return NULL;
}
int main(void){
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0){
printf("pthread creat error %d :%s", ret, strerror(ret));
exit(1);
}
printf("main pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(5);
ret = pthread_cancel(tid);
if(ret != 0){
exit(1);
}
while(1);
}
if the target thread was canceled, then PTHREAD_CANCELED is placed in the location pointed to by retval
这一个进程使用pthread_join回收的值是-1
clong retv; pthread_join(tid, (void **)&retv); printf("ret = %ld\n", retv);
**注: **
如果这一个子线程使用的是一个while(1);之类的死循环是无法退出的, 因为没有取消点(进入内核)
c#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> void * tfn(void *arg){ while(1){ } return NULL; } int main(void){ pthread_t tid; int ret = pthread_create(&tid, NULL, tfn, NULL); if(ret != 0){ printf("pthread creat error %d :%s", ret, strerror(ret)); exit(1); } printf("main pid = %d, tid = %lu\n", getpid(), pthread_self()); sleep(5); ret = pthread_cancel(tid); if(ret != 0){ exit(1); } long retv; pthread_join(tid, (void **)&retv);//会在这里卡死 printf("ret = %ld\n", retv); }
可以使用函数pthread_testcancel添加一个取消点
cwhile(1){ pthread_testcancel(); }
pthread_kill发送信号
c
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
发送零的时候不会发送信息, 但是会检测错误, 可以使用这一个来判断这一个线程是不是存活
总结
和进程对比
线程 | 进程 |
---|---|
pthread_create | fork |
pthread_self | getpid |
pthread_exit | exit |
pthread_join | wait/waitpid |
pthread_cancel | kill |
pthread_detach | 无 |
终止线程方式
总结:终止某个线程而不终止整个进程,有三种方法:
- 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。
- 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
- 线程可以调用pthread_exit终止自己。
线程使用注意事项
- 主线程退出其他线程不退出,主线程应调用pthread_exit
- 避免僵尸线程
pthread_join
pthread_detach
pthread_create指定分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
- malloc和mmap申请的内存可以被其他线程释放
- 应避免在多线程模型中调用fork除非,马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
- 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制