linux取消线程的原理,浅析 Linux 进程与线程
简介
进程与线程是所有的程序员都熟知的概念,简单来说进程是一个执行中的程序,而线程是进程中的一条执行路径。进程是操作系统中基本的抽象概念,本文介绍 Linux 中进程和线程的用法以及原理,包括创建、消亡等。
进程
创建与执行
Linux 中进程的创建与执行分为两个函数,分别是 fork 和 exec,如下代码所示:
int main() {
pid_t pid;
if ((pid = fork() < 0) {
printf("fork error\n");
} else if (pid == 0) {
// child
if (execle("/home/work/bin/test1", "test1", NULL) < 0) {
printf("exec error\n");
}
}
// parent
if (waitpid(pid, NULL) < 0) {
printf("wait error\n");
}
}
fork 从当前进程创建一个子进程,此函数返回两次,对于父进程而言,返回的是子进程的进程号,对于子进程而言返回 0。子进程是父进程的副本,拥有与父进程一样的数据空间、堆和栈的副本,并且共享代码段。
由于子进程通常是为了调用 exec 装载其它程序执行,所以 Linux 采用了写时拷贝技术,即数据段、堆和栈的副本并不会在 fork 之后就真的拷贝,只是将这些内存区域的访问权限变为只读,如果父子进程中有任一个要修改这些区域,才会修改对应的内存页生成新的副本,这样子是为了提高性能。
fork 之后父进程先执行还是子进程先执行是不确定的,所以如果要求父子进程进行同步,往往需要使用进程间通信。fork 之后子进程会继承父进程的很多东西,如:
打开的文件
实际用户 ID、组用户 ID 等
进程组
当前工作目录
信号屏蔽和安排
...
父子进程的区别在于:
进程 ID 不同
子进程不继承父进程的文件锁
子进程的未处理信号集为空
...
fork 之后,子进程可以执行不同的代码段,也可以使用 exec 函数执行其它的程序。
进程描述符
进程在运行的时候,除了加载程序,还会打开文件、占用一些资源,并且会进入睡眠等其它状态。操作系统为了支持进程的运行,必然有一个数据结构保存着这些东西。在 Linux 中,一个名为 task_struct 的结构保存了进程运行时的所有信息,称为进程描述符:
struct task_struct {
unsigned long state;
int prio;
pid_t pid;
...
}
进程描述符完整描述了一个进程:打开的文件、进程的地址空间、挂起的信号以及进程的信号等。系统将所有的进程描述符放在一个双端循环列表中:
进程描述符具体存放在内存的哪里呢?在内核栈的末尾。众所周知,进程中占用的内存一部分是栈,主要用于函数调用,不过这里说的栈一般指的是用户空间的栈,其实进程还有内核栈。当进程调用系统调用的时候,进程陷入内核,此时内核代表进程执行某个操作,此时使用的是内核空间的栈。
进程状态
进程描述符中的 state 描述了进程当前的状态,有如下 5 种:
TASK_RUNNING:进程是可执行的,此时进程要么是正在执行,要么是在运行队列中等待被调度
TASK_INTERRUPTIBLE:进程正在睡眠(阻塞),等待条件达成。如果条件达成或者收到信号,进程会被唤醒并且进入可运行状态
TASK_UNINTERRUPTIBLE:进程处于不可中断状态,就算信号也无法唤醒,这种状态用的比较少
_TASK_TRACED:进程正在被其它进程追踪,通常是为了调试
_TASK_STOPPED:进程停止运行,通常是接收到 SIGINT、SIGTSTP 信号的时候。
fork 与 vfork
在使用了写时拷贝后,fork 的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。fork 为了创建一个进程到底做了什么呢?fork 其实调用了 clone,这是一个系统调用,通过给 clone 传递参数,表明父子进程需要共享的资源,clone 内部会调用 do_fork,而 do_fork 的主要逻辑在 copy_process 中,大致有以下几步:
为新进程创建一个内核栈以及 task_struct,此时它们的值与父进程相同
将 task_struct 中某些变量,如统计信息,设置为 0
将子进程状态设置为 TASK_UNINTERRUPTIBLE,保证它不会被投入运行
分配 pid
根据传递给 clone 的参数,拷贝或者共享打开的文件、文件系统信息、信号处理函数以及进程的地址空间等。
返回指向子进程的指针
除了 fork 之外,Linux 还有一个类似的函数 vfork。它的功能与 vfork 相同,子进程在父进程的地址空间运行。不过,父进程会阻塞,直到子进程退出或者执行 exec。需要注意的是,子进程不能向地址空间写入数据。如果子进程修改数据、进行函数调用或者没有调用 exec 那么会带来未知的结果。vfork 在 fork 没有写时拷贝的技术时是有着性能优势,现在已经没有太大的意义。
退出
进程的运行终有退出的时候,有 8 种方式使进程终止,其中 5 中为正常终止:
从 main 返回
调用 exit
调用 _exit 或 _Exit
最后一个线程从其启动例程返回
从最后一个线程调用 pthread_exit
异常终止方式有 3 种:
调用 abort
接收到一个信号
最后一个线程对取消请求作出响应
exit 函数会执行标准 I/O 库的清理关闭操作:对所有打开的流调用 fclose 函数,所有缓冲中的数据会被冲洗,而 _exit 会直接陷入内核。看下面的代码:
#include
#include
#include
int main()
{
printf("line 1\n");
printf("line 2"); // 没有换行符
// exit(0)
_exit(0);
}
其中第二行输出没有 \n,如果末尾调用的是 _exit,则只会输出 line 1,如果替换为 exit,则第二行 line 2 也会输出。
进程退出最终会执行到系统的 do_exit 函数,主要有以下步骤:
删除进程定时器
释放进程占用的页表
递减文件描述符的引用计数,如果某个引用计数为 0,则关闭文件
向父进程发信号,给子进程重新找养父,并且把进程状态设置为 EXIT_ZOMBIE
调度其它进程
此时,进程的大部分资源都被释放了,并且不会进入运行状态。不过还有些资源保持着,主要是 task_struct 结构。之所以要留着是给父进程提供信息,让父进程知道子进程的一些信息,如退出码等。
需要注意的是,如果父进程不进行任何操作,那么这些信息会一直保留在内存中,成为僵尸进程,占用系统资源,如下面的代码:
int main() {
pid_t pid = fork();
if (pid == 0) {
exit(0);
} else {
sleep(10);
}
}
父进程 fork 出子进程后,子进程立刻退出,而父进程则进入睡眠。运行程序,观察进程状态:
可以看到,第一行进程为父进程,状态为 S,表示其正在睡眠,而第二为子进程,状态为 Z,表示僵尸状态(zombie),因为此时子进程已经退出,然而 task_struct 还保存着,等待父进程来处理。
父进程如何处理?调用 wait 函数,正如本文第一段代码中所示。当父进程调用 wait 后,子进程的 task_struct 才被释放。
如果父进程先结束了呢?在父进程结束的时候,会为其子进程找新的父进程,一直往上找,最终成为 init 进程的子进程。init 子进程会负责调用 wait 释放子进程的遗留信息。
线程
上面介绍了 Linux 中的进程,那么线程又是怎么的?网上一些说法是,Linux 中并没有真正的内核线程,线程是以进程的方式实现的,只不过它们之间会共享内存。这种说法有一定道理,但并不完全准确。
Linux 中刚开始是不支持线程的,后来出现了线程库 LinuxThreads,不过它有很多问题,主要是与 POXIS 标准不兼容。自 Linux 2.6 以来,Linux 中使用的就是新的线程库,NPTL(Native POSIX Thread Library)。
NPTL 中线程的创建也是通过 clone 实现的,并且通过以下的参数表明了线程的特征:
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS |
CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
部分参数的含义如下:
CLONE_VM:所有线程都共享同一个进程地址空间
CLONE_FILES:所有线程都共享进程的文件描述符列表
CLONE_THREAD:所有线程都共享同一个进程 ID 以及 父进程 ID
NPTL 所实现的线程库是 1:1 的从用户线程映射到内核线程,并且内核为了实现 POSIX 的线程标准也做了一些改动,比如对于信号的处理等。所以说 Linux 内核完全不区分进程和线程,甚至不知道线程的存在这种说法现在是不准确的。
线程间共享代码段、堆以及打开的文件等,线程私有的部分有以下内容:
线程 ID
寄存器
错误码(errno)
栈
信号屏蔽
...
总结
Linux 中进程与线程的使用是程序员必备的技能,而如果能了解一些实现的原理,则可以使用的更加得心应手。本文介绍了 Linux 中进程的创建、执行以及消亡等,对于线程的实现及其与进程的关系也进行了简单的说明。进程和线程还有更多的内容可以研究,如进程调度、进程以及线程间的通信等。
参考
《UNIX 环境高级编程》
《Linux 内核设计与实现》
linux取消线程的原理,浅析 Linux 进程与线程相关推荐
- linux知识(一) 程序、进程与线程
linux知识(一) 程序.进程与线程 程序 进程 程序如何变成进程? 线程 线程与进程 fork和创建新线程的区别 优点 程序 程序:程序是已编译好的二进制文件,存储在磁盘中,不占用系统资源 程序包 ...
- 进程、线程、协程 关于进程、线程、协程,有非常详细和丰富的博客或者学习资源,我不在此做赘述,我大致在此介绍一下这几个东西。 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
进程.线程.协程 关于进程.线程.协程,有非常详细和丰富的博客或者学习资源,我不在此做赘述,我大致在此介绍一下这几个东西. 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度. 线程 ...
- linux 时间与定时器编程原理,浅析 Linux 中的时间编程和实现原理-嵌入式-火龙果软件工程...
引子 我们都生活在时间中,但却无法去思考它.什么是时间呢?似乎这是一个永远也不能被回答的问题.然而作为一个程序员,在工作中,总有那么几次我必须思考什么是时间.比如,需要知道一段代码运行了多久:要在 l ...
- cef linux 多线程模式,CEF3开发者系列之进程和线程
CEF3是一个多进程架构框架,如果有了解过chromium的进程架构的,那么就很容易了解CEF3的多进程了.打开CEF3源代码中发布的cefclient实例,如果打开的页面带有flash或者其他插件. ...
- linux进程线程协程的区别,进程和线程、协程的区别
现在多进程多线程已经是老生常谈了,协程也在最近几年流行起来.python中也有协程库,tornado中也用了gevent封装好的协程.本文主要介绍进程.线程和协程三者之间的区别. 一.概念 1.进程 ...
- 【Linux】Shell运行原理及Linux权限的概念
文章目录 一.Shell运行原理 二.Linux权限的概念 1)Linux中的用户 2)Linux权限管理 1.文件访问者的分类(人) 2.文件的权限(事物属性) ① 文件类型 ② 文件基本权限 ④ ...
- linux内核定时器死机,浅析linux内核中timer定时器的生成和sofirq软中断调用流程
浅析linux内核中timer定时器的生成和sofirq软中断调用流程 mod_timer添加的定时器timer在内核的软中断中发生调用,__run_timers会spin_lock_irq(& ...
- linux查看共享内存max,浅析Linux的共享内存与tmpfs文件系统
浅析Linux的共享内存与tmpfs文件系统 前言 共享内存主要用于进程间通信,Linux有两种共享内存(Shared Memory)机制: (1)** System V shared memory( ...
- java 线程亲缘性_CPU affinity 进程和线程的亲缘性
设置Processor Affinity 作用: 1.进程和线程的亲缘性(affinity),使进程或线程在指定的CPU(核)上运行.(比如程序A,在第4个核心上运行) 2.设置进程 或者 线程, 使 ...
最新文章
- 教你用java统计目录下所有文档的词频
- hdu-3033-I love sneakers!--背包
- 使用snmp4j实现Snmp功能(一)
- 企业被黑客攻击,“怼回去”合法吗?
- echarts 关系 两个多个关系_情感归宿:异性聊天时,常用这些结束语的两个人,关系很难清白...
- 寻路之 A* 搜寻算法
- 面向对象基础知识四:关联关系
- 三星android o测试版,三星美版 Galaxy Note8 N950U 升级安卓8.0测试版固件和教程
- RC522(RFID模块)实践总结
- python使用matplotlib 画柱状图代码_Python 使用 matplotlib 画柱状图教程
- 数据架构建设方法及案例
- 沃顿研究数据服务推出高级研究学者计划,为全球研究人员提供实地教育课程
- AI高考的信息检索策略
- 计算机应届毕业生怎么获得BATJ实习转正机会呢?
- 传感器技术(徐科军 第四版)第三章:变电抗式传感器
- 对excel表格按照某个字段拆分
- matlab中的电子器件,对电力电子器件控制设计进行硬件在环测试
- heart(js源码)
- Windows平台分布式架构实践 - 负载均衡
- 荣耀v20支持html,荣耀V20支持NFC刷公交吗 荣耀V20支持NFC功能吗
热门文章
- Modbus协议栈开发笔记之二:Modbus消息帧的生成
- iPhone各版本屏幕尺寸
- 查看函数库.a函数符号信息
- 顶级程序员的心得ndash;Coders at Work
- JAVA入门级教学之(classpath的配置)
- node获取服务器cpu信息,听说你不知道如何监控Node服务的内存?
- unique函数_unique函数使用场景(一)
- supervisor 重启_supervisor_twiddler的使用
- lnmp php文件访问不了,记一次lnmp环境下无法执行php文件
- python修复不了_如何修复Python代码?