十四、Linux之守护进程、线程
目录:
- 十四、Linux之守护进程、线程
- kill -SIGKILL -进程组ID
- pid_t setsid(void);
- \ pid_t getsid(pid_t pid);
- ps -Lf [PID]
- ✅共享资源:
- ✅非共享资源:
- int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- \ pthread_t pthread_self(void);
- void pthread_exit(void *retval);
- \ int pthread_join(pthread_t thread, void **retval);
- int pthread_detach(pthread_t thread);
- int pthread_cancel(pthread_t thread);
- 1️⃣int pthread_attr_init(pthread_attr_t *attr);
- \ 2️⃣int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
- \ 3️⃣int pthread_attr_destroy(pthread_attr_t *attr);
一、进程组和会话(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,失败返回 -1、errno
pid_t getsid(pid_t pid);获取指定进程的会话ID
pid_t pid
传 0 时,返回调用该函数的进程的会话ID,> 0 时,返回指定 pid 进程的会话 ID
返回值
成功时返回会话ID,失败返回 -1、errno
例如:创建一个子进程,以该子进程创建新会话,查看进程组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.创建守护进程的模型
-
创建子进程,父进程退出
所有的工作都在子进程中进行,其形式上脱离了控制终端 -
子进程调用
setsid()
创建新的会话
使进程完全独立出来,脱离控制 -
通常根据需要,调用
chdir()
来改变工作目录位置
目的是防止目录被卸载 -
通常根据需要,调用
umask()
函数重设 umask 文件权限掩码
防止影响创建新文件的权限,增加守护进程的灵活性
✳umask:0022;权限:0755
✳umask:0345;权限:0432 -
通常根据需要,关闭/重定向 文件描述符
子进程所继承的 stdin、stdout、stderr 文件的文件描述符默认打开,浪费系统资源,应该关闭
但通常情况下会进行重定向,而不是直接关闭,操作如下:
1️⃣关闭 stdin 文件描述符,使用open()
打开 /dev/null,此时 /dev/null 的文件描述符将占用 0,因为 stdin 被关闭
✳ /dev/null 是一个空洞文件,可以无限写入字节
2️⃣将 stdout 的文件描述符重定向至 0(/dev/null)
3️⃣将 stderr 的文件描述符重定向至 0 (/dev/null) -
开始执行守护进程,业务逻辑:
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()
可避免僵尸线程,为线程指定线程分离属性也可以避免- malloc 和 mmap 所申请的内存可以被其他线程释放
- 应避免在多线程模型中使用
fork()
,除非立刻调用 exec 族函数;fork 出来的子进程只有调用 fork 的线程存在,其他线程均 pthread_exit 了 - 信号的复杂语义很难和多线程共存,应避免在多线程中引入信号机制,因为向一个进程发送一个信号,并不知道由哪个线程来处理该信号,除非其他线程设置该信号屏蔽(线程的信号屏蔽字非共享)