思维导图整理Linux进程描述符

简介:

[导读] 内核是怎么工作的,首先要理解进程管理,进程调度,本文开始阅读进程管理部分,首先从进程的抽象描述开始。抽象是软件工程的灵魂,而对于Linux操作系统而言,更是将抽象思想体现的淋漓尽致。本文从抽象建模的角度来对Linux进程描述符进行个人解读,同时也参考了内核文档,一些网络信息。

注:代码基于linux-5.4.31,是一个最新的长期支持稳定版本。

整理匆忙,限于水平,文章中错误一定很多,真诚恳请有这方面擅长的朋友帮忙指出,不甚感激!

进程的基本概念

进程 or 线程 or 任务?

进程:进程是一个正在运行的程序实例,由可执行的目标代码组成,通常从某些硬媒介(如磁盘,闪存等)读取并加载到内存中。 但是,从内核的角度来看,涉及很多相关的工作内容。 操作系统存储和管理有关任何当前正在运行的程序的其他信息:地址空间,内存映射,用于读/写操作的打开文件,进程状态,线程等。

进程是正在执行的计算机程序的实例。它包含程序代码及其当前活动。取决于操作系统(OS),进程可能由同时执行指令的多个执行线程组成。基于进程的多任务处理使您可以在使用文本编辑器的同时运行Java编译器。在单个CPU中采用多个进程时,使用了各种内存上下文之间的上下文切换。每个过程都有其自己的变量的完整集合。

但是,在Linux中,如果不讨论线程(有时称为轻量级进程),进程的抽象是不完整的。 根据定义,线程是流程中的执行上下文或执行流; 因此,每个进程至少包含一个线程。 包含多个执行线程的进程被称为多线程进程。 一个进程中有多个线程可以进行当前编程,并且在多处理器系统上可以实现真正的并行性。

线程:则是某一进程中一路单独运行的程序,也就是说,线程存在于进程之中。一个进程由一个或多个线程构成,各线程共享相同的代码和全局数据,但各有其自己的堆栈。由于堆栈是每个线程一个,所以局部变量对每一线程来说是私有的。由于所有线程共享同样的代码和全局数据,它们比进程更紧密,比单独的进程间更趋向于相互作用,线程间的相互作用更容易些,因为它们本身就有某些供通信用的共享内存:进程的全局数据。

线程是CPU利用率的基本单位,由程序计数器,堆栈和一组寄存器组成。执行线程是由计算机程序的分支分解为两个或多个同时运行的任务而产生的。线程和进程的实现因一个操作系统而异,但在大多数情况下,线程包含在进程内部。多个线程可以存在于同一进程中并共享资源(例如内存),而不同进程则不共享这些资源。同一进程中的线程示例是自动拼写检查和写入时自动保存文件。线程基本上是在相同内存上下文中运行的进程。线程在执行时可能共享相同的数据。线程图,即单线程与多线程

任务:是最抽象的,是一个一般性的术语,指由软件完成的一个活动。一个任务既可以是一个进程,也可以是一个线程。简而言之,它指的是一系列共同达到某一目的的操作。与线程非常相似,不同之处在于它们通常不直接与OS交互。 像线程池一样,任务不会创建自己的OS线程。 一个任务内部可能有一个线程,也可能没有。例如,读取数据并将数据放入内存中。这个任务可以作为一个进程来实现,也可以作为一个线程(或作为一个中断任务)来实现。在RTOS中,一般会将调度的基本单元称为任务,比如freeRTOS,ucos,embOS等,在RTOS中没有进程的概念

进程 线程
进程是重量级的操作 线程是轻量级操作
每个进程都有自己的内存空间 线程共享它们所属的进程的内存空间
进程间的通信速度很慢,因为进程具有不同的内存地址 线程间通信可能比进程间通信快,因为同一进程的线程与其所属的进程共享内存
进程之间的上下文切换开销大 在同一进程的线程之间进行上下文切换的开销较低
进程不与其他进程共享内存 线程与同一进程的其他线程共享内存

进程间通讯机制

  • 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
  • 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数 sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal 函数);
  • 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
  • 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
  • 套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

线程间的同步机制:为啥线程间没有讨论通讯机制?因为同一进程内的线程共享进程的资源。那么资源共享,则需要处理资源共享时的同步问题。

  • 互斥锁(mutex):通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。这部分代码常称为临界区。哪些可能是临界区呢?简言之,多个线程可能竞争访问的资源。以下一些函数是互斥锁的API函数。
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex *mutex);
int pthread_mutex_destroy(pthread_mutex *mutex);
int pthread_mutex_unlock(pthread_mutex *
AI 代码解读
  • 全局条件变量(condition variable): 创建一些全局条件变量进行互斥访问控制。以下是其操作的基本接口函数:
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);  
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
AI 代码解读
  • 信号量(semaphore):如同进程一样,线程也可以通过信号量来实现通信,其基本操作接口API:
int sem_init (sem_t *sem , int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_destroy(sem_t *sem);
AI 代码解读

进程在内核中如何描述?

Linux中进程描述在./include/linux/sched.h中定义:

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
    /* 必须是首个元素     */
    struct thread_info        thread_info;
#endif
    /* -1 unrunnable, 0 runnable, >0 stopped: */
    volatile long            state;

    /* 前面是与调度密切相关的信息添加在这之前 */
    randomized_struct_fields_start

    void                *stack;
    refcount_t            usage;
    /* Per task flags (PF_*), defined further below: */
    unsigned int            flags;
    unsigned int            ptrace;
    .........
};
AI 代码解读

该结构非常大,集总抽象了进程的所有信息,包括进程ID,状态,父进程,子进程,同级,处理器寄存器,打开的文件,地址空间等。系统使用循环双向链接列表进行存储 所有过程描述符。

像这样的大型结构肯定会占用大量内存空间。 为每个进程提供较小的内核堆栈大小(可以使用编译时选项进行配置,但默认情况下限制为一页,即对于32位体系结构严格为4KB(一个页),对于64位体系结构严格为8KB(两个页) –内核堆栈不具备增长或收缩),以这种浪费的方式使用资源并不是很方便。 因此,决定在堆栈中放置一个更简单的结构,并带有指向实际task_struct的指针,从而引申出thread_info。

抽象建模思想看进程描述符

进程首先是操作系统对底层进行抽象而提供面向应用接口的一种抽象,而进程描述符则将底层资源、进程本身的调度从以下几个大的方面进行高级别的抽象封装:

  • 应用程序信息抽象
  • 操作系统资源抽象
  • 调度接口抽象
  • 内存管理抽象
  • 账户信息抽象
  • ......

通过预读进程描述符,个人将进程描述相关信息大致分为以下几个大类抽象:

_

涉及thread_info、优先级、栈、上下文切换、调度相关链表等关键数据。

  • CPU相关抽象
    CPU

涉及SMP多核处理抽象、CPUSET子系统相关、当前CPU等相关数据抽象。

  • 保护机制抽象
    _
  • 内存管理抽象
    _
  • 缓存相关抽象
    _
  • 信号通信抽象
    _
  • 接口相关抽象
    _
  • 调试跟踪抽象
    trace
  • 安全机制抽象
    _
  • 资源管理抽象

  • 杂项信息抽象
    _

最后附上些整理搜集到数据域的一些较详细的介绍。

thread_info

该字段保存特定于处理器的状态信息,并且是进程描述符的关键元素。具体定义在./arch/xxx/include/asm/thread_info.h中。

  • entry.S需要立即访问此结构的低级任务数据应完全适合一个缓存行,此结构共享主管堆栈页面
  • 如果更改此结构的内容,则还必须更改汇编代码。
  • 因为thread_info包含了当前进程的指针,存储在栈底或栈顶,取决于不同体系架构栈的增长方向,利用thread_info可以快速的访问当前进程的信息,而不必依次遍历。

ARM32的定义:

struct thread_info {
    unsigned long    flags;        /* low level flags */
    int                preempt_count;    /* 0 => preemptable, <0 => bug */
    mm_segment_t    addr_limit;    /* address limit */
    struct task_struct    *task;        /* main task structure */
    __u32            cpu;        /* cpu */
    __u32            cpu_domain;    /* cpu domain */
#ifdef CONFIG_STACKPROTECTOR_PER_TASK
    unsigned long        stack_canary;
#endif
    struct cpu_context_save    cpu_context;    /* cpu context */
    __u32            syscall;    /* syscall number */
    __u8            used_cp[16];    /* thread used copro */
    unsigned long        tp_value[2];    /* TLS registers */
#ifdef CONFIG_CRUNCH
    struct crunch_state    crunchstate;
#endif
    union fp_state        fpstate __attribute__((aligned(8)));
    union vfp_state        vfpstate;
#ifdef CONFIG_ARM_THUMBEE
    unsigned long        thumbee_state;    /* ThumbEE Handler Base register */
#endif
};
AI 代码解读

从书上和网上看到都是前面这样描述的,但是对于ARM64的却没有当前进程指针,这是为何呢?没弄明白,有谁知道告诉下我呗。

struct thread_info {
    unsigned long    flags;        /* low level flags */
    mm_segment_t    addr_limit;    /* address limit */
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
    u64            ttbr0;           /* saved TTBR0_EL1 */
#endif
    union {
        u64     preempt_count;       /* 0 => preemptible, <0 => bug */
        struct {
#ifdef CONFIG_CPU_BIG_ENDIAN
            u32    need_resched;
            u32    count;
#else
            u32    count;
            u32    need_resched;
#endif
        } preempt;
    };
};
AI 代码解读

利用如下的几种方式,可以获取thread_info信息:

  • static inline struct thread_info *current_thread_info(void)
  • define GET_THREAD_INFO(reg)

  • ...

SLUB 分配器

thread_info实现了进程存储对描述符的引用以及如何访问它们。 但是,如果task_struct不是在内核堆栈内部,则task_struct到底位于内存中的什么位置? 为此,Linux提供了一种特殊的内存管理机制,称为SLUB层。SLUB动态生成task_struct,并把thread_info存在栈底或栈顶。

volatile long state

进程状态,可取的进程状态:

  • TASK_RUNNING: 可执行态
  • TASK_INTERRUPTIBLE:可中断
  • TASK_UNINTERRUPTIBLE:不可中断
  • __TASK_STOPPED:停止态
  • __TASK_TRACED:被其他进程跟踪的进程

为何用volatile修饰。 由于内核经常需要从不同位置更改进程的状态,例如,如果在单个CPU硬件上同时将两个进程设置为RUNNABLE。熟悉单片机编程的朋友一定知道,当在中断函数中需要修改以及在中断外部也会被修改的变量,就会使用到volatile修饰变量。

randomized_struct_fields_start

这是gcc的一个插件(插件来自于Grsecurity),其作用就是这之后的变量不会按照声明顺序存储在内存中,而会按照一定的随机顺序存放,这样做是基于安全考虑,比如应用程序的进程描述符被劫持,如果按顺序存放,则容易篡改其内容。

文章出自微信公众号:嵌入式客栈,更多更新内容请关注,版权所有,严禁商用
ghao

目录
相关文章
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
134 67
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
65 4
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
130 34
|
2月前
|
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
76 7
|
2月前
|
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
106 5
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
44 3
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
85 16
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
279 20