【LinuxC编程】06 - 守护进程,线程

进程组和会话

概念和特性

进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。

当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID=第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID=其进程ID

可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。

组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。

一个进程可以为自己或子进程设置进程组ID

会话概念

会话(Session)是操作系统中的一个概念,用于表示一个用户与系统之间的一系列交互活动 。它是一个逻辑上的分组,用于管理用户的登录状态、控制终端、进程组以及与这些进程相关的环境和属性。

会话的主要作用是提供一个框架,使得用户可以在一个隔离 的环境中执行程序,同时允许系统对这些程序进行统一 的管理。

一般是包括多个进程组的集合

例如:一个浏览器与Web服务器之间连续发生的一系列请求和响应的过程

新会话进程脱离于终端,不随用户注销消亡,也作为守护进程

主要还是为了服务于一次终端与用户的会话,搭建的框架,用于统一的资源调度。

会话创建

创建一个会话需要注意以下6点注意事项:

  1. 若要启动一个新的会话,当前进程不能是其所属进程组的组长。启动后,该进程将成为新会话的首进程。
  2. 创建新会话的进程,将自动成为新进程组的组长。
  3. 需有root权限 (ubuntu不需要)
  4. 新创建的会话将不再与任何控制终端关联,因此该会话将没有控制终端。
  5. 如果一个进程在尝试创建新会话时已经是其进程组的组长,那么这个操作将不会成功,并且会返回一个错误。
  6. 在创建新会话的过程中,首先需要调用fork函数创建子进程,然后父进程结束运行,子进程继续执行setsid函数。确保新会话的创建不会受到原有进程环境的影响。

!info\] setsid后的IO变化

当一个进程调用 setsid 后,它将失去与控制终端的联系,并且它的标准输入、输出和错误流(stdin、stdout、stderr)将不再与任何终端相关联。这意味着:

标准输入(stdin):进程将不再有控制终端作为其标准输入。如果进程尝试从 stdin 读取,它将收到一个 EOF(文件结束符),因为没有任何终端与之关联。

标准输出(stdout)和标准错误(stderr):这两个流的行为取决于进程的设置。默认情况下,如果进程没有重定向这些流到其他文件或设备,它们将被重定向到 /dev/null,这是一个特殊的文件,它会丢弃所有写入其中的数据。这意味着任何尝试写入 stdout 或 stderr 的输出都将丢失。

一保险起见,一般需要自己手动重定向到null

getsid 函数

获取进程所属的会话ID

c 复制代码
pid_t getsid(pid_t pid); 
// 成功:返回调用进程的会话ID;失败:-1,设置errno

pid为0表示察看当前进程session ID

ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。

组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。

setsid函数

创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。

调用setsid()创建一个新的会话,并成为该会话的首进程。这将使进程脱离任何已存在的终端和进程组(也就是成为守护进程),不随终端注销。

标准输入输出所对应的文件描述符为空未使用,一般重定向标准输入输出(stdin、stdout、stderr)所对应的fd 0 1 2到/dev/null或特定的日志文件。

c 复制代码
pid_t setsid(void);  
//成功:返回调用进程的会话ID;失败:-1,设置errno

调用了setsid函数的进程,既是新的会长,也是新的组长。

!info\] 练习 fork一个子进程,并使其创建一个新会话。查看进程组ID、会话ID前后变化

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>

int main() {
    pid_t pid;
    pid=fork();
    if(pid<0){
        perror("fork");
        exit(1);
    }else if(pid==0){
        printf("chile process PID is %d\n",getpid());
        printf("Group ID of child is %d\n", getpgid(0));
        printf("Session ID of child is %d\n", getsid(0));

        sleep(1); 
        setsid();// 子进程成为新会话首进程,且成为组长进程。
        printf("Changed:\n");


        printf("chile process PID is %d\n",getpid());
        printf("Group ID of child is %d\n", getpgid(0));
        printf("Session ID of child is %d\n", getsid(0));

        sleep(1);
        exit(0);
    }
    return 0;
}

守护进程

Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。

Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。

创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。

创建守护进程模型

  1. 创建子进程,父进程退出
    所有工作在子进程中进行形式上脱离了控制终端
  2. 在子进程中创建新会话
       setsid()函数
       使子进程完全独立出来,脱离控制
  3. 改变当前目录为根目录
       chdir()函数
       防止占用可卸载的文件系统
       也可以换成其它路径
  4. 重设文件权限掩码
       umask()函数
       防止继承的文件创建屏蔽字拒绝某些权限
       增加守护进程灵活性
  5. 关闭文件描述符
       继承的打开文件不会用到,浪费系统资源,无法卸载
  6. 开始执行守护进程核心工作守护进程退出处理程序模型

守护进程创建实例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>
#include <sys/stat.h>
#include <fcntl.h>

void err(const char *str) {
    perror(str);
    exit(1);
}

int main() {
    // 创建不是进程组首进程的进程
    pid_t pid;
    pid = fork();
    if (pid > 0)
        exit(0);

    // 创建新会话,进程脱离原进程组,成为会话首进程
    if (setsid() == -1)
        err("setsid error");

    // 改变目录位置
    if (chdir("/home/qiuliw/vscode") == -1)
        err("chdir error");

    // 改变默认文件访问权限,屏蔽组和其他用户写权限
    umask(0022);

    // 重定向文件描述符 0 1 2 到 /dev/null
    int fd;
    close(STDIN_FILENO);
    fd = open("/dev/null", O_RDWR);
    if (fd == -1)
        err("open error");
    dup2(fd,STDOUT_FILENO);
    dup2(fd,STDERR_FILENO);

    // 模拟守护进程业务
    while(1){
        sleep(1);
    }
    return 0;
}

线程概念

什么是线程

LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)

进程:独立地址空间,拥有PCB

线程:有独立的PCB,但没有独立的地址空间(共享)

区别:在于是否共享地址空间。 独居(进程);合租(线程)。

Linux下:

线程:最小的执行 单位

进程:最小分配资源单位,可看成是只有一个线程的进程。

Linux内核线程实现原理

类Unix系统中,早期是没有"线程"概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。

  1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
  3. 进程可以蜕变成线程
  4. 线程可看做寄存器和栈的集合
  5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
    察看LWP号:ps --Lf pid 查看指定线程的lwp号。

三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元

参考:《Linux内核源代码情景分析》 ----毛德操

对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。

如果复制对方的地址空间,那么就产出一个"进程";如果共享对方的地址空间,就产生一个"线程"。

因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

线程共享资源

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID
  5. 内存地址空间 (.text/.data/.bss/heap/共享库)

线程非共享资源

  1. 线程id
  2. 处理器现场和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

线程优、缺点

优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便

缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好

优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

线程控制原语

pthread_self函数

获取线程ID。其作用对应进程中 getpid() 函数。

c 复制代码
pthread_t pthread_self(void);	
//返回值:成功:0;	失败:无!

线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现

线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)

注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。

pthread_create函数

创建一个新线程。 其作用,对应进程中fork() 函数。

c 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
	//返回值:成功:0;	
	//失败:错误号	-----Linux环境下,所有线程特点,失败均直接返回错误号。
c 复制代码
pthread_t:当前Linux中可理解为:
typedef  unsigned long int pthread_t;

参数

  • thread:传出参数,保存系统为我们分配好的线程ID
  • attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
  • void *(*start_routine) (void *):函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
  • arg :线程主函数执行期间所使用的参数。

在一个线程中调用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。

!info\] 练习 创建一个新线程,打印线程ID。注意:链接线程库 -lpthread

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程函数
void* thread_function(void* arg) {
    // 获取并打印线程ID
    pthread_t thread_id = pthread_self();
    printf("Thread ID: %ld\n", (long)thread_id);
    return NULL;
}

int main() {
    pthread_t thread_id;

    // 创建线程
    if (pthread_create(&thread_id, NULL, thread_function, NULL) != 0) {
        perror("pthread_create");
        return EXIT_FAILURE;
    }

    // 等待线程结束
    if (pthread_join(thread_id, NULL) != 0) {
        perror("pthread_join");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。

!info\] 练习 循环创建多个线程,每个线程打印自己是第几个被创建的线程。(类似于进程循环创建子进程)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

void sys_err(const char *str){
    perror(str);
    exit(1);
}

void *tfn(void *arg){
    int n =*((int *)arg);

    sleep(n);
    printf("I'm %dth thread: pid = %d, tid = %lu\n",n+1,getpid(),pthread_self());

    return NULL;
}

int main(){

    int i,ret;
    pthread_t tid;
    int *n;
    for(i=0;i<5;i++){
        n = (int *)malloc(sizeof(int));
        *n = i;
        ret = pthread_create(&tid,NULL,tfn,n);
        if(ret != 0){
            sys_err("pthread_create error");
        }
    }
    sleep(i); // 防止主线程返回内核执行exit销毁进程
    printf("I'm %dth thread: pid = %d, tid = %lu\n",i+1,getpid(),pthread_self());
    return 0;
}

for循环的不同轮次内创建的同名变量使用相同的地址

拓展思考:将pthread_create函数参4修改为(void *)&i, 将线程主函数内改为i=*((int *)arg)是否可以?

线程与共享

线程间共享全局变量!

【牢记】:线程默认共享数据段、代码段等地址空间,常用的是全局变量。而进程不共享全局变量,只能借助mmap。

pthread_exit函数

将单个线程退出

c 复制代码
void pthread_exit(void *retval);	
//参数:retval表示线程退出状态,通常传NULL

思考:使用exit将指定线程退出,可以吗?

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

void sys_err(const char *str){
    perror(str);
    exit(1);
}

void *tfn(void *arg){
    int n =*((int *)arg);
    if(n==2)
        exit(0); // 退出进程
    sleep(n);
    printf("I'm %dth thread: pid = %d, tid = %lu\n",n+1,getpid(),pthread_self());

    return NULL;
}

int main(){

    int i,ret;
    pthread_t tid;
    int *n;
    for(i=0;i<5;i++){
        n = (int *)malloc(sizeof(int));
        *n = i;
        ret = pthread_create(&tid,NULL,tfn,n);
        if(ret != 0){
            sys_err("pthread_create error");
        }
    }
    sleep(i); // 防止主线程返回内核执行exit销毁进程
    printf("I'm %dth thread: pid = %d, tid = %lu\n",i+1,getpid(),pthread_self());
    return 0;
}

结论:线程中,禁止使用exit函数,会导致进程内所有线程全部退出。

在不添加sleep控制输出顺序的情况下。pthread_create在循环中,几乎瞬间创建5个线程,但只有第1个线程有机会输出(或者第2个也有,也可能没有,取决于内核调度)如果第3个线程执行了exit,将整个进程退出了,所以全部线程退出了。

所以,多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。

另注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

【练习】:编写多线程程序,总结exit、return、pthread_exit各自退出效果。

return:返回到调用者那里去。非主函数线程return不会导致进程结束。主函数线程return会直接通知内核立即调用exit回收这个进程,只有main函数线程与内核回收钩子绑定。

pthread_exit():将调用该函数的线程退出

exit: 将进程退出。

pthread_join函数

阻塞等待线程退出,获取线程退出状态

其作用,对应进程中 waitpid() 函数。

c 复制代码
int pthread_join(pthread_t thread, void **retval); 
// 成功:0;失败:错误号

参数:thread:线程ID (【注意】:不是指针);retval:存储线程结束状态。返回值是指针,所以需要传存储指针的地址,也就是二级指针

对比记忆:

进程中:main返回值、exit参数-->int;等待子进程结束 wait 函数参数-->int *

线程中:线程主函数返回值、pthread_exit-->void *;等待线程结束 pthread_join 函数参数-->void **

!info\] 练习 参数 retval 非空用法。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct thrd{
    int var;
    char str[256];
};

void *tfn(void * arg){
    struct thrd *tval;

    tval =(struct thrd *) malloc(sizeof(struct thrd));

    tval->var = 100;
    strcpy(tval->str,"hello thread");

    return (void *)tval;
}

void sys_err(const char *str){
    perror(str);
    exit(1);
}

int main(int argc,char *argv[]){
    pthread_t tid;
    int ret;
    struct thrd *retval;
    pthread_create(&tid,NULL,tfn,NULL);

    if(ret!= 0){
        sys_err("pthread_create error");
    }

    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);

    return 0;
}

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

!info

练习线程返回常量与类型转换

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>


void *tfn(void * arg){
    return (void *)74;
}

void sys_err(const char *str){
    perror(str);
    exit(1);
}

int main(int argc,char *argv[]){
    pthread_t tid;
    int ret;
    ret = pthread_create(&tid,NULL,tfn,NULL);
    int retval;
    if(ret!= 0){
        sys_err("pthread_create error");
    }

    // pthread_join会把返回值存入retval,所以传入一个&retval。只要接收变量的空间和返回值空间长度一样即可,不用管类型,函数内部自动对形参解引用然后装入返回值。参数强转(void **),返回值强转(void *)。
    ret = pthread_join(tid,(void **)&retval);
    if(ret !=0)
        sys_err("pthread_join error");

    printf("child thread exit with var = %d\n",retval);

    pthread_exit(NULL);

    return 0;
}

!info\] 练习 使用pthread_join函数将循环创建的多个子线程回收。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define NUMS 5

void *tfn(void *arg) {
    int n = *(int*)arg;
    printf("I'm thread %dth ...\n",n);
    pthread_exit( (void*)n);
}

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

int main(int argc, char *argv[]) {

    pthread_t tid[NUMS];
    int arg[NUMS];
    for (int i = 0; i < NUMS; ++i) {
        arg[i] = i+1;
        if (pthread_create(tid + i, NULL, tfn, arg + i) != 0)
            sys_err("pthread_create error");
    }
    for(int i=0;i<NUMS;++i){
        int ret[NUMS];
        pthread_join(tid[i],(void**)(ret+i));
        printf("已回收线程 tid = %d, ret = %d\n",tid[i],ret[i]);
    }
    printf("回收完成\n");
    return 0;
}

pthread_detach函数

实现线程分离

c 复制代码
int pthread_detach(pthread_t thread);	
//成功:0;失败:错误号

线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。

也可使用 pthread_create函数参2(线程属性)来设置线程分离。

【练习】:使用pthread_detach函数实现线程分离

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>


void *tfn1(void *arg) {
    printf("Hello I'm thread: pid = %d, tid = %lu ...\n\n",getpid(),pthread_self());
    return NULL;
}

int main(int argc, char *argv[]) {
    pthread_t tid;
    int ret;

    ret = pthread_create(&tid,NULL,tfn1,NULL);
    if(ret !=0)
        perror("pthread_create error");

    ret = pthread_detach(tid);
    if(ret !=0)
        perror("pthread_detach error");

    printf("main: pid = %d, tid = %lu ...\n",getpid(),pthread_self());

    sleep(2);
    return 0;
}

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。

不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。

也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

检测出错返回

c 复制代码
fprintf(stderr,"xxx error: %s\n",strerror(ret))

pthread_cancel函数

杀死(取消)线程。其作用,对应进程中 kill() 函数。

c 复制代码
int pthread_cancel(pthread_t thread);	成功:0;失败:错误号

简单使用

c 复制代码
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>


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);
    pthread_cancel(tid); // 终止线程

    return 0;
}

!warning\] 注意 线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。

取消点 :是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用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

!info\] 练习 终止线程的三种方法。注意"取消点"的概念。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>


void *tfn1(void *arg) {
    printf("thread 1 returning\n");
    return (void *)111;
}

void *tfn2(void *arg){
    printf("thread 2 exiting\n");
    pthread_exit((void*)222);
}

void *tfn3(void *arg){
    while(1){
        printf("thread 3: I'm going to die in 3 second\n");
        sleep(1); 

        // pthread_testcancel(); // 自己添加取消点
        return (void *)666;
    }
}


int main(int argc, char *argv[]) {
    pthread_t tid;
    void *tret = NULL;

    pthread_create(&tid,NULL,tfn1,NULL);
    pthread_join(tid,&tret);
    printf("thread 1 exit code = %d\n\n",tret);

    pthread_create(&tid,NULL,tfn2,NULL);
    pthread_join(tid,&tret);
    printf("thread 2 exit code = %d\n\n",tret);

    pthread_create(&tid,NULL,tfn3,NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid,&tret);
    printf("thread 3 exit code = %d\n",tret);


    return 0;
}

终止线程方式

总结:终止某个线程而不终止整个进程,有三种方法:

  1. 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。
  2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
  3. 线程可以调用pthread_exit终止自己。

控制原语对比

复制代码
进程			线程
fork			pthread_create
exit			pthread_exit
wait			pthread_join
kill			pthread_cancel
getpid		pthread_self		命名空间

线程属性

本节作为指引性介绍,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; 

主要结构体成员:

  1. 线程分离状态

  2. 线程栈大小(默认平均分配)

  3. 线程栈警戒缓冲区大小(位于栈末尾) 参 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创建线程

初始化线程属性

c 复制代码
int pthread_attr_init(pthread_attr_t *attr); 
//成功:0;失败:错误号 

销毁线程属性所占用的资源

c 复制代码
int pthread_attr_destroy(pthread_attr_t *attr); 
//成功:0;失败:错误号 

线程的分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。

非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

设置线程属性,分离or非分离

c 复制代码
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 

获取程属性,分离or非分离

c 复制代码
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()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>


void *tfn1(void *arg) {
    printf("Hello I'm thread: pid = %d, tid = %lu ...\n\n", getpid(), pthread_self());
    return NULL;
}

int main(int argc, char *argv[]) {

    pthread_t tid;
    int ret;

    // 初始化线程属性
    pthread_attr_t attr;
    ret = pthread_attr_init(&attr);
    if (ret != 0)
        fprintf(stderr, "pthread_attr_init attr:%s\n", strerror(ret));

    // 设置线程 分离状态属性 为 分离
    ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (ret != 0)
        fprintf(stderr, "pthread_attr_setdetachstate attr:%s\n", strerror(ret));

    // 创建线程,传入线程属性
    pthread_create(&tid, &attr, tfn1, NULL);
    if (ret != 0)
        perror("pthread_create error");

    // 销毁线程属性所占用的资源
    ret = pthread_attr_destroy(&attr);
    if (ret != 0)
        fprintf(stderr, "pthread_attr_destroy attr:%s\n", strerror(ret));

    // 检测是否分离,分离态无法被回收
    ret = pthread_join(tid, NULL);
    if (ret != 0){
        fprintf(stderr, "pthread_join attr:%s\n", strerror(ret));
        exit(1);
    }

    // ...主函数任务代码...
    printf("main: pid = %d, tid = %lu ...\n", getpid(), pthread_self());

    pthread_exit((void *) 0);
}

线程属性控制示例

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;
}											

线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应调用pthread_exit
  2. 避免僵尸线程
    pthread_join
    pthread_detach
    pthread_create指定分离属性
    被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
  3. malloc和mmap申请的内存可以被其他线程释放
  4. 避免在多线程模型中调用fork,除非,马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
  5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
相关推荐
一只自律的鸡12 天前
【Linux系统编程】文件IO 函数篇
linux·linux系统编程
Ching·4 个月前
linux系统编程(十)RK3568 socket之 UDP的实现
linux系统编程·rk3568·嵌入式开发
优美的赫蒂7 个月前
认识linux进程内存布局以及与命令行参数和环境变量的关系
linux·运维·服务器·linux系统编程
日落星野1 年前
进程间通信(二)消息队列、共享内存、信号量
linux·linux系统编程·1024程序员节
日落星野1 年前
线程(三) 线程的互斥
linux·linux系统编程
小乌龟不会飞1 年前
【C++】【网络】【Linux系统编程】单例模式,加锁封装TCP/IP协议套接字
网络·c++·网络协议·tcp/ip·linux系统编程
几度春风里2 年前
嵌入式Linux系统编程 — 7.2 进程的环境变量
嵌入式·linux系统编程·进程的环境变量
几度春风里2 年前
嵌入式Linux系统编程 — 6.2 signal和 sigaction信号处理函数
嵌入式·linux系统编程·信号处理函数
橘猫0.o2 年前
【Linux系统编程】进程间通信
linux·运维·服务器·经验分享·笔记·linux系统编程