运行一个c程序,相当于哪个系统调用 – 用execve()系统调用。
进程和线程?
ps 查看所有子进程?pstree -p pid
ps -eLf各字段含义?
PID:process id 进程id
PPID: parent process id 父进程id
LWP:表示这是个线程;要么是主线程(进程),要么是线程
NLWP: num of light weight process 轻量级进程数量,即线程数量
TIME: 占用的CPU总时间
C CPU利用率,以整数表示。
查看进程下的所有线程cpu利用率/内存/优先级等信息?top -H -p 25120
查看线程在哪个CPU上运行?ps -eo ruser,pid,ppid,lwp,psr,args -L |grep l3-agent
常见的进程状态?
-D:不可被唤醒的睡眠状态,通常用于 I/O 情况。
-R:该进程正在运行。
-S:该进程处于睡眠状态,可被唤醒。
-T:停止状态,可能是在后台暂停或进程处于除错状态。
-W:内存交互状态(从 2.6 内核开始无效)。
-X:死掉的进程(应该不会出现)。
-Z:僵尸进程。进程已经中止,但是部分程序还在内存当中。
-<:高优先级(以下状态在 BSD 格式中出现)。
-N:低优先级。
-L:被锁入内存。
-s:包含子进程。
-l:多线程(小写 L)。
-+:位于后台。
进程的六种状态及其转换过程(生命周期)——就绪态,运行态,深度睡眠态,浅睡眠态,停止态,僵尸态。
父子进程共享的是?不同的是?——fork后,父子进程共享有(文件描述符、mmap建立的映射区。其他都是复制了一份,只是该进程的进程空间甚至进程地址完全跟父进程看着一样,但是完全是独立的空间了)
fork与vfork的区别——vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。区别一:不将父进程的地址空间复制到子进程。区别二:vfork保证子进程先运行,在它调用exec或(exit)之后父进程才可能被调度运行。
**共享内存的地址值是在用户进程空间范围之内么?**fork之后子进程使用该地址和父进程是一样的么?如果是不同的进程,共享内存的进程空间中显示的地址是一样的么?
进程的生命周期,就绪态和运行态在数值上是相等的,都是由宏TASK_RUNNING
定义。
Linux中Task_struct
是如何被管理的呢?
理解僵尸进程——放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置(资源已经释放,Task_struct结构还在)。如果他的父进程没安装SIGCHLD
信号处理函数调用wait()/waitpid()
等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。如果父进程不会结束,子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。系统所能使用的进程号是有限的(cat /proc/sys/kernel/pid_max
),如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
子进程回收:
wait/waitpid
等函数等待子进程结束,这会导致父进程挂起。子进程结束后为什么要进入僵尸状态?——因为父进程可能要取得子进程的退出状态等信息。
僵尸状态是每个子进程必经的状态吗?——任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构(它占用一点内存 资源,也就是进程表里还有一个记录),等待父进程处理。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
僵尸进程清除的方法:
SIGCHLD
信号:子进程结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。进程终止时exit()函数,那么线程终止是什么呢?——线程终止的三种情况:
pthread_exit
。如果不等待一个线程,同时对线程的返回值不感兴趣,可以设置这个线程为被分离状态,让系统在线程退出的时候自动回收它所占用的资源。一个线程不能自己调用pthread_detach改变自己为被分离状态,只能由其他线程调用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
对拷给子进程,父子进程最初资源完全一样,但是是两份不同的拷贝,因此任何改动都造成二者的分裂。
父子进程对内存资源(mm)的管理使用了COW(Copy-On-Write, 写时拷贝)
技术:
MMU(Memory Management Unit, 内存管理单元)
管理。fork运行在有MMU的CPU上。fork(2) 系统调用用于创建一个新进程,称为子进程,它与父进程同时运行(并发),且运行顺序不定(异步)。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
看到,线程没有)
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保 留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸。
如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。
但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程.
wait()/waitpid()
等待子进程结束,这样处理父进程一般会阻塞在wait处而不能处理其他事情。SIGCHLD
信号,并在信号处理函数里面调用wait函数,这样处理可避免1中描述的问题。孤儿进程
:父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(pid=1)所收养,并由init进程对它们完成状态收集工作。(如果系统中出现孤儿进程,说明主进程在退出前没有清理子进程)
同一进程的多个线程获取进程ID时得到的是唯一ID值。Linux同一进程的多线程,在内核视角实际上每个线程都有一个PID,但在用户空间需要getpid()返回唯一值,Linux使用了一个小技巧,引入了TGID的概念,getpid()返回的的TGID值。
Linux 线程本质上就是进程,只是与进程间资源共享方式不同,线程间共享所有资源。每个线程都有自己的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 ) |
堆起始->|-----------------|
| 堆向高地址增长 |
| |
| |
| 自由空间 |
| |
| |
| 栈向低地址增长 |
高 栈起始->|-----------------|