linux操作系统笔记(进程)

0号进程、1号进程、2号进程

0号进程为idel进程,由系统创建,是唯一一个没有通过fork或者kernel_thread产生的进程  Linux idle进程 - 思禽 - 博客园

1号进程为init进程,由0号进程通过kernel_thread创建,在内核空间完成初始化后,加载init程序,最终运行在用户空间,是其他所有用户态进程的祖先进程。init进程是被软连接到systemd的(/sbin/init -> ../lib/systemd/systemd)。init进程的使命:1初始化系统脚本,创建一系列的进程。2在一个死循环中等待子进程的退出事件,调用wait系列系统调用完成“收尸”工作

2号进程为kthreadd,由0号进程通过kernel_thread创建,始终运行在内核空间,负责管理和调度内核线程,是内核线程的祖先

(转)Linux内核本身和进程的区别 内核线程、用户进程、用户线程 - _浪潮之巅.☆ - 博客园

守护进程

deamon是一种运行在后台的一种特殊的进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。由于在Linux中,每个系统与用户进行交流的界面成为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端信息所打断。它从被执行的时候开始运转,直到整个系统关闭才退出(当然可以人为的杀死相应的守护进程)。如果想让某个进程不因为用户或中断或其他变化而影响,那么就必须把这个进程变成一个守护进程。

实现守护进程的两种方法:

1 按守护进程的规则去编程,比较麻烦

2 用普通方法编程,用nohub命令启动程序

进程几种常见的状态

运行态 R(TASK_RUNNING):包括已经在cpu上运行和已经就绪尚未被调度

可中断睡眠状态 S(TASK_INTERRUPTIBLE):进程在等待某事件的发生,比如等待I/O,这些进程的tast_strcut被放入到对应事件的等待队列中,当事件发生时,对应等待队列中的进程将被唤醒

不可中断睡眠状态 D(TASK_UNINTERRUPTIBLE):不可中断并不是CPU不响应外部中断,而是不响应异步信号(kill -9 也杀不死)。D状态的意义是内核中某些关键流程是不能被打断的。D状态跟S状态的区别是:假如等待键盘的输入,键盘还没输入前是S状态。当键盘输入数据时,数据由硬件写到内核的过程中的状态为D状态

暂停状态或跟踪状态 T(TASK_STOP、TASK_TRACED):向进程发送一个SIGSTOP信号后,进程会进入TASK_STOP状态。进程被gdb调试时,会进入TASK_TRACED状态

僵死状态 Z (EXIT_ZOMBIE):处于这个状态时,进程占用的所有资源将被回收,除了task_struct结构,只剩下task_struct这个空壳。之所以保留task_struct,是因为task_struct中保留了进程的退出码和一些统计信息,其父进程很可能关心这些信息。子进程在退出时发送SIGCHIL信号来通知父进程。父进程通过wait系列系统调用来获取退出信息、释放tast_struct结构。危害:占用进程pid号。如何处理僵尸进程:父进程不退出,僵尸进程会一直存在, 可以重启父进程,僵尸进程就会变成孤儿进程,然后由init进程处理

退出态 X (EXIT_DEAD):进程即将被销毁

正确理解cpu的平均负载


查看平均负载

0.63、0.83、0.88代表过去1分钟、5分钟、15分钟的平均负载

平均负载定义:平均负载指单位时间内,系统处于可运行状态和不可中断状态的平均进程数

平均负载为多少比较合理:平均负载最理想的状态是值跟cpu个数相等,每个cpu上刚好有一个进程运行。可通过/proc/cpuinfo查看cpu个数。当平均负载高于cpu个数70%时,需要排查原因了

平均负载和cpu使用率的区别:平均负载不仅包括了正在使用cpu的进程,还包括了等待cpu和等待io的进程。如果是cpu密集型进程,使用大量cpu会导致cpu负载增高,cpu使用率增高。如果是io密集型进程,等待io会导致平均负载升高,但cpu使用率不一定高。因此,平均负载高,cpu使用率不高的情况,瓶颈在io。负载高,cpu使用率也高的情况下,瓶颈在cpu

进程数据结构task_struct

进程数据结构包括以下几部分内容:任务id、任务状态、亲缘关系、权限、运行统计、调度相关、信号处理、内存管理、文件与文件系统、内核栈

任务id:主要包括pid(进程id)、tgid(线程组id)、group_leader(组leader)。如果只有主线程,pid是自己,tgid是自己,group_leader指向的也是自己。如果是进程创建的线程,pid是自己,tgid是主线程的pid,group_leader指向的是进程的主线程

任务状态:参考上边进程状态介绍

亲缘关系:主要包括parent、children、sibling。parent指向当前进程的父进程、children指向当前进程子进程链表的头部、sibling用于把当前进程加入到兄弟链表

权限:权限主要定义我能操作谁,谁能操作我。比如:-rwxr-xr-x (755) -- 属主有读、写、执行权限;而属组用户和其他用户只有读、执行权限

运行统计:主要包括utime(用户态消耗的cpu时间)、stime(内核态消耗的cpu时间)、nvcsw(自愿上下文切换计数)、nivcsw(非自愿上下文切换计数)、start_time(进程启动时间,不包含睡眠)、real_start_time(进程启动时间,包含睡眠)

进程调度:记录了进程优先级、调度器类、调度实体、调度策略等信息

信号处理:定义了哪些信号被阻塞暂不处理、哪些信号尚待处理、哪些信号正在处理中等等

内存管理:每个进程都有自己独立的虚拟内存空间,就是mm_struct

文件与文件系统:主要是文件系统数据结构fs_struct和打开文件的数据结构files_struct

内核栈:主要是tread_info和void *stack。tread_info是对task_struct的补充,task_struct结构庞大而通用,但是不同的体系结构需要保存不同的东西,跟体系机构相关的都放在了tread_info中。stack就是进程运行在内核空间时的栈

进程函数调用


linux操作系统笔记(进程)_第1张图片

ESP为栈顶指针寄存器,入栈和出栈会自动调整ESP的值。EBP是栈基地址指针寄存器,指向当前栈帧的最底部

举例A调用B,A的栈里面包含函数A的局部变量,调用B的时候传给B的参数,返回A的地址。这些就是A的栈帧。接下来就是B的栈帧了,调用B时,先保存A栈帧的栈底位置(保存当前的EBP,返回时,用来恢复EBP寄存器),更新EBP的值为当前ESP(初始化函数B的帧指针),接下来保存B的局部变量。函数B中访问参数时,通过函数B的帧指针加偏移量才访问。B返回时,从栈中弹出EBP地址,弹出函数A的返回地址,弹出参数,将指令跳转回去,继续执行函数A

函数调用过程栈帧变化详解 - 立超的专栏 - 博客园

进程用户态和内核态切换

用户态切换到内核态前,先将用户态运行过程中的cpu上下文保存到内核的pt_regs结构中的寄存器变量里,调整ESP指向内核栈的栈顶(如何将ESP正确指向内核栈的栈顶?内核的函数处理完返回用户态时,内核栈肯定是空的,所以每次用户态到内核态时,内核栈的栈顶都一样,可以通过stack指针算出),之后内核的函数调用跟用户态的一样

内核态返回用户态态时,根据pt_regs中记录的信息恢复cpu上下文

进程的调度过程

linux中的进程大概可以分为两种,一种是实时进程,一种是普通进程。实时进程的优先级是0~99。普通进程的优先级是100~139。数值越小优先级越高

实时进程的调度策略:fifo(相同优先级的进程,按照先来先到的顺序)、RR(轮流调度算法,相同优先级的进程采用时间片来调度)、DEADLIN(按照任务的deadline进行调度)

普通进程的调度策略:cfs(完全公平调度算法),会记录每个进程的运行时间vruntime,vruntime的计算方法: vruntime += rruntime*NICE_0_LOAD/权重。cfs需要一个数据结构对vruntime排序,找出最小的,在更新的时候,可以快速排序,这里采用的是红黑树。

每个cpu都有自己的struct rq,用于描述在此cpu上的所有进程。这个结构中包括一个实时进程队列rt_rq和一个cfs运行队列cfs_rq,在调度时,调度器会先去实时进程队列中查看是否有实时进程需要调度,没有才会去cfs队列中找是否有进程需要调度

所有的调度过程都是从schedule()函数进入的,schedule函数的执行过程:

1 在当前cpu上,取出任务队列rq

2 prev指针指向当前运行的进程

3 选取下一个task,选取的过程按照调度类的优先级依次获取,这里只举例cfs调度类的过程:取出当前运行的进程,如果仍然是可运行状态,调用update_curr更新vruntime,然后从cfs_rq红黑树中取出最左的节点,复制给next

4 如果pre和next不同,就需要进行上下文的切换

进程上下文切换过程

上下文切换函数context_switch执行过程:

1 内存空间切换

2 切换栈顶指针

3 将curr_task设置为新进程的task

4 将cpu寄存器保存到老进程的thread_struct结构

5 将新进程的thread_struct里的寄存器的值,写入 cpu的TR执向的tss_struct

现在梳理一下进程相关的信息都在哪个时刻切换了:

1 用户栈:在切换内存空间的时候已经切换

2 内核栈:在将curr_task设置为新的task时,新task中的void *stack就是新的内核栈

3  内核栈的栈顶指针:在切换栈顶指针时已设置为新的

4 用户栈的栈顶指针:保存在了新进程内核的pt_regs中,新进程内核态返回用户态时从pt_regs中恢复出用户态栈顶指针

5 指令指针寄存器:所有进程调度都是从schedule函数进入的,因此,切换到新进程后,新进程内核栈中保存的返回地址就是schedule处的地址

进程内的线程切换的过程

一,用户空间到内核空间的切换,具体过程如下:

1 将用户态运行过程中的cpu上下文保存到内核的pt_regs结构中的寄存器变量

2 调整栈顶指针寄存器,指向内核栈的栈顶

二,在内核空间,切换线程,具体过程如下:

1 切换栈顶指针寄存器,指向新的线程内核栈

2 将curr_task指向新线程的task_struct(每个线程,都有一个task_struct)

3 将cpu寄存器保存到老线程的thread_struct中

4 将新线程的thread_struct里的寄存器的值,写入cpu的TR执向的tss_struct,用来恢复新线程的cpu寄存器

三,新的线程从内核态切会用户态

1 从新线程的内核pt_regs结构中恢复用户态上下文

2 调整栈顶指针寄存器,执向用户态栈

总结:从用户态切换到内核态、内核态切换线程,从内核态再切换到用户态

golang 协程的切换过程

1 将老协程上下文写入老协程的sched域

2 从新协程的sched域恢复新协程的上下文

3 调整栈顶指针寄存器,指向新协程的栈

总结:在用户态切换,不涉及用户态和内核态的切换

进程调度的时机

主动调度:写块设备、从网络设备等待读取时,如果设备还未准备好,会主动执行schedule()让出cpu

抢占式调度:用户态的抢占时机有系统调用返回时,从中断中返回时。内核态的抢占时机有preempt_enable(内核中的某些处理流程需要关中断,此函数是中断再次打开的函数)、内核态中断返回时。

进程间通信方式

1 匿名管道:在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利,一般使用fork函数实现父子进程的通信。

2 命名管道:在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利,没有亲缘关系的进程也可以进程间通信。

3 消息队列:在内核中创建一队列,队列中每个元素是一个数据报,不同的进程可以通过句柄去访问这个队列

4 共享内存: 将同一块物理内存一块映射到不同的进程的虚拟地址空间中,实现不同进程间对同一资源的共享。共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式

5 信号量:信号量其实是一个计数器,实现进程间的互斥与同步,通常和共享内存一起使用

6 信号:信号是Unix系统中使用的最古老的进程间通信的方法之一,信号可以在任何时候发送给某一进程,进程需要为这个信号设置信号处理函数

7 unix domain socket:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC 机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。tcp/ip socket 需要实现跨网络主机通讯的全部环节,包括建立socket连接,tcp流控,封装/解封,路由等。发送数据接收数据还有系统调用,产生上下文切换。

fork()做了哪些复制

1 task_struct

2 cred 父进程的权限

3 files_struct 复制父进程打开的文件信息、fs_struct父进程的目录信息

4 sighand_struct 信号处理函数从父进程复制到子进程

5 mm_struct 复制父进程的内存空间

写时复制

fork()会产生一个和父进程完全相同的子进程(除了pid,子进程的PID返回给父进程,0返回给子进程)。如果按传统的做法,会直接将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的。但是,往往子进程都会执行exec()来做自己想要实现的功能,原有的复制的数据会被清空

写时复制原理:fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。

超线程

对于单一处理器核心来说,虽然也可以每秒钟处理成千上万条指令,但是在某一时刻,只能够对一条指令(单个线程)进行处理,超线程技术能够把一个物理处理器在软件层变成两个逻辑处理器,可以使处理器在某一时刻,同步并行处理更多指令和数据(多个线程),超线程是一种可以将CPU内部暂时闲置处理资源充分“调动”起来的技术

你可能感兴趣的:(linux操作系统笔记(进程))