• 进程数据结构


    在 Linux 里面,无论是进程,还是线程,到了内核里面,统一称为任务(Task)

    Linux内核通过一个被称为进程描述符task_struct 结构体来管理,这个结构体包含了一个task所需的所有信息。

    task_struct  定义在 include/linux/sched.h 文件中。

    struct task_struct

    /****** 内核栈相关 ******/
    struct thread_info    thread_info;
    void  *stack;
    
    /****** 任务 ID 相关 ******/
    pid_t pid; // process id 进程或线程的ID;如果task是进程pid=tgid, 最大值:/proc/sys/kernel/pid_max
    pid_t tgid; // thread group ID; 如果task是线程,tgid是主线程的 pid
    struct task_struct *group_leader; //如果task是线程,指向主线程
    
    /****** 信号处理相关 ******/
    /* Signal handlers: */
    // signal成员有struct sigpending shared_pending; 信号尚等待处理(线程组共享)
    struct signal_struct    *signal;
    struct sighand_struct    *sighand; // 信号正在通过信号处理函数进行处理
    sigset_t      blocked; // 信号被阻塞暂不处理
    sigset_t      real_blocked;
    sigset_t      saved_sigmask;
    struct sigpending    pending; // 信号尚等待处理(本任务)
    /* 信号处理函数默认使用用户态的函数栈,也可以开辟新的栈专门用于信号处理(sas_ss_xxx 的作用) */
    unsigned long      sas_ss_sp;
    size_t        sas_ss_size;
    unsigned int      sas_ss_flags;
    
    /****** 任务状态相关 ******/
     volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
     int exit_state;
     unsigned int flags;
    
    /****** 进程调度相关 ******/
    // 是否在运行队列上
    int        on_rq;
    // 优先级
    int        prio;
    int        static_prio;
    int        normal_prio;
    unsigned int      rt_priority;
    // 调度器类
    const struct sched_class  *sched_class;
    // 调度实体
    struct sched_entity    se;
    struct sched_rt_entity    rt;
    struct sched_dl_entity    dl;
    // 调度策略
    unsigned int      policy;
    // 可以使用哪些CPU
    int        nr_cpus_allowed;
    cpumask_t      cpus_allowed;
    struct sched_info    sched_info;
    
    /****** 运行统计信息 ******/
    u64        utime;// 用户态消耗的CPU时间
    u64        stime;// 内核态消耗的CPU时间
    unsigned long      nvcsw;// 自愿(voluntary)上下文切换计数
    unsigned long      nivcsw;// 非自愿(involuntary)上下文切换计数
    u64        start_time;// 进程启动时间,不包含睡眠时间
    u64        real_start_time;// 进程启动时间,包含睡眠时间
    
    /****** 进程亲缘关系 ******/
    struct task_struct __rcu *real_parent; /* real parent process */
    struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
    struct list_head children;      /* list of my children */
    struct list_head sibling;       /* linkage in my parent's children list */
    
    /****** 进程权限 ******/
    /* Objective and real subjective task credentials (COW): */
    const struct cred __rcu         *real_cred;
    /* Effective (overridable) subjective task credentials (COW): */
    const struct cred __rcu         *cred;
    
    /****** 内存管理相关 ******/
    struct mm_struct                *mm;
    struct mm_struct                *active_mm;
    
    /****** 文件系统相关 ******/
    /* Filesystem information: */
    struct fs_struct                *fs;
    /* Open file information: */
    struct files_struct             *files;

     Linux 给每个 task 都分配了内核栈。

    // arch/x86/include/asm/page_32_types.h, PAGE_SIZE = 4K
    #define THREAD_SIZE_ORDER  1
    #define THREAD_SIZE    (PAGE_SIZE << THREAD_SIZE_ORDER) // 8K
    
    // arch/x86/include/asm/page_64_types.h, 
    #ifdef CONFIG_KASAN
    #define KASAN_STACK_ORDER 1
    #else
    #define KASAN_STACK_ORDER 0
    #endif
    
    #define THREAD_SIZE_ORDER  (2 + KASAN_STACK_ORDER)
    // 16K,起始地址必须是 8192 的整数倍
    #define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)

    在内核栈的最高地址端,存放的是另一个结构struct  。其中,32 位和 64 位的定义不一样。

    当系统调用从用户态到内核态的时候,首先要做的第一件事情,就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在 struct  pt_regs 的寄存器变量里。

    这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。

    系统调用的时候,压栈的值的顺序和 struct pt_regs 中寄存器定义的顺序是一样的。

    // include/linux/sched.h
    union thread_union {
    #ifndef CONFIG_THREAD_INFO_IN_TASK
      struct thread_info thread_info;
    #endif
      unsigned long stack[THREAD_SIZE/sizeof(long)];
    };
    
    #ifdef __i386__
    struct pt_regs { unsigned long bx; unsigned long cx; unsigned long dx; unsigned long si; unsigned long di; unsigned long bp; unsigned long ax; unsigned long ds; unsigned long es; unsigned long fs; unsigned long gs; unsigned long orig_ax; unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss; }; #else struct pt_regs { unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long bp; unsigned long bx; unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long ax; unsigned long cx; unsigned long dx; unsigned long si; unsigned long di; unsigned long orig_ax; unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss; /* top of stack page */ }; #endif

    通过 task_struct 找内核栈

    如果有一个 task_struct 的 stack 指针在手,你可以通过下面的函数找到这个线程内核栈:

    static inline void *task_stack_page(const struct task_struct *task)
    {
      return task->stack;
    }

    从 task_struct 如何得到相应的 pt_regs 呢?我们可以通过下面的函数:

    /*
     * TOP_OF_KERNEL_STACK_PADDING reserves 8 bytes on top of the ring0 stack.
     * This is necessary to guarantee that the entire "struct pt_regs"
     * is accessible even if the CPU haven't stored the SS/ESP registers
     * on the stack (interrupt gate does not save these registers
     * when switching to the same priv ring).
     * Therefore beware: accessing the ss/esp fields of the
     * "struct pt_regs" is possible, but they may contain the
     * completely wrong values.
     */
    #define task_pt_regs(task) 
    ({                  
      unsigned long __ptr = (unsigned long)task_stack_page(task);  
      __ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING;    
      ((struct pt_regs *)__ptr) - 1;          
    })

    这是先从 task_struct 找到内核栈的开始位置。然后这个位置加上 THREAD_SIZE 就到了最后的位置,然后转换为 struct pt_regs,再减一,就相当于减少了一个 pt_regs 的位置,就到了这个结构的首地址。

    #ifdef CONFIG_X86_32
    # ifdef CONFIG_VM86
    #  define TOP_OF_KERNEL_STACK_PADDING 16
    # else
    #  define TOP_OF_KERNEL_STACK_PADDING 8
    # endif
    #else
    # define TOP_OF_KERNEL_STACK_PADDING 0
    #endif

    也就是说,32 位机器上是 8,其他是 0。这是为什么呢?因为压栈 pt_regs 有两种情况。

    我们知道,CPU 用 ring 来区分权限,从而 Linux 可以区分内核态和用户态。

    因此,第一种情况,我们拿涉及从用户态到内核态的变化的系统调用来说。因为涉及权限的改变,会压栈保存 SS、ESP 寄存器的,这两个寄存器共占用 8 个 byte。

    另一种情况是,不涉及权限的变化,就不会压栈这 8 个 byte。这样就会使得两种情况不兼容。如果没有压栈还访问,就会报错,所以还不如预留在这里,保证安全。

    在 64 位上,修改了这个问题,变成了定长的。

    通过内核栈找 task_struct

    32位

    struct thread_info {
      struct task_struct  *task;    /* main task structure */
      __u32      flags;    /* low level flags */
      __u32      status;    /* thread synchronous flags */
      __u32      cpu;    /* current CPU */
      mm_segment_t    addr_limit;
      unsigned int    sig_on_uaccess_error:1;
      unsigned int    uaccess_err:1;  /* uaccess failed */
    };
    
    
    static inline struct thread_info *current_thread_info(void)
    {
      return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);
    }

    我们常用 current_thread_info()->task 来获取 task_struct。

    64位

    struct thread_info {
            unsigned long           flags;          /* low level flags */
    };
    
    //  include/linux/thread_info.h
    #include <asm/current.h>
    #define current_thread_info() ((struct thread_info *)current)
    #endif
    
    
    // arch/x86/include/asm/current.h
    
    struct task_struct;
    
    // 声明 Per CPU 变量 current_task
    DECLARE_PER_CPU(struct task_struct *, current_task);
    
    static __always_inline struct task_struct *get_current(void)
    {
      return this_cpu_read_stable(current_task);
    }
    
    #define current get_current
    
    
    // arch/x86/kernel/cpu/common.c
    // 定义 Per CPU 变量 current_task,系统刚刚初始化的时候,current_task 都指向 init_task
    DEFINE_PER_CPU(struct task_struct *, current_task) = &init_task;

    到这里,你会发现,新的机制里面,每个 CPU 运行的 task_struct 不通过 thread_info 获取了,而是直接放在 Per CPU 变量里面了。

    多核情况下,CPU 是同时运行的,但是它们共同使用其他的硬件资源的时候,我们需要解决多个 CPU 之间的同步问题。

    Per CPU 变量是内核中一种重要的同步机制。Per CPU 变量就是为每个 CPU 构造一个变量的副本,这样多个 CPU 各自操作自己的副本,互不干涉。

    当某个 CPU 上的进程进行切换的时候,current_task 被修改为将要切换到的目标进程。

    例如,进程切换函数 __switch_to 就会改变 current_task。

    当要获取当前的运行中的 task_struct 的时候,就需要调用 this_cpu_read_stable 进行读取。

    __visible __notrace_funcgraph struct task_struct *
    __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
    {
      ......
      this_cpu_write(current_task, next_p);
      ......
      return prev_p;
    }
    
    #define this_cpu_read_stable(var)       percpu_stable_op("mov", var)

    others

    // state(状态)可以取的值定义在 include/linux/sched.h 头文件中
    /* Used in tsk->state: */
    #define TASK_RUNNING                    0
    #define TASK_INTERRUPTIBLE              1
    #define TASK_UNINTERRUPTIBLE            2
    #define __TASK_STOPPED                  4
    #define __TASK_TRACED                   8
    /* Used in tsk->exit_state: */
    #define EXIT_DEAD                       16
    #define EXIT_ZOMBIE                     32
    #define EXIT_TRACE                      (EXIT_ZOMBIE | EXIT_DEAD)
    /* Used in tsk->state again: */
    #define TASK_DEAD                       64
    #define TASK_WAKEKILL                   128
    #define TASK_WAKING                     256
    #define TASK_PARKED                     512
    #define TASK_NOLOAD                     1024
    #define TASK_NEW                        2048
    #define TASK_STATE_MAX                  4096
    
    // struct cred { ...... kuid_t uid; /* real UID of the task */ kgid_t gid; /* real GID of the task */ kuid_t suid; /* saved UID of the task */ kgid_t sgid; /* saved GID of the task */ kuid_t euid; /* effective UID of the task */ kgid_t egid; /* effective GID of the task */ kuid_t fsuid; /* UID for VFS ops */ kgid_t fsgid; /* GID for VFS ops */ ...... kernel_cap_t cap_inheritable; /* caps our children can inherit */ kernel_cap_t cap_permitted; /* caps we're permitted */ kernel_cap_t cap_effective; /* caps we can actually use */ kernel_cap_t cap_bset; /* capability bounding set */ kernel_cap_t cap_ambient; /* Ambient capability set */ ...... } __randomize_layout;
    // 权限机制 capabilities 用位图表示权限,在 capability.h 可以找到定义的权限。 #define CAP_CHOWN 0 #define CAP_KILL 5 #define CAP_NET_BIND_SERVICE 10 #define CAP_NET_RAW 13 #define CAP_SYS_MODULE 16 #define CAP_SYS_RAWIO 17 #define CAP_SYS_BOOT 22 #define CAP_SYS_TIME 25 #define CAP_AUDIT_READ 37 #define CAP_LAST_CAP CAP_AUDIT_READ ……

    从cred的定义可以看出,大部分是关于用户和用户所属的用户组信息。

    uid & gid: real user/group id。一般情况下,谁启动的进程,就是谁的 ID。但是权限审核的时候,往往不比较这两个,也就是说不大起作用。

    euid & egid: effective user/group id。当这个进程要操作消息队列、共享内存、信号量等对象的时候,其实就是在比较这个用户和组是否有权限。

    fsuid & fsgid:  filesystem user/group id。这个是对文件操作会审核的权限。

    一般说来,fsuid, euid, uid 是一样的,fsgid, egid , gid 也是一样的。因为谁启动的进程,就应该审核启动的用户到底有没有这个权限。

     

    除了以用户和用户组控制权限,Linux 还有另一个机制就是 capabilities。

    原来控制进程的权限,要么是高权限的 root 用户,要么是一般权限的普通用户,这时候的问题是,root 用户权限太大,而普通用户权限太小。

    有时候一个普通用户想做一点高权限的事情,必须给他整个 root 的权限。这个太不安全了。

    于是,我们引入新的机制 capabilities,用位图表示权限。

    对于普通用户运行的进程,当有这个权限的时候,就能做这些操作;没有的时候,就不能做,这样粒度要小很多。

    cap_permitted 表示进程能够使用的权限。但是真正起作用的是 cap_effective。

    cap_permitted 中可以包含 cap_effective 中没有的权限。一个进程可以在必要的时候,放弃自己的某些权限,这样更加安全。假设自己因为代码漏洞被攻破了,但是如果啥也干不了,就没办法进一步突破。

    cap_inheritable 表示当可执行文件的扩展属性设置了 inheritable 位时,调用 exec 执行该程序会继承调用者的 inheritable 集合,并将其加入到 permitted 集合。但在非 root 用户下执行 exec 时,通常不会保留 inheritable 集合,但是往往又是非 root 用户,才想保留权限,所以非常鸡肋。

    cap_bset,也就是 capability bounding set,是系统中所有进程允许保留的权限。如果这个集合中不存在某个权限,那么系统中的所有进程都没有这个权限。即使以超级用户权限执行的进程,也是一样的。这样有很多好处。例如,系统启动以后,将加载内核模块的权限去掉,那所有进程都不能加载内核模块。这样,即便这台机器被攻破,也做不了太多有害的事情。

    cap_ambient 是比较新加入内核的,就是为了解决 cap_inheritable 鸡肋的状况,也就是,非 root 用户进程使用 exec 执行一个程序的时候,如何保留权限的问题。当执行 exec 的时候,cap_ambient 会被添加到 cap_permitted 中,同时设置到 cap_effective 中。

     

  • 相关阅读:
    为什么要用do-while(0)?
    网络字节序&大小端存储
    sql语句w3school教程
    C++编码规范
    std::deque双端队列介绍
    gdb基本操作
    gdb调试多线程
    数据库基础
    删除vector所有元素
    stl迭代器失效
  • 原文地址:https://www.cnblogs.com/sunnycindy/p/14908098.html
Copyright © 2020-2023  润新知