【Linux系统编程】Linux线程浅析(linux线程detach)
ahcoder 2025-01-23 14:57 60 浏览
01. 进程和线程区别
在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。
为了让进程完成一定的工作,进程必须至少包含一个线程。
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是资源分配的最小单位。
线程存在与进程当中,是操作系统调度执行的最小单位。说通俗点,线程就是干活的。
如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。
线程就是个无产阶级,但无产阶级干活,总得有自己的劳动工具吧,这个劳动工具就是栈,线程有自己的栈,这个栈仍然是使用进程的地址空间,只是这块空间被线程标记为了栈。每个线程都会有自己私有的栈,这个栈是不可以被其他线程所访问的。
进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler,等;
线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集,等;
然而,一直以来,linux 内核并没有线程的概念。每一个执行实体都是一个 task_struct 结构,通常称之为进程。
进程是一个执行单元,维护着执行相关的动态资源。同时,它又引用着程序所需的静态资源。通过系统调用 clone 创建子进程时,可以有选择性地让子进程共享父进程所引用的资源,这样的子进程通常称为轻量级进程。
linux 上的线程就是基于轻量级进程,由用户态的 pthread 库实现的。使用 pthread 以后,在用户看来,每一个 task_struct 就对应一个线程,而一组线程以及它们所共同引用的一组资源就是一个进程。
但是,一组线程并不仅仅是引用同一组资源就够了,它们还必须被视为一个整体。
对此,POSIX 标准提出了如下要求:
1)查看进程列表的时候,相关的一组 task_struct 应当被展现为列表中的一个节点;
2)发送给这个“进程”的信号(对应 kill 系统调用), 将被对应的这一组 task_struct 所共享,并且被其中的任意一个“线程”处理;
3)发送给某个“线程”的信号(对应 pthread_kill), 将只被对应的一个task_struct接收,并且由它自己来处理;
4)当“进程”被停止或继续时(对应 SIGSTOP/SIGCONT 信号), 对应的这一组 task_struct 状态将改变;
5)当“进程”收到一个致命信号(比如由于段错误收到 SIGSEGV 信号), 对应的这一组 task_struct 将全部退出;
6)等等(以上可能不够全)
02. LinuxThreads
在 linux 2.6 以前,pthread 线程库对应的实现是一个名叫 LinuxThreads 的 lib。
LinuxThreads 利用前面提到的轻量级进程来实现线程,但是对于 POSIX 提出的那些要求,LinuxThreads 除了第 5 点以外(当“进程”收到一个致命信号(比如由于段错误收到 SIGSEGV 信号), 对应的这一组 task_struct 将全部退出),都没有实现(实际上是无能为力):
1)如果运行了 A 程序,A 程序创建了 10 个线程,那么在 shell 下执行 ps 命令时将看到 11 个 A 进程,而不是 1 个(注意, 也不是10个,下面会解释);
2)不管是 kill 还是 pthread_kill,信号只能被一个对应的线程所接收;
3)SIGSTOP/SIGCONT 信号只对一个线程起作用;
还好 LinuxThreads 实现了第 5 点,我认为这一点是最重要的。如果某个线程“挂”了,整个进程还在若无其事地运行着,可能会出现很多的不一致状态。进程将不是一个整体,而线程也不能称为线程。
或许这也是为什么 LinuxThreads 虽然与 POSIX 的要求差距甚远,却能够存在,并且还被使用了好几年的原因吧~
但是,LinuxThreads 为了实现这个“第 5 点”, 还是付出了很多代价,并且创造了 LinuxThreads 本身的一大性能瓶颈。
接下来要说说,为什么 A 程序创建了 10 个线程,但是 ps 时却会出现 11 个 A 进程了。 因为 LinuxThreads 自动创建了一个管理线程。上面提到的“第5点”就是靠管理线程来实现的。
当程序开始运行时, 并没有管理线程存在(因为尽管程序已经链接了 pthread 库, 但是未必会使用多线程)。 程序第一次调用 pthread_create 时,LinuxThreads 发现管理线程不存在,于是创建这个管理线程。这个管理线程是进程中的第一个线程(主线程)的儿子。然后在 pthread_create 中,会通过 pipe 向管理线程发送一个命令,告诉它创建线程。即是说,除主线程外,所有的线程都是由管理线程来创建的,管理线程是它们的父亲。
于是,当任何一个子线程退出时,管理线程将收到 SIGUSER1 信号(这是在通过 clone 创建子线程时指定的)。管理线程在对应的 sig_handler 中会判断子线程是否正常退出,如果不是,则杀死所有线程,然后自杀。
那么,主线程怎么办呢? 主线程是管理线程的父亲,其退出时并不会给管理线程发信号。 于是,在管理线程的主循环中通过 getppid 检查父进程的 ID 号,如果 ID 号是 1,说明父亲已经退出,并把自己托管给了 init 进程(1 号进程)。这时候,管理线程也会杀掉所有子线程,然后自杀。
那么,如果主线程是调用 pthread_exit 主动退出的呢? 按照 posix 的标准,这种情况下其他子线程是应该继续运行的。于是,在 LinuxThreads 中,主线程调用 pthread_exit 以后并不会真正退出,而是会在 pthread_exit 函数中阻塞等待所有子线程都退出了, pthread_exit 才会让主线程退出。(在这个等等过程中,主线程一直处于睡眠状态。)
可见,线程的创建与销毁都是通过管理线程来完成的,于是管理线程就成了 LinuxThreads 的一个性能瓶颈。线程的创建与销毁需要一次进程间通信,一次上下文切换之后才能被管理线程执行,并且多个请求会被管理线程串行地执行。
03. NPTL
到了 linux 2.6,glibc 中有了一种新的 pthread 线程库 —— NPTL(Native POSIX Threading Library)。
NPTL 实现了前面提到的 POSIX 的全部5点要求:
1)查看进程列表的时候,相关的一组 task_struct 应当被展现为列表中的一个节点;
2)发送给这个“进程”的信号(对应 kill 系统调用), 将被对应的这一组 task_struct 所共享,并且被其中的任意一个“线程”处理;
3)发送给某个“线程”的信号(对应 pthread_kill), 将只被对应的一个task_struct接收,并且由它自己来处理;
4)当“进程”被停止或继续时(对应 SIGSTOP/SIGCONT 信号), 对应的这一组 task_struct 状态将改变;
5)当“进程”收到一个致命信号(比如由于段错误收到 SIGSEGV 信号), 对应的这一组 task_struct 将全部退出;
但是,实际上,与其说是 NPTL 实现了,不如说是linux内核实现了。
在 linux 2.6 中,内核有了线程组的概念,task_struct 结构中增加了一个 tgid(thread group id)字段。 如果这个 task 是一个“主线程”, 则它的 tgid 等于 pid,否则 tgid 等于进程的 pid(即主线程的 pid)。
在 clone 系统调用中,传递 CLONE_THREAD 参数就可以把新进程的 tgid 设置为父进程的 tgid(否则新进程的 tgid 会设为其自身的 pid)。
类似的 XXid 在 task_struct 中还有两个:task->signal->pgid 保存进程组的打头进程的 pid、task->signal->session 保存会话打头进程的 pid 。通过这两个 id 来关联进程组和会话。
有了 tgid,内核或相关的 shell 程序就知道某个 tast_struct 是代表一个进程还是代表一个线程,也就知道在什么时候该展现它们,什么时候不该展现(比如在 ps 的时候,线程就不要展现了)。而 getpid(获取进程 ID)系统调用返回的也是 tast_struct 中的 tgid,而 tast_struct 中的 pid 则由 gettid 系统调用来返回。
在执行 ps 命令的时候不展现子线程,也是有一些问题的。比如程序 a.out 运行时,创建了一个线程。假设主线程的 pid 是 10001、子线程是 10002(它们的 tgid 都是10001)。这时如果你 kill 10002,是可以把 10001 和 10002 这两个线程一起杀死的,尽管执行 ps 命令的时候根本看不到 10002 这个进程。如果你不知道 linux 线程背后的故事,肯定会觉得遇到灵异事件了。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
void *fun(void *arg)
{
printf("thread is created!\n");
pause(); //挂起线程
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, fun, NULL);
pause();//主线程挂起(否则主线程终止,子线程也就挂了)
return 0;
}
这个程序创建一个线程后挂起,子线程在输出 “thread is created!” 也挂起。运行结果如下
deng@itcast:/mnt/hgfs/LinuxHome/code.bak2$ gcc 1.c -pthread
deng@itcast:/mnt/hgfs/LinuxHome/code.bak2$ ./a.out
thread is created!
我们打开另外一个终端,查看后台运行的进程,发现 a.out的进程号(pid)是 4183,我们使用 kill 终止 pid 为 4184的进程(注意 ps 中并没有这个进程),如下
deng@itcast:~$ ps aux | grep a.out
deng 4183 0.0 0.0 80424 876 pts/3 Sl+ 18:02 0:00 ./a.out
deng 4186 12.0 0.0 21536 1056 pts/4 S+ 18:02 0:00 grep --color=auto a.out
deng@itcast:~$ kill 4184
deng@itcast:~$
结果发现,demo 进程也终止了,如下图。其原因就是 2362 就是所创建线程的线程号,线程异常终止了,其对应的进程也就终止了。
deng@itcast:/mnt/hgfs/LinuxHome/code.bak2$ gcc 1.c -pthread
deng@itcast:/mnt/hgfs/LinuxHome/code.bak2$ ./a.out
thread is created!
已终止
deng@itcast:/mnt/hgfs/LinuxHome/code.bak2$
为了应付“发送给进程的信号”和“发送给线程的信号”, task_struct 里面维护了两套 signal_pending,一套是线程组共享的,一套是线程独有的。
通过 kill 发送的信号被放在线程组共享的 signal_pending 中,可以由任意一个线程来处理;通过 pthread_kill 发送的信号(pthread_kill 是 pthread 库的接口,对应的系统调用中 tkill)被放在线程独有的 signal_pending 中, 只能由本线程来处理。
当线程停止/继续,或者是收到一个致命信号时,内核会将处理动作施加到整个线程组中。
04. NGPT
说到这里,也顺便提一下 NGPT(Next Generation POSIX Threads)。
上面提到的两种线程库使用的都是内核级线程(每个线程都对应内核中的一个调度实体),这种模型称为 1:1 模型(1 个线程对应 1 个内核级线程);
而 NGPT 则打算实现 M:N 模型(M 个线程对应 N 个内核级线程),也就是说若干个线程可能是在同一个执行实体上实现的。 线程库需要在一个内核提供的执行实体上抽象出若干个执行实体,并实现它们之间的调度。这样被抽象出来的执行实体称为用户级线程。
大体上,这可以通过为每个用户级线程分配一个栈,然后通过 longjmp 的方式进行上下文切换。(百度一下"setjmp,longjmp", 你就知道。)
但是实际上要处理的细节问题非常之多,目前的 NGPT 好像并没有实现所有预期的功能,并且暂时也不准备去实现。
用户级线程的切换显然要比内核级线程的切换快一些,前者可能只是一个简单的长跳转,而后者则需要保存/装载寄存器,进入然后退出内核态。(进程切换则还需要切换地址空间等)
而用户级线程则不能享受多处理器,因为多个用户级线程对应到一个内核级线程上,一个内核级线程在同一时刻只能运行在一个处理器上。
不过,M:N 的线程模型毕竟提供了这样一种手段,可以让不需要并行执行的线程运行在一个内核级线程对应的若干个用户级线程上,可以节省它们的切换开销。
据说一些类 UNIX 系统(如 Solaris)已经实现了比较成熟的 M:N 线程模型,其性能比起 linux 的线程还是有着一定的优势。
相关推荐
- Linux基础运维篇:Linux日志管理(第013课)
-
日志是什么Linux系统中的日志就像是一个记录员,它会把系统中发生的各种事情都记录下来,比如系统什么时候启动了、哪些用户登录了系统、运行了哪些程序、程序有没有出错、硬件有没有问题等等。这些记录对于系...
- 目标取代deb包格式,深度Linux操作系统公布“如意玲珑”项目
-
IT之家7月16日消息,7月13日,在上海站的deepinMeetup如意玲珑专场上,深度操作系统官方正式宣布了项目全新的品牌名称——如意玲珑(Linyaps)。目前如意玲珑已...
- 基于 Linux 快速搭建私有化 Helm Chart 仓库(ChartMuseum 实战指南)
-
一、前言在Kubernetes中,Helm是最流行的包管理工具,用于简化应用部署与管理。如果你希望在内网环境中部署、管理自己的HelmCharts,就需要搭建一个私有化HelmChart...
- Linux环境开发效率--那些强大的命令
-
之前写了一篇关于linux下常用的几个命令,参考Linux入门--提高工作效率的命令。接下来接着介绍一些在linux下做开发经常使用到得命令,其中有打包压缩命令tar,文件查找命令find,文件内容查...
- 怎么使用再生龙打包(备份)liunx系统
-
liunx系统区别于Windows,由于分区格式的特殊性并不能像GHOST那样进行备份封包,有款开源的针对liunx系统备份还原的软件-再生龙(clonezilla),可能有部分人已经用过,本篇文章让...
- python打包那些事儿(python的打包)
-
python是个万金油,什么活都能干,开发一些小应用的时候,要想其他人也方便使用,就需要对程序进行打包生成可执行文件,不再依赖python环境或者其他文件。前几天写了一个视频去水印的小程序,主要依赖f...
- 「项目部署」使用Jenkins一键打包部署SpringBoot应用
-
前言嗨,大家好,我是希留,一个被迫致力于全栈开发的老菜鸟。一般而言,一个项目部署的由:拉取代码->构建->测试->打包->部署等过程组成,如果我们经常需要部署项目,特别是在微服...
- Linux面试最高频的5个基本问题(linux面试题必会题目)
-
CPU利用率和CPU负载的区别是什么提到CPU利用率,就必须理解时间片。什么是CPU时间片?我们现在所使用的Windows、Linux、MacOS都是“多任务操作系统”,就是说他们可以“同时”运行多...
- jtti:如何将Linux服务器的文件系统创建成镜像
-
如何将Linux服务器的文件系统创建成镜像?如何把Linux文件系统创建成一个镜像文件,也就是"系统打包"?这类操作不仅适用于迁移、备份,还能用于批量部署、自定义系统安装,甚至可以作为...
- 用Nuitka打包 Python,效果竟如此惊人!
-
使用Python开发一个程序后,将Python脚本打包成独立可执行文件是一项常见需求。Nuitka是一个Python到C的编译器,它会将Python代码转换为等效的C代码,然后使用标...
- Linux基础运维篇:Linux软件包管理(第014课)
-
什么是软件包管理在Linux系统中,软件包管理就像是一个“软件管家”,负责软件的安装、卸载、更新以及软件之间依赖关系的处理。它把软件及其相关的文件、配置信息等打包在一起,方便用户进行管理和操作。...
- 给你的Nodejs后端项目打个包(node为基础的后端框架)
-
前阵子,碰到个问题用Nodejs做了个小项目需要交付客户使用之前Nodejs做的都是自用,所以也没碰到需要打包保护源码之类的问题上网搜了半天,找到的大多是针对Vue的前端项目打包/混淆,例如用...
- 爆强!直接把 Python 编写的图形程序打包为安卓 APP
-
请大家多多关注点赞哦如果想使用Python语言编写图形界面程序,那么有不少的框架可以提供支持,比如Tkinter、QtforPython、WxPython等等。不过这些框架都是只能创建桌面图...
- 几行代码教你zip打包(打包zip命令)
-
01准备有时我们不想去手动一个个去操作,然后傻等他打包完,python依赖库zipfile很方便地帮助我们封装了解压压缩,shutil用于文件目录处理,方法类似于linux命令。1、安装pipin...
- Linux基础运维篇:Linux文件操作超详细版(第007课)
-
一、Linux文件系统基础1.文件类型Linux一切皆文件,常见文件类型:普通文件(-):文本文件(.txt)、二进制文件(可执行程序)、压缩文件等。目录文件(d):用于组织文件和子目录(类似...
- 一周热门
- 最近发表
- 标签列表
-
- linux 远程 (37)
- u盘 linux (32)
- linux 登录 (34)
- linux 路径 (33)
- linux 文件命令 (35)
- linux 是什么 (35)
- linux 界面 (34)
- 查看文件 linux (35)
- linux 语言 (33)
- linux代码 (32)
- linux 查看命令 (33)
- 关闭linux (34)
- root linux (33)
- 删除文件 linux (35)
- linux 主机 (34)
- linux与 (33)
- linux 函数 (35)
- linux .ssh (35)
- cpu linux (35)
- 查看linux 系统 (32)
- linux 防火墙 (33)
- linux 手机 (32)
- linux 镜像 (34)
- linux mac (32)
- linux ip地址 (34)