本文共 11104 字,大约阅读时间需要 37 分钟。
TASK_RUNNING
定义。Task_struct
是如何被管理的呢?SIGCHLD
信号处理函数调用wait()/waitpid()
等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。如果父进程不会结束,子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。系统所能使用的进程号是有限的(cat /proc/sys/kernel/pid_max
),如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。wait/waitpid
等函数等待子进程结束,这会导致父进程挂起。SIGCHLD
信号:子进程结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。pthread_exit
。pthread_detach
。pthread_cancel()
允许一个线程取消 th 指定的另一个线程。进程是资源分配的基本单位,线程是调度的基本单位
Linux调度器实际是识别task_struct
进行调度。无论进程线程,底层都对应一个task_struct,进程和线程的区别是共享资源的多少,两个进程间完全不共享资源,两个线程间共享所有资源。
task_struct
就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符
」。存放着进程所需要的所有资源的结构的描述。/proc/${pid}
进程相关信息。对于操作系统,进程就是一个数据结构。
struct task_struct { long state; // 进程状态 -1为不可运行, 0为可运行, >0为已中断 struct mm_struct *mm; // 指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方 pid_t pid; // 进程标识符,用来代表一个进程 struct task_struct __rcu *parent; // 指向父进程的指针 struct list_head children; // 子进程列表 struct list_head sibling; // 兄弟进程 struct fs_struct *fs; // 存放文件系统信息的指针 struct files_struct *files; // 一个数组,包含该进程打开的文件指针 unsigned int policy; // 调度策略:一般有FIFO,RR,CFS ...};
从2.6版本以后,Linux改用了slab分配器
动态生成task_struct, 只需要在栈底(向下增长的栈)或栈顶(向上增长的栈)创建一个新的结构struct thread_info(这里是栈对象的尾端),你可以把slab分配器认为是一种分配和释放数据结构的优化策略。通过预先分配和重复使用task_struct, 可以避免动态分配和释放带来的资源消耗。
所谓进程地址空间(process address space),就是从进程的视角看到的地址空间,是进程运行时所用到的虚拟地址的集合。
程序段(Text)
:程序代码在内存中的映射,存放函数体的二进制代码。
初始化过的数据(Data)
:在程序运行初已经对变量进行初始化的数据。
未初始化过的数据(BSS)
:在程序运行初未对变量进行初始化的数据。
栈 (Stack)
:存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。
堆 (Heap)
:存储动态内存分配,需要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式类似于链表。
注:1.Text, BSS, Data段在编译时已经决定了进程将占用多少VM
可以通过size,知道这些信息:
创建进程后都创建了哪些资源
system()
通过调用shell启动一个新进程
exec()
以替换当前进程映像的方式启动一个新进程 fork()
以复制当前进程映像的方式启动一个新进程 fork(2)
执行fork后,父进程的task_struck
对拷给子进程,父子进程最初资源完全一样,但是是两份不同的拷贝,因此任何改动都造成二者的分裂。
COW(Copy-On-Write, 写时拷贝)
技术: MMU(Memory Management Unit, 内存管理单元)
管理。fork运行在有MMU的CPU上。 对于无MMU的CPU,无法应用COW,无法支持fork。无MMU的CPU使用vfork创建进程,父进程将一直阻塞直到子进程exit或exec。vfork和fork的本质区别是,vfork中的父子进程共用同一片内存区。 系统调用用于创建一个新进程,称为子进程,它与父进程同时运行(并发),且运行顺序不定(异步)。pid_t
是一个宏定义,其实质是 int,若成功则返回两个值,子进程返回 0,父进程返回子进程 ID;否则,出错返回 -1
父级的整个虚拟地址空间在子进程中复制,包括互斥锁的状态,
子进程与父进程完全相同,除了以下几点:
wait()
和waitpid()
通过 waitpid()/wait()
回收子进程的 task_struct
结构。
区别:
waitpid(-1, &status, 0)
== wait(&status)
, wait() 是 waitpid() 的一个子集。僵尸进程
:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。(子进程可以通过/proc/$pid
看到,线程没有)
wait()/waitpid()
等待子进程结束,这样处理父进程一般会阻塞在wait处而不能处理其他事情。SIGCHLD
信号,并在信号处理函数里面调用wait函数,这样处理可避免1中描述的问题。孤儿进程
:父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(pid=1)所收养,并由init进程对它们完成状态收集工作。(如果系统中出现孤儿进程,说明主进程在退出前没有清理子进程)
同一进程的多个线程获取进程ID时得到的是唯一ID值。Linux同一进程的多线程,在内核视角实际上每个线程都有一个PID,但在用户空间需要getpid()返回唯一值,Linux使用了一个小技巧,引入了TGID的概念,getpid()返回的的TGID值。
task_struct
,因此每个线程都可被CPU调度。多线程间又共享同一进程资源。 在一个线程中创建了另外一个线程,主线程要等到创建的线程返回了,获取该线程的返回值后主线程才退出。这个时候就需要用到线程挂起。pthread_join
函数用于挂起当前线程,直至指定的线程终止为止。
说线程的PID,是指用户空间的进程ID,值就是TGID (thread group ID for the thread group leader);当特别指出,线程在内核空间的PID,则指线程在内核中task_struct
里特有的PID。top –H
命令从线程视角显示CPU占用率。不带参数的top命令,进程ID是主线程的PID(也就是TGID)。
进程是处于运行期的程序和相关资源的总称,具备一些要素:
# ps -eo ppid,pid,tid,lwp,tgid,pgrp,sid,tpgid,args -L | awk '{if(NR==1) print $0; if($9~/a.out/) print $0}' PPID PID TID LWP TGID PGRP SID TPGID COMMAND 579046 2436128 2436128 2436128 2436128 2436128 579046 2436128 ./a.out 579046 2436128 2436129 2436129 2436128 2436128 579046 2436128 ./a.out 579046 2436128 2436130 2436130 2436128 2436128 579046 2436128 ./a.out
pidstat -t [-p pid号]
可以打印出线程之间的关系。
各种ID最后都归结到pid和lwp(tid)上。所以理解各种ID,最终归结为理解pid和lwp(tid)的联系和区别。
PID: 进程ID。
LWP: 线程ID。在用户态的命令(比如ps)中常用的显示方式。 TID: 线程ID,等于LWP。TID在系统提供的接口函数中更常用,比如syscall(SYS_gettid)
和syscall(__NR_gettid)
。 TGID: 线程组ID,也就是线程组 leader的进程ID,等于 PID。 pgid (process group ID): 进程组ID,也就是进程组leader的进程ID。 pthread id: pthread库提供的ID,生效范围不在系统级别,可以忽略。 sid: session ID for the session leader。 TPGID: tty process group ID for the process group leader。 上图很好地描述了用户视角(user view)和内核视角(kernel view)看到线程的差别: 协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户的程序自己调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的CPU控制权切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。
本质上,goroutine 就是协程。 不同的是,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU(P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。
linux内核的三种主要调度策略:
1,SCHED_OTHER 分时调度策略, 2,SCHED_FIFO实时调度策略,先到先服务 3,SCHED_RR实时调度策略,时间片轮转SCHED_FIFO
:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑;同等优先级先进先出。
SCHED_RR
:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑;同等优先级轮转。 内核RT补丁: 如下两个参数
/proc/sys/kernel/sched_rt_period_us /proc/sys/kernel/sched_rt_runtime_us 表示在period的时间里RT最多只能跑runtime的时间SCHED_OTHER:
红黑树,左边节点小于右边节点的值
运行到目前为止vruntime最小的进程 同时考虑了CPU/IO和nice 总是找vruntime最小的线程调度。 vruntime = pruntime/weight × 1024; vruntime是虚拟运行时间,pruntime是物理运行时间,weight权重由nice值决定(nice越低权重越高),则运行时间少、nice值低的的线程vruntime小,将得到优先调度。这是一个随运行而动态变化的过程。Linux的虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分, 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
Linux使用两级保护机制:0级供内核使用,3级供用户程序使用,每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的,最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外, 使用虚拟地址可以很好的保护 内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。注:多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盒中,这个 沙盒就是虚拟地址空间(virtual address space),在32位模式下,它总是一个4GB的内存地址块。这些虚拟地址通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用。每个进程都拥有一套属于它自己的页表。
进程内存空间分布如下图所示:
堆向上增长,栈向下增长(为啥我的是相反的呢???)
低 ->|-----------------| | 全局量(所有已初始化量 .data, | | 未初始化量 .bss ) | 堆起始->|-----------------| | 堆向高地址增长 | | | | | | 自由空间 | | | | | | 栈向低地址增长 | 高 栈起始->|-----------------|转载地址:http://lviti.baihongyu.com/