[X]关闭

进程原理

文档创建者:qnpragyl
浏览次数:1359
最后更新:2024-05-09
本帖最后由 qnpragyl 于 2024-5-9 17:01 编辑

一、 进程概念
进程是计算机中运行的程序的实例。每个进程都有自己的内存空间、代码和数据,它们相互隔离,互不干扰。进程可以通过操作系统的调度机制来执行,并且可以同时运行多个进程。每个进程都有一个唯一的进程标识符(PID),用于操作系统识别和管理进程。进程可以与其他进程进行通信和协作,例如通过共享内存、管道或套接字等机制。进程的创建、调度、销毁等操作由操作系统负责管理。
(Linux内核中把进程叫做任务(task),进程的虚拟地址空间可分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。)
1、 进程有俩种特殊的形式:没有用户虚拟地址空间的进程叫做内核线程,共享用户虚拟地址空间的进程叫用户线程。共享同一个用户虚拟地址空间的所有用户线程叫线程组。
俩种进程的对应名称
  C语言标准库进程  
Linux内核进程
  包括多个线程的进程  
线程组
  只有一个线程的进程  
任务或者进程
  线程  
共享用户虚拟地址空间的进程
Linux通过:ps命令用于输出当前系统的进程状态(查看进程运行状态),显示瞬间进程的状态,并不是动态连续;
image.jpg

各列的详细信息

  • USER:启动该进程的用户名。
  • PID:进程的唯一标识符(PID)。
  • %CPU:该进程占用 CPU 的使用百分比。
  • %MEM:该进程占用系统内存的百分比。
  • VSZ:该进程使用的虚拟内存大小(以 KB 为单位)。
  • RSS:该进程使用的实际物理内存大小(以 KB 为单位)。
  • TTY:启动该进程的终端设备(如果有)。
  • STAT:进程的状态。常见的状态包括 S(休眠)、R(运行)、D(不可中断的休眠)、Z(僵尸)等。
  • START:进程启动的时间。
  • TIME:该进程消耗的 CPU 时间。
  • COMMAND:启动该进程的命令及参数。

top指令能够对进程进行实时的监控
image.jpg

命令输出的各部分解释
·        top - 20:00:09 up 2:25, 1 user, load average: 0.20, 0.05, 0.02:这行显示了当前系统时间、系统已运行时间、当前登录用户数量以及系统的平均负载(过去1分钟、5分钟和15分钟的平均负载)。
·        Tasks: 320 total, 1 running, 240 sleeping, 0 stopped, 0 zombie:这行显示了系统中的任务数量,包括运行中的任务、休眠中的任务、已停止的任务和僵尸任务的数量。
·        %Cpu(s): 0.2 us, 0.1 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si,0.0 st:这行显示了 CPU 使用情况的统计信息,包括用户空间时间、系统内核时间、用户空间低优先级时间、空闲时间、等待I/O 时间、硬中断处理时间、软中断处理时间和虚拟化环境中的虚拟CPU 闲置时间。
·        KiB MemKiB Swap:这两行显示了系统内存和交换空间的使用情况,包括总量、空闲量、已使用量和可用量。


在top基本视图中,按键盘数字“1”,可以监控每个逻辑CPU的状况:
image.jpg

显示指定的进程信息
top -p 1809

image.jpg


二、 进程生命周期
Linux操作系统属于多任务操作系统,系统中的每个进程能够分时复用CPU时间片,通过有效的进程调度策略实现多任务并行执行。而进程在被CPU调度运行,等待CPU资源分配以及等待外部事件时会属于不同的状态。进程状态如下:创建状态:创建新进程;就绪状态:进程获取可以运作所有资源及准备相关条件;执行状态:进程正在CPU中执行操作;阻塞状态:进程因等待某些资源而被跳出CPU;终止状态:进程消亡。
Linux内核进程状态间关系如图:
image.jpg
1、 Linux内核提供API函数来设置进程的状态:l  TASK_RUNNING  (可运行态或者可就绪状态)l  TASK_INTERRUPTIBLE  (可中断睡眠状态,又叫浅睡眠状态)l  TASK_UNINTERRUPTIBLE (不可中断状态,深度睡眠状态)可以通过ps命令查看被标记为D状态的进程l  __TASK_STOPPEF(终止状态)l  EXIT_ZOMBIE (僵尸状态) :当一个进程终止时,其父进程通常需要调用 wait() 系统调用来获取子进程的终止状态。如果父进程没有调用 wait(),而子进程已经终止,那么子进程的进程描述符(ProcessDescriptor)仍然会被保留在系统中,但进程已经不再存在

三、 task_struct数据结构分析
在Linux内核中采用task_struct结构体来描述专程控制块。Linux内核涉及进程和程序的所有算法都围绕名为task_struct的数据结构而建立操作的。 image.jpg
核心成员组结构体注释:

  • /* -1 unrunnable, 0 runnable, >0 stopped: */
  • volatile long   state;  //进程的状态标志
  • /*
  •   * This begins the randomizable portion of task_struct. Only
  •   * scheduling-critical items should be added above here.
  •   */
  • randomized_struct_fields_start
  • void    *stack; // 指向内核栈
  • refcount_t   usage;
  • /* Per task flags (PF_*), defined further below: */
  • unsigned int   flags;
  • unsigned int   ptrace;

  • pid_t    pid; //全局的进程号
  • pid_t    tgid; // 全局的线程组标识符
1.  struct hlist_node  pid_links[PIDTYPE_MAX];//进程号,进程组表示符和会话标识符

  • struct task_struct __rcu *real_parent; //指向真实的父进程
  • /* Recipient of SIGCHLD, wait4() reports: */
  • struct task_struct __rcu *parent; // 指向父进程

  • /*
  •   * Children/sibling form the list of natural children:
  •   */
  • struct list_head  children;
  • struct list_head  sibling;
  • struct task_struct  *group_leader; //指向线程组的组长

  • /* Objective and real subjective task credentials (COW): */
  • const struct cred __rcu  *real_cred; //此成员指向主体和真实客体的证书
  • /* Effective (overridable) subjective task credentials (COW): */
  • const struct cred __rcu  *cred; //指向有效客体证书 (可以被临时改变)

  • /*
  •   * executable name, excluding path.
  •   *
  •   * - normally initialized setup_new_exec()
  •   * - access it with [gs]et_task_comm()
  •   * - lock it with task_lock()
  •   */
  • char    comm[TASK_COMM_LEN]; //进程名称
  • struct nameidata  *nameidata;

  • /*下面四个是进程调度策略和优先级*/
  • int    prio;
  • int    static_prio;
  • int    normal_prio;
  • unsigned int   rt_priority;

  • //这个两指针指向内存描述符。进程:mm和active_mm指向同一个内存描述符。内核线程:mm是空指针
  • //当内核线程运行时,active_mm指向从进程借用内存描述符
  • struct mm_struct  *mm;
  • struct mm_struct  *active_mm;

  • /* Filesystem information: */
  • struct fs_struct  *fs; //此成员文件系统信息,主要是进程的根目录和当前工作目录
  • /* Open file information: */
  • struct files_struct  *files; // 打开文件表
  • /* Namespaces: */
  • struct nsproxy   *nsproxy; // 命名空间
  •     //下面这一块的成员主要是用于 信号处理
  • /* Signal handlers: */
  • struct signal_struct  *signal;
  • struct sighand_struct __rcu  *sighand;
  • sigset_t   blocked;
  • sigset_t   real_blocked;
  • /* Restored if set_restore_sigmask() was used: */
  • sigset_t   saved_sigmask;
  • struct sigpending  pending;


四、 进程优先级/系统调用
1、 进程优先级l  限期进程的优先级比实时进程要高,实时进程的优先级比普通进程要高。l  限期进程的优先级是-1;l  实时进程的优先级1-99,优先级数值越大,表示优先级越高;l  普通进程的静态优先级为:100-139,优先级数值越小,表示优先级越高,可通过修改nice值改变普通进程的优先级,优先级等于120加上nice值。
image.jpg

如下表:
  优先级  
限期进程
实时进程
普通进程
  prio调度优先级  (数值越小,优先级越高)  
大多数情况下prio等于normal_prio。特殊情况,如果进程x占有实时互斥锁,进程y正在等待锁,进程y的优先级比进程进程x优先级高,那么把进程x的优先级临时提高到进程y的优先级,即进程x的prio的值等于进程y的prio值。
  static_prio静态优先级  
没有意义,总是0
没有意义,总是0
120+nice值,数值越小,表示优先级越高
  normal_prio正常优先级  (数值越小优先级越高)  
-1
99- rt_priority
static_prio
  rt_priority 实时优先级  
没有意义,总是0
实时进程的优先级,范围1-99,数值越大优先级越高
没有意义,总是0

2、 系统调用当运行应用程序的时候,调用fork()/vfork()/clone()/open()/write()函数就是系统调用。系统调用就是应用程序如何进入内核空间执行任务,程序使用系统调用执行一系列操作:比如创建进程、文件IO等等。具体如下图: image.jpg
内核源码: image.jpg
在 Linux 内核中定义了 fork() 系统调用的实现。在启用 MMU 支持的情况下,它会调用 _do_fork() 函数来创建一个新的子进程。

  • SYSCALL_DEFINE0(fork)
  • {
  • #ifdef CONFIG_MMU
  • struct kernel_clone_args args = {
  •   .exit_signal = SIGCHLD,
  • };
  • return _do_fork(&args);// 调用 _do_fork() 函数
  • #else
  • /* can not support in nommu mode */
  • return -EINVAL;
  • #endif
  • }



代码定义了 vfork() 系统调用的实现,会在子进程运行期间暂停父进程的执行。

  • #ifdef __ARCH_WANT_SYS_VFORK
  • SYSCALL_DEFINE0(vfork)
  • {
  • struct kernel_clone_args args = {
  •   .flags  = CLONE_VFORK | CLONE_VM,
  •   .exit_signal = SIGCHLD,
  • };
  • return _do_fork(&args);// 调用 _do_fork() 函数
  • }
  • #endif

实现了 clone() 系统调用的逻辑,允许创建新的进程或线程,并传递各种参数以定制进程的行为。

  • #else
  • SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
  •    int __user *, parent_tidptr,
  •    int __user *, child_tidptr,
  •    unsigned long, tls)
  • #endif
  • {
  • struct kernel_clone_args args = {
  •   .flags  = (lower_32_bits(clone_flags) & ~CSIGNAL),
  •   .pidfd  = parent_tidptr,
  •   .child_tid = child_tidptr,
  •   .parent_tid = parent_tidptr,
  •   .exit_signal = (lower_32_bits(clone_flags) & CSIGNAL),
  •   .stack  = newsp,
  •   .tls  = tls,
  • };
  • if (!legacy_clone_args_valid(&args))
  •   return -EINVAL;
  • return _do_fork(&args);// 调用 _do_fork() 函数
  • }
  • #endif


这段代码是 _do_fork() 函数的实现,用于执行实际的进程复制(fork)操作。

  • long _do_fork(struct kernel_clone_args *args)
  • {
  • u64 clone_flags = args->flags;
  • struct completion vfork;
  • struct pid *pid;
  • struct task_struct *p;
  • int trace = 0;
  • long nr;
  • /*
  •   * Determine whether and which event to report to ptracer.  When
  •   * called from kernel_thread or CLONE_UNTRACED is explicitly
  •   * requested, no event is reported; otherwise, report if the event
  •   * for the type of forking is enabled.
  •   */
  • if (!(clone_flags & CLONE_UNTRACED)) {
  •   if (clone_flags & CLONE_VFORK)
  •    trace = PTRACE_EVENT_VFORK;
  •   else if (args->exit_signal != SIGCHLD)
  •    trace = PTRACE_EVENT_CLONE;
  •   else
  •    trace = PTRACE_EVENT_FORK;
  •   if (likely(!ptrace_event_enabled(current, trace)))
  •    trace = 0;
  • }
  • p = copy_process(NULL, trace, NUMA_NO_NODE, args);// 调用 copy_process() 函数创建新的进程。传递给 copy_process() 函数的参数包括父进程的地址空间、需要报告给 ptracer 的事件类型、NUMA     节点编号和进程复制的参数。
  • add_latent_entropy();


代码定义了一个名为 do_fork() 的函数,用于在不支持复制线程 TLS(Thread Local Storage)的架构上创建新的进程。

  • #ifndef CONFIG_HAVE_COPY_THREAD_TLS
  • /* For compatibility with architectures that call do_fork directly rather than
  • * using the syscall entry points below. */
  • long do_fork(unsigned long clone_flags,//创建进程的标志位集合
  •        unsigned long stack_start,//用户态栈的起始地址
  •        unsigned long stack_size,//用户态栈的大小,一般情况下设置为0
  •        int __user *parent_tidptr,//指向用户空间中地址的指针,分配指向父子进程的PID
  •        int __user *child_tidptr)
  • {
  • struct kernel_clone_args args = {
  •   .flags  = (lower_32_bits(clone_flags) & ~CSIGNAL),
  •   .pidfd  = parent_tidptr,
  •   .child_tid = child_tidptr,
  •   .parent_tid = parent_tidptr,
  •   .exit_signal = (lower_32_bits(clone_flags) & CSIGNAL),
  •   .stack  = stack_start,
  •   .stack_size = stack_size,
  • };
  • if (!legacy_clone_args_valid(&args))
  •   return -EINVAL;
  • return _do_fork(&args);     // 调用 _do_fork() 函数来执行实际的进程复制操作,并返回其返回值。
  • }
  • #endif


3、 内核线程
内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行(实际上,也并行于内核自身的执行)。内核线程经常称之为(内核)守护进程。它们用于执行下列任务。(task_struct数据结构里面有一个成员指针mm设置为NULL,它只能运行在内核空间。)
l  周期性地将修改的内存页与页来源块设备同步(如mmap的文件映射);
l  如果内存页很少使用,则写入交换区;
l  管理延时动作;

l  实现文件系统的事务日志。
创建一个内核线程

  • /*
  • * Create a kernel thread.
  • */
  • pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
  • {
  • struct kernel_clone_args args = {
  •   .flags  = ((lower_32_bits(flags) | CLONE_VM |
  •         CLONE_UNTRACED) & ~CSIGNAL),
  •   .exit_signal = (lower_32_bits(flags) & CSIGNAL),
  •   .stack  = (unsigned long)fn,
  •   .stack_size = (unsigned long)arg,
  • };
  • return _do_fork(&args);// 调用 _do_fork(&args) 函数来执行实际的线程创建操作,并返回其返回值,即新线程的进程 ID。
  • }


4、 退出进程
进程主动终止:从main()函数返回,链接程序会自动添加到exit()系统函数;

进程被动终止:进程收到一个自己不能处理的信号;进程收到SIGKILL等终止信息。
image.jpg
这段代码定义了一个名为 exit 的系统调用,接受一个整数参数作为错误码,然后使用 do_exit() 函数终止当前进程并返回指定的错误码。

  • SYSCALL_DEFINE1(exit, int, error_code)// 将系统调用映射到内核中的实际函数
  • {
  • do_exit((error_code&0xff)<<8);// do_exit() 函数用于终止当前进程,其中 error_code 参数被左移 8 位,并通过按位与操作确保只取低     8 位的值。
  • }







您需要登录后才可以回帖 登录 | 立即注册

本版积分规则