|
[导读] 内核是怎么工作的,首先要理解进程管理,进程调度,本文开始阅读进程管理部分,首先从进程的抽象描述开始。抽象是软件工程的灵魂,而对于Linux操作系统而言,更是将抽象思想体现的淋漓尽致。本文从抽象建模的角度来对Linux进程描述符进行个人解读,同时也参考了内核文档,一些网络信息。 ( J( N! j( V* _6 Y$ X 注:代码基于linux-5.4.31,是一个最新的长期支持稳定版本。 . d7 X2 g. Q; P. V1 R8 h: l. N) |/ Q 整理匆忙,限于水平,文章中错误一定很多,真诚恳请有这方面擅长的朋友帮忙指出,不甚感激! Y# w& V$ X/ Y) u9 n% B+ j" R 进程的基本概念进程 or 线程 or 任务? 0 X; j3 q& Y9 J! k, y: p F 进程:进程是一个正在运行的程序实例,由可执行的目标代码组成,通常从某些硬媒介(如磁盘,闪存等)读取并加载到内存中。但是,从内核的角度来看,涉及很多相关的工作内容。操作系统存储和管理有关任何当前正在运行的程序的其他信息:地址空间,内存映射,用于读/写操作的打开文件,进程状态,线程等。 ) h }4 j, d/ X P5 L6 [, C 进程是正在执行的计算机程序的实例。它包含程序代码及其当前活动。取决于操作系统(OS),进程可能由同时执行指令的多个执行线程组成。基于进程的多任务处理使您可以在使用文本编辑器的同时运行Java编译器。在单个CPU中采用多个进程时,使用了各种内存上下文之间的上下文切换。每个过程都有其自己的变量的完整集合。 , F5 {. s# M4 R3 L: m9 J 但是,在Linux中,如果不讨论线程(有时称为轻量级进程),进程的抽象是不完整的。根据定义,线程是流程中的执行上下文或执行流;因此,每个进程至少包含一个线程。包含多个执行线程的进程被称为多线程进程。一个进程中有多个线程可以进行当前编程,并且在多处理器系统上可以实现真正的并行性。 线程:则是某一进程中一路单独运行的程序,也就是说,线程存在于进程之中。一个进程由一个或多个线程构成,各线程共享相同的代码和全局数据,但各有其自己的堆栈。由于堆栈是每个线程一个,所以局部变量对每一线程来说是私有的。由于所有线程共享同样的代码和全局数据,它们比进程更紧密,比单独的进程间更趋向于相互作用,线程间的相互作用更容易些,因为它们本身就有某些供通信用的共享内存:进程的全局数据。 线程是CPU利用率的基本单位,由程序计数器,堆栈和一组寄存器组成。执行线程是由计算机程序的分支分解为两个或多个同时运行的任务而产生的。线程和进程的实现因一个操作系统而异,但在大多数情况下,线程包含在进程内部。多个线程可以存在于同一进程中并共享资源(例如内存),而不同进程则不共享这些资源。同一进程中的线程示例是自动拼写检查和写入时自动保存文件。线程基本上是在相同内存上下文中运行的进程。线程在执行时可能共享相同的数据。线程图,即单线程与多线程 任务:是最抽象的,是一个一般性的术语,指由软件完成的一个活动。一个任务既可以是一个进程,也可以是一个线程。简而言之,它指的是一系列共同达到某一目的的操作。与线程非常相似,不同之处在于它们通常不直接与OS交互。像线程池一样,任务不会创建自己的OS线程。一个任务内部可能有一个线程,也可能没有。例如,读取数据并将数据放入内存中。这个任务可以作为一个进程来实现,也可以作为一个线程(或作为一个中断任务)来实现。在RTOS中,一般会将调度的基本单元称为任务,比如freeRTOS,ucos,embOS等,在RTOS中没有进程的概念。
进程间通讯机制: $ g! ?; T% r5 |4 o% i# ]
线程间的同步机制:为啥线程间没有讨论通讯机制?因为同一进程内的线程共享进程的资源。那么资源共享,则需要处理资源共享时的同步问题。
int pthread_mutex_lock(pthread_mutex *mutex);( } p. i5 T3 F/ Z int pthread_mutex_destroy(pthread_mutex *mutex);' j8 U3 R& ~7 w1 f0 Y9 w int pthread_mutex_unlock(pthread_mutex *% S0 ?. t, h. x# U1 Z
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); P* R& c+ j/ C2 M4 N9 d0 p int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);) e4 v, ^6 [: z- ~( h int pthread_cond_destroy(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);
int sem_wait(sem_t *sem);4 P7 K& A" k0 \9 ]% d0 u( e int sem_post(sem_t *sem); int sem_destroy(sem_t *sem);6 a3 n2 j, ^) s9 u 1 \& n; J: t9 W; [6 K: k- \ 进程在内核中如何描述?& S! P1 ^7 h% r% M ' {/ a0 g$ K$ z( [; ]8 W 0 x" `" z: f# t Linux中进程描述在./include/linux/sched.h中定义: struct task_struct {& J. Y# ^5 A; x" G#ifdef CONFIG_THREAD_INFO_IN_TASK /* 必须是首个元素 */ {8 {" x5 }# L& J struct thread_info thread_info;8 B( @1 I6 y6 \ #endif! @0 w, g" Z1 b! q4 | /* -1 unrunnable, 0 runnable, >0 stopped: */$ Q& C. J, e+ x) M" n volatile long state; /* 前面是与调度密切相关的信息添加在这之前 */ randomized_struct_fields_start& C0 a+ b& K. g$ q % G, H+ e1 Q1 w- ?: [1 j void *stack; refcount_t usage;5 _% E* L2 ]5 w% J E7 r /* Per task flags (PF_*), defined further below: */( u0 }* a) \5 x ^5 p unsigned int flags;# B3 M. I6 _! Z) o( b; @# I8 ~ unsigned int ptrace;7 _/ F4 Y1 W w- j .........' }: D7 g* l0 G- }+ x$ v" \6 H }; ! m' S& H D* x% { g5 @2 ?5 U 该结构非常大,集总抽象了进程的所有信息,包括进程ID,状态,父进程,子进程,同级,处理器寄存器,打开的文件,地址空间等。系统使用循环双向链接列表进行存储 所有过程描述符。 ( ^1 R, m3 u. b: t' Y, f& M 像这样的大型结构肯定会占用大量内存空间。为每个进程提供较小的内核堆栈大小(可以使用编译时选项进行配置,但默认情况下限制为一页,即对于32位体系结构严格为4KB(一个页),对于64位体系结构严格为8KB(两个页) –内核堆栈不具备增长或收缩),以这种浪费的方式使用资源并不是很方便。因此,决定在堆栈中放置一个更简单的结构,并带有指向实际task_struct的指针,从而引申出thread_info。 i/ Q0 S0 S2 k9 u$ A) X/ S9 | 进程首先是操作系统对底层进行抽象而提供面向应用接口的一种抽象,而进程描述符则将底层资源、进程本身的调度从以下几个大的方面进行高级别的抽象封装: % B( q. ?9 J- ?0 w2 Y, Q" o
3 O6 q: k% Q, U2 S- P% o 通过预读进程描述符,个人将进程描述相关信息大致分为以下几个大类抽象:
涉及thread_info、优先级、栈、上下文切换、调度相关链表等关键数据。
涉及SMP多核处理抽象、CPUSET子系统相关、当前CPU等相关数据抽象。
http://blog.csdn.net/wang_xya/article/details/35234429
最后附上些整理搜集到数据域的一些较详细的介绍。 thread_info该字段保存特定于处理器的状态信息,并且是进程描述符的关键元素。具体定义在./arch/xxx/include/asm/thread_info.h中。
ARM32的定义: struct thread_info {unsigned long flags; /* low level flags */- N. |) B" V4 B. ?" h int preempt_count; /* 0 => preemptable, <0 => bug */ mm_segment_t addr_limit; /* address limit */$ t- Z1 y% r7 P! l9 `% c$ L2 J struct task_struct *task; /* main task structure */1 p7 e' y: ^- I" V9 y: [: Q5 @ __u32 cpu; /* cpu */ __u32 cpu_domain; /* cpu domain */; u0 y5 N. U* |# b1 Y# _ #ifdef CONFIG_STACKPROTECTOR_PER_TASK unsigned long stack_canary;/ G, m. v' c3 @: @, z( L #endif/ j- e/ w d7 t$ C0 \9 W9 ~; ~ struct cpu_context_save cpu_context; /* cpu context */* u0 @+ S: M; V6 d0 [0 b __u32 syscall; /* syscall number */$ D. U# L0 _6 c) C$ C9 P; l' K0 V __u8 used_cp[16]; /* thread used copro */ unsigned long tp_value[2]; /* TLS registers */" `! v, [; ^' i% i& _ #ifdef CONFIG_CRUNCH8 D6 M5 u1 r% c+ F' a" p struct crunch_state crunchstate;# L0 f K( X; L* o #endif union fp_state fpstate __attribute__((aligned(8))); union vfp_state vfpstate; #ifdef CONFIG_ARM_THUMBEE unsigned long thumbee_state; /* ThumbEE Handler Base register */* [& Q( G% O/ a" H- F7 ] #endif };) D& Q7 K/ z0 K' c3 h3 {* @ 从书上和网上看到都是前面这样描述的,但是对于ARM64的却没有当前进程指针,这是为何呢?没弄明白,有谁知道告诉下我呗。 struct thread_info {unsigned long flags; /* low level flags */ mm_segment_t addr_limit; /* address limit */: ]; P! |2 \ b3 U" B #ifdef CONFIG_ARM64_SW_TTBR0_PAN u64 ttbr0; /* saved TTBR0_EL1 */- z) p: [8 E% o #endif union { u64 preempt_count; /* 0 => preemptible, <0 => bug */ struct { #ifdef CONFIG_CPU_BIG_ENDIAN u32 need_resched;$ |: p& D$ f; X$ ~& t u32 count; #else) ^1 t$ L% S6 c0 t( |; S( p" x u32 count; u32 need_resched; #endif } preempt;* k3 J6 ~2 |5 o @ };( U3 |& }% g0 p. l3 \! Z }; 利用如下的几种方式,可以获取thread_info信息:
thread_info实现了进程存储对描述符的引用以及如何访问它们。但是,如果task_struct不是在内核堆栈内部,则task_struct到底位于内存中的什么位置?为此,Linux提供了一种特殊的内存管理机制,称为SLUB层。SLUB动态生成task_struct,并把thread_info存在栈底或栈顶。 volatile long state进程状态,可取的进程状态:
为何用volatile修饰。由于内核经常需要从不同位置更改进程的状态,例如,如果在单个CPU硬件上同时将两个进程设置为RUNNABLE。熟悉单片机编程的朋友一定知道,当在中断函数中需要修改以及在中断外部也会被修改的变量,就会使用到volatile修饰变量。 randomized_struct_fields_start这是gcc的一个插件(插件来自于Grsecurity),其作用就是这之后的变量不会按照声明顺序存储在内存中,而会按照一定的随机顺序存放,这样做是基于安全考虑,比如应用程序的进程描述符被劫持,如果按顺序存放,则容易篡改其内容。 |
微信公众号
手机版