十四、Linux之守护进程、线程

十四、Linux之守护进程、线程


一、进程组和会话(session)
1.进程组
  • 当父进程创建子进程时,子进程会和父进程同属于一个进程组,且该组的组长为父进程,进程组ID为父进程的PID
  • 组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程还存在,那么进程组就存在,与组长进程是否终止无关,即一个进程组的生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)

kill -SIGKILL -进程组ID

可以结束同一进程组的所有进程

-进程组ID:实际上是 负号 + 进程组ID,取绝对值,表示进程组

2.创建会话

✅会话实际上就是多个进程组的集合,例如:一个 Treminal 终端就是一个会话

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

  • 创建会话时,调用进程不能是进程组组长,若调用进程是组长进程,则出错返回
  • 调用的进程将变成新会话的首进程,并且成为一个新进程组的组长进程
  • 需有 root 权限(Ubuntu 不需要)
  • 新的会话会丢弃原有的控制终端,该会话就没有控制终端,无法与用户进行交互
  • 建立新的会话时,先调用 fork,父进程终止,子进程调用 setsid(因为父进程是组长进程)

✅进程ID:pid (process id),进程组ID:pgid (process group id) ,会话ID:sid (session id)

3.setsid、getsid函数

包含头文件

#include <unistd.h>

pid_t setsid(void);

创建一个会话,并以自己的PID设置进程组ID,同时也是新的会话的ID;
调用了 setsid() 函数的进程,以自身为中心,既是新的会话会长,也是新的进程组组长

返回值(pid_t) 成功时返回调用进程的会话ID,失败返回 -1errno


pid_t getsid(pid_t pid);

获取指定进程的会话ID

pid_t pid0 时,返回调用该函数的进程的会话ID,> 0 时,返回指定 pid 进程的会话 ID
返回值 成功时返回会话ID,失败返回 -1errno

例如:创建一个子进程,以该子进程创建新会话,查看进程组ID、会话ID的前后变化

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

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

    pid = fork();
    sys_err(pid, "fork error");
    if(pid == 0)
    {
        printf("I'm child, pid = %d, gid = %d, sid = %d\n", getpid(), getpgid(0),getsid(0));

        pid_t wpid = setsid();
        sys_err(wpid, "setsid error");

        printf("Create new sid successfully!\n");
        printf("New sid, pid = %d, gid = %d, sid = %d\n", getpid(), getpgid(0),getsid(0));

        exit(0);
    }

    // 父进程终止

    return 0;
}

发现在创建会话前,子进程的PID,组ID和会话ID都不一样,而创建后则一致了,且都是子进程的PID
在这里插入图片描述


二、守护进程(重要)
1.基本概念

守护进程又称为 Daemon(精灵)进程,通常运行于操作系统后台,是Linux中的后台服务进程,一般周期性地等待某个事件发生或周期性地执行某一动作,例如:服务器就是做这些事。它们一般采用以 “d” 结尾的名字,如:httpd、mysqld等

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

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

2.创建守护进程的模型
  1. 创建子进程,父进程退出
    所有的工作都在子进程中进行,其形式上脱离了控制终端

  2. 子进程调用 setsid() 创建新的会话
    使进程完全独立出来,脱离控制

  3. 通常根据需要,调用 chdir() 来改变工作目录位置
    目的是防止目录被卸载

  4. 通常根据需要,调用 umask() 函数重设 umask 文件权限掩码
    防止影响创建新文件的权限,增加守护进程的灵活性
    ✳umask:0022;权限:0755
    ✳umask:0345;权限:0432

  5. 通常根据需要,关闭/重定向 文件描述符
    子进程所继承的 stdinstdoutstderr 文件的文件描述符默认打开,浪费系统资源,应该关闭
    但通常情况下会进行重定向,而不是直接关闭,操作如下

    1️⃣关闭 stdin 文件描述符,使用 open() 打开 /dev/null,此时 /dev/null 的文件描述符将占用 0,因为 stdin 被关闭
    /dev/null 是一个空洞文件,可以无限写入字节
    2️⃣将 stdout 的文件描述符重定向至 0(/dev/null)
    3️⃣将 stderr 的文件描述符重定向至 0 (/dev/null)

  6. 开始执行守护进程,业务逻辑:while()

3.创建守护进程

例如:根据模型写一个守护进程 demo

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    pid_t pid;
    int ret, fd;

    pid = fork();
    sys_err(pid, "fork error");
    if(pid > 0)
    {
        exit(0); // 父进程终止
    }

    // 子进程
    pid = setsid(); // 创建新的会话
    sys_err(pid, "setsid error");

    ret = chdir("/root/Desktop/Linux系统编程"); // 改变工作目录位置
    sys_err(ret, "chdir error");

    ret = umask(0022); // 改变文件访问权限掩码 umask
    sys_err(ret, "umask error");

    close(STDIN_FILENO); // 关闭/重定向 文件描述符
    fd = open("/dev/null", O_RDWR);
    ret = dup2(fd, STDOUT_FILENO);
    sys_err(ret, "dup2 stdout error");
    ret = dup2(fd, STDERR_FILENO);
    sys_err(ret, "dup2 stderr error"); 

    /* 业务代码 */

    return 0;
}

运行起来
在这里插入图片描述
通过 ps 命令查看,发现守护进程已经在后台运行
在这里插入图片描述
即使注销用户,守护进程仍然存在,需要通过 kill -9 来结束
在这里插入图片描述


三、线程(thread)
1.线程的概念

线程——LWP(Light Weight Process)轻量级的进程,在Linux下,其本质仍是进程

百度百科——线程:线程是操作系统能够进行运算调度的最小单位;它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

进程:有独立的进程地址空间,有独立的 PCB,是分配资源的最小单位

线程:没有独立的进程地址空间,有独立的PCB,是执行的最小单位,即对于CPU来说,会把线程看作一个执行单位
在这里插入图片描述

ps -Lf [PID]

查看进程中的线程

[PID]:进程号PID

2.三级映射

如图所示:进程创建的新线程和进程指向的页目录是同一个页目录,它们具有相同的三级映射,借助MMU映射到同一块物理内存,因此它们在相同的内存上,它们在运行指令部分的地址部分不同
在这里插入图片描述

3.线程共享/非共享的资源

✅共享资源:

  • 文件描述符表
  • 每种信号的处理方式
  • 当前工作目录
  • 用户ID和组ID
  • 内存地址空间(.text / .data / .rodataa / .bss / .heap / 共享库),共享全局变量

✅非共享资源:

  • 线程ID
  • 处理器现场和栈指针(内核栈)
  • 独立的栈空间(用户空间栈)
  • errno变量
  • 信号屏蔽字
  • 调度优先级

优点:(1)提高程序并发性、(2)数据通信,共享数据方便…优点突出
缺点:对信号支持不好…缺点不明显,在能够使用线程的情况下推荐使用线程

4.创建线程

包含头文件

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

创建一个子线程,执行指定的回调函数

pthread_t *thread 传出参数,返回所创建的新线程的线程ID,其类型是无符号整数:%lu,失败时不定义该参数
const pthread_attr_t *attr 设置线程的属性,若没有特殊需求,一般传 NULL,表示使用线程的默认属性
void *(*start_routine) (void *) 返回值和参数为任意类型的函数指针,是回调函数,即线程执行的函数
void *arg 上述回调函数所需要传入的参数,若没有参数则传 NULL
返回值 成功时返回 0,失败返回 errno(Linux环境下,所有线程特点——失败均直接返回错误号)


pthread_t pthread_self(void);

获取当前线程的线程ID,其类型是无符号整数:%lu
线程ID是进程内部识别不同线程的标准;

返回值(pthread_t) 返回调用线程的ID,该函数总会调用成功

man 手册中说明了:凡使用线程相关函数,需要在编译和链接阶段指定相关库:-pthread
在这里插入图片描述
因此在通过 gcc 时,需要指定 -pthread 库:

gcc test.c -o test -lpthread

相应地,makefile 也需要指定 -pthread

src = $(wildcard ./*.c)
obj = $(patsubst %.c, %, $(src))

ALL:$(obj)

%:%.c
	gcc $< -o $@ -lpthread

clean:
	rm -rf $(obj)

.PHONY: clean

此外,之前使用 perror() 的方式来打印线程错误是有问题的,可能会出现无法打印的问题,因此对于线程,需要改变出错检查

#include <string.h>

if(ret != 0)
{
	fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); // 向标准输出错误设备 stderr 写入错误原因,它打印错误
	exit(1);
}

例如:分别查看主函数和 pthread_create() 出来的线程的PID、线程ID

其中 sleep(1); 的作用是等待线程执行完毕,防止主线程先 return 0; 结束,因为主线程和子线程共用一个内存空间,如果主线程先结束,那么进程结束,内存就会被回收,则子线程将无法继续执行;当然,后面还有更好的防止进程结束的方法会介绍

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

void *tfunc(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, tfunc, NULL); // 创建线程
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		exit(1);
    }

    printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    sleep(1); // 等待线程执行完,防止主函数先结束

    return 0;
}

发现主函数和创建出来的线程处于同一个进程(进程PID相同),但线程号不同
在这里插入图片描述
例如:循环创建 N 个线程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

#define N 5

void *tfunc(void *arg)
{
    printf("I'm No.%d thread: pid = %d, tid = %lu\n", (int)arg, getpid(), pthread_self());

    return NULL;
}

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

    for(i=0; i<N; i++) // 循环创建多个子线程
    {
        ret  = pthread_create(&tid, NULL, tfunc, (void *)i); // 传参采用:值传递,借助强制类型转换
        if(ret != 0)
        {
            fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
			exit(1);
        }
    }

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

    return 0;
}

我们可以发现,main 线程率先执行,这是因为 pthread_create() 创建线程涉及到用户空间和内核空间的切换,需要一定的时间,因此执行时间会比 main 线程滞后
在这里插入图片描述
注意此代码在编译时会出现类型强转的警告,这是因为:在 pthread_create() 函数原型中,函数参数4:void *arg 明明需要传入 void * 类型,但上述代码中却直接把 i 强转:(void *)i 传入,然后通过 (int)arg 取值

为什么要这样做?而不是这样传参:(void *)&i、这样用参:*((int *)arg) 呢?

如果用后者这种方式来传参和用参,执行的结果是这样的:
在这里插入图片描述
这是因为,在传 (void *)&i 的时候,它传的是 i 的地址,但在 main() 函数仍在执行,for 循环在执行,因此这个 i 的值是从 0 ~ 5 不断变化的,因此如果直接传 i 的地址,得到的就是 i 的实时值

至于为什么最后都是 5,显而易见,main() 已经执行到打印那一步了,说明 for 循环已经结束了,已经在执行 sleep(1); 了,这个时候 i 已经是 5 了(由于线程的执行涉及到用户空间和内核空间的切换,因此执行时间较为滞后)

✅这个例子说明——主线程和子线程之间共享数据空间,线程回调函数内的变量属于局部变量

5.pthrea_exit()、pthread_join()函数

void pthread_exit(void *retval);

将调用该函数的线程退出,实际上就是代替回调函数中 return 的作用;
线程回调函数中的 return 只是表示返回到调用者那里,并不是退出线程;而主函数中的 return 则表示结束进程(所有线程都会被迫结束);
此外,exit(0) 表示退出进程,因此无法使用 exit(0) 来退出线程;
在前面的案例中,我们需要使用 sleep(1); 来让主线程等待子线程,防止主线程提前结束,现在我们可以使用 pthread_exit() 来代替 return 0;,使主线程退出,但同时并不结束进程

void *retval 传入线程的退出状态,可以传 NULL;这个值将会传给 pthread_join() 中的 void **retval


int pthread_join(pthread_t thread, void **retval);

阻塞等待指定线程退出,并获取线程退出状态,类似进程中的 waitpid()
实际上就是获取子线程的 return 值;或者是 pthread_exit() 中传入的 void *retval

pthread_t thread 线程号 tid
void **retval 传出参数,返回线程退出状态
返回值 成功时返回 0,失败返回 errno

例如:将子线程中的结构体作为返回状态,返回到主线程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

struct thread_test
{
    int var;
    char srt[256];
};

void *tfunc(void *arg) // 线程回调函数
{
    struct thread_test *value;
    value = (struct thread_test *)malloc(sizeof(struct thread_test));
    // 初始化结构体
    value->var = 10000;
    memset(value->srt, '\0', 256);
    strcpy(value->srt, "Hello Thread Test!");

    pthread_exit((void *)value);
    // return (void *)value; 可以使用 return 来返回
}

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

    ret = pthread_create(&tid, NULL, tfunc, NULL);
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		exit(1);
    }

    ret = pthread_join(tid, (void **)&return_value); // 阻塞等待子线程,并获取返回状态
    if(ret != 0)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		exit(1);
    }

    printf("var = %d, str = %s\n", return_value->var, return_value->srt);
	
	free(return_value); // 释放申请的内存
	
    pthread_exit(NULL);
}

上述代码中,由于回调函数的返回值是指针,因此不能直接定义结构体,而需要使用结构体指针和 malloc() 函数
在这里插入图片描述
上例中,可改为在主线程中定义结构体,在子线程中初始化其值,再返回到主线程,就可不直接申请内存空间

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

struct thread_test
{
    int var;
    char srt[256];
};

void *tfunc(void *arg) // 线程回调函数
{
    struct thread_test *value = (struct thread_test *)arg;
    // 初始化结构体
    value->var = 10000;
    memset(value->srt, '\0', 256);
    strcpy(value->srt, "Hello Thread Test!");

    pthread_exit((void *)value);
    // return (void *)value; 可以使用 return 来返回
}

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

    ret = pthread_create(&tid, NULL, tfunc, (void *)&value);
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		exit(1);
    }

    ret = pthread_join(tid, (void **)&return_value); // 阻塞等待子线程,并获取返回状态
    if(ret != 0)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		exit(1);
    }

    printf("var = %d, str = %s\n", return_value->var, return_value->srt);

    pthread_exit(NULL);
}

例如:循环创建 N 个子进程,并使用 pthread_join() 多个回收

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

#define N 5

void *tfunc(void *arg) // 线程回调函数
{
    printf("I'm No.%d thread, tid = %lu\n", (int)arg, pthread_self()); // 打印 i 和 tid,注意 tid 是 %lu

    pthread_exit(arg); // 直接把 i 返回
}

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

    for(i=0; i<N; i++)
    {
        ret = pthread_create(&tid[i], NULL, tfunc, (void *)i); // 直接将 i 作为 void * 类型传入
        if(ret != 0)
        {
            fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
			exit(1);
        }
    }

    for(i=0; i<N; i++)
    {
        ret = pthread_join(tid[i], &return_value); // 阻塞等待子线程,并获取返回状态
        if(ret != 0)
        {
            fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
			exit(1);
        }
        printf("Join No.%d thread successfully!\n", (int)return_value); // 将 i 强转为 int 类型
    }

    pthread_exit(NULL);
}

在这里插入图片描述

6.pthread_detach()、pthread_cancel()函数

int pthread_detach(pthread_t thread);

实现线程分离;
注意:不能对一个已经处于线程分离状态的线程调用 pthread_join(),否则会出错:Invalid argument;
也可使用 pthread_create() 函数的参数2(线程属性)来设置线程分离

pthread_t thread 待分离线程的线程号 tid
返回值 成功时返回 0,失败返回 errno

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

int pthread_cancel(pthread_t thread);

杀掉一个线程,但不是立即杀掉,该函数工作的必要条件是进入内核;
注意:该函数需要一个契机进入内核才能杀死线程,即需要有系统调用(取消点)进入内核,才能杀掉;
我们可以手动设置取消点,就是 pthread_testcancel() 函数;
被杀掉的线程将没有返回状态

pthread_t thread 要杀掉的线程的线程号 tid
返回值 成功时返回 0,失败返回 errno

7.线程属性设置分离线程

1️⃣int pthread_attr_init(pthread_attr_t *attr);

初始化线程属性

pthread_attr_t *attr 传出参数,返回初始化后的线程属性
返回值 成功时返回 0,失败返回 errno


2️⃣int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

设置线程属性

pthread_attr_t *attr 传出参数,返回设置好的 attr
int detachstate 要为 attr 设置的属性,使用 即可

int detachstate含义
PTHREAD_CREATE_DETACHED使使用 attr 创建的线程将在进程分离状态下创建
PTHREAD_CREATE_JOINABLE使使用 attr 创建的线程将在可连接状态下创建

修改好进程属性后,将 attr 作为 pthread_create() 的参数 2 来创建线程即可

返回值 成功时返回 0,失败返回 errno


3️⃣int pthread_attr_destroy(pthread_attr_t *attr);

销毁线程属性所占用的资源;
attr 使用完后,销毁该 attr 即可

pthread_attr_t *attr 传出参数,
返回值 成功时返回 0,失败返回 errno

例如

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

void *tfunc(void *arg)
{
    printf("I'm thread, tid = %lu\n", pthread_self());

    pthread_exit(NULL);
}

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

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

    ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // attr 加入线程分离属性
    if(ret != 0)
    {
        fprintf(stderr, "pthread_attr_setdetachstate error: %s\n", strerror(ret));
        exit(1);
    }

    ret = pthread_create(&tid, &attr, tfunc, NULL); // 用 attr 创建子线程
    if(ret != 0)
    {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(1);
    }

    ret = pthread_attr_destroy(&attr); // 销毁 attr
    if(ret != 0)
    {
        fprintf(stderr, "pthread_attr_destroy error: %s\n", strerror(ret));
        exit(1);
    }

    ret = pthread_join(tid, NULL); // 等待子线程,若报错,说明子线程设置线程分离成功
    if(ret != 0)
    {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(1);
    }

    pthread_exit(NULL);
}

发现 pthread_join error,故实现了线程分离
在这里插入图片描述

8.使用线程的注意事项
  • 主线程退出而其他线程不退出,主线程应调用 pthread_exit()
  • pthread_join() 可避免僵尸线程,为线程指定线程分离属性也可以避免
  • mallocmmap 所申请的内存可以被其他线程释放
  • 应避免在多线程模型中使用 fork(),除非立刻调用 exec 族函数;fork 出来的子进程只有调用 fork 的线程存在,其他线程均 pthread_exit
  • 信号的复杂语义很难和多线程共存,应避免在多线程中引入信号机制,因为向一个进程发送一个信号,并不知道由哪个线程来处理该信号,除非其他线程设置该信号屏蔽(线程的信号屏蔽字非共享)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值