posix-process

posix-process

posix-process主要提供了POSIX中要求的大部分关于进程、线程管理的结构体与函数。

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
posix-process/
├── Config.uk
├── include
│   ├── sys
│   │   └── prctl.h
│   └── uk
│   └── process.h
├── Makefile.uk
├── musl-imported
│   ├── arch
│   │   └── generic
│   │   └── bits
│   │   └── resource.h
│   └── include
│   └── sys
│   └── resource.h
└── process.c

数据结构

该结构体位于musl-imported/include/sys/resource.h中
该结构体主要定义了对资源的使用限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct rusage {

    struct timeval ru_utime;    //  线程在用户态下的执行时间

    struct timeval ru_stime;    //  线程在内核态下的执行时间

    /* linux extentions, but useful */

    long    ru_maxrss;          //  最大驻留集大小

    long    ru_ixrss;           //  整体共享内存大小

    long    ru_idrss;           //  整体非共享内存大小

    long    ru_isrss;           //  整体非共享堆栈大小

    long    ru_minflt;          //  页面回收(软页面错误)

    long    ru_majflt;          //  页面错误(硬页面错误)

    long    ru_nswap;           //  交换

    long    ru_inblock;         //  块输入操作

    long    ru_oublock;         //  块输出操作

    long    ru_msgsnd;          //  发送IPC消息

    long    ru_msgrcv;          //  接受IPC消息

    long    ru_nsignals;        //  接受的信号量

    long    ru_nvcsw;           //  自愿的上下文切换

    long    ru_nivcsw;          //  非自愿的上下文切换

    /* room for more... */

    long    __reserved[16];

};
  • ru_utime : 在用户态下执行程序的总时间,以timeval结构体表示。
  • ru_stime : 在内核态下执行程序的总时间,以timeval结构体表示。
  • ru_maxrss : 使用的最大驻留集大小(以KB为单位)。
  • ru_ixrss : TODO:在Linux中未使用,在Unikraft中未知。
  • ru_idrss : TODO:在Linux中未使用,在Unikraft中未知。
  • ru_isrss : TODO:在Linux中未使用,在Unikraft中未知。
  • ru_minflt : minor page fault(次要的缺页错误)。没有任何IO服务的缺页异常。即请求的页面已在内存中,但是没有向MMU注册,此时需要MMU建立映射关系。(注释中提到,没有IO的异常服务,应是指,不需要从外存读取到物理内存中)。
  • ru_majflt : major page fault(主要的缺页错误)。即请求的页面不在物理内存中,此时指示CPU从外存读取对应内容到物理内存中。
  • ru_nswap : 向主存交换的次数
  • ru_inblock : 文件系统需要输入操作的次数
  • ru_oublock : 文件系统需要输出操作的次数
  • ru_msgsnd : 发送消息的次数
  • ru_msgrcv : 接受消息的次数
  • ru_nsignals : 接受信号量的次数
  • ru_nvcsw : 自愿的上下文切换
  • ru_nivcsw : 非自愿的上下文切换
  • __reserved :

该结构体定义了对资源数量的限制

1
2
3
4
5
6
7
struct rlimit {

    rlim_t rlim_cur;        // 软限制

    rlim_t rlim_max;        // 硬限制

};
  • rlim_cur : 软限制,允许程序在一定时间内超过该限制。
  • rlim_max : 硬限制,在任何情况都不允许程序超过该限制。

该结构体定义了对线程在内存区域的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct prctl_mm_map {

    uint64_t start_code;    // 可执行代码的起点

    uint64_t end_code;      // 可执行代码的终点

    uint64_t start_data;    // 数据区域的起点

    uint64_t end_data;      // 数据区域的终点

    uint64_t start_brk;     // brk系统调用区域的起点

    uint64_t brk;           // brk系统调用区域的终点

    uint64_t start_stack;   // 在计算命令行参数、环境和shmat()系统调用所需空间时使用

    uint64_t arg_start;     // 表示为命令行参数、环境变量提供的内存区域

    uint64_t arg_end;       // 表示为命令行参数、环境变量提供的内存区域

    uint64_t env_start;     // 表示为命令行参数、环境变量提供的内存区域

    uint64_t env_end;       // 表示为命令行参数、环境变量提供的内存区域

    uint64_t *auxv;         // 辅助向量

    uint32_t auxv_size;     // 辅助向量大小

    uint32_t exe_fd;        // 可执行链接的文件描述符编号

};

具体可以参考手册prctl

提供的接口

prctl

在Linux中,prctl提供了一系列操作线程/进程的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#if UK_LIBC_SYSCALLS

int prctl(int option,

      unsigned long arg2,

      unsigned long arg3,

      unsigned long arg4,

      unsigned long arg5);

#endif /* UK_LIBC_SYSCALLS */

UK_SYSCALL_R_DEFINE(int, prctl, int, option,

            unsigned long, arg2,

            unsigned long, arg3,

            unsigned long, arg4,

            unsigned long, arg5)

{

    UK_WARN_STUBBED();

    return 0; /* syscall has no effect */

}

调用prctl()时,第一个参数决定做什么,后面的参数依赖于第一个参数。参数说明如下所示(以缩进代表参数次序)。下面举几个例子加以说明。

  • PR_CAP_AMBIENT : 根据arg2的值改变或者读取thread的环境能力集(the ambient capability set)。
    • PR_CAP_AMBIENT_RAISE : arg3指定的能力被添加到环境能力集中,指定的功能必须在已经存在于进程允许且可继承的集合中。
    • PR_CAP_AMBIENT_LOWER : arg3指定的能力将从集合中删除。
    • PR_CAP_AMBIENT_IS_SET: 判断arg3指定的能力是否在环境集中。
    • PR_CAP_AMBIENT_CLEAR_ALL :所有能力从环境集中删除。
      arg4与arg5需要设置为0。
  • PR_SET_CHILD_SUBREAPER : 用于收养孤儿线程,让当前进程充当init线程的功能,使其收养该线程树下的所有线程。
  • PR_SET_FP_MODE : 允许用户从用户空间操控浮点模式。
  • PR_SET_NO_NEW_PRIVS: 将调用线程的no_new_privs属性设置为arg2。如果no_new_privs设置为1时,禁止调用系统调用execve()。且该选项一旦设置,无法被取消;还会通过clone和fork传递给子进程。
  • PR_SET_PDEATHSIG : 将调用进程的父线程死亡信号设置为arg2(范围为1~MaxSig, 0代表清除信号)。对于Linux而言,父指的是创建该进程的父线程,对于Unikraft还需再深入了解。
  • PR_SET_NAME : 设定线程的名字。

exec函数族

在Unikraft中,提供了六个以exec开头的函数,以及两个对参数进行处理的函数。以及一个系统调用execve。查阅后发现,exec函数基本是尚未实现的。

1
2
3
4
5
6
7
8
9
10
11
12
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ...

        /*, (char *) NULL, char * const envp[] */);
int execlp(const char *file, const char *arg, ...

        /* (char  *) NULL */);
int execl(const char *path, const char *arg, ...

        /* (char  *) NULL */);

其中在exec函数族中大量调用了以下两个函数,但是分析发现,以下函数仅仅是在调用内部的print函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#define uk_pr_warn(fmt, ...)  uk_printk(KLVL_WARN,  (fmt), ##__VA_ARGS__)

static void exec_warn_argv_variadic(const char *arg, va_list args)

{

    int i = 1;

    char *argi;



    uk_pr_warn(" argv=[%s", arg);



    argi = va_arg(args, char *);

    while (argi) {

        uk_pr_warn("%s%s", (i > 0 ? ", " : ""), argi);

        i++;

        argi = va_arg(args, char *);

    }

    uk_pr_warn("]\n");

}



static void __exec_warn_array(const char *name, char *const argv[])

{

    int i = 0;



    uk_pr_warn(" %s=[", name);



    if (argv) {

        while (argv[i]) {

            uk_pr_warn("%s%s", (i > 0 ? ", " : ""), argv[i]);

            i++;

        }

    }

    uk_pr_warn("]\n");

}

#define exec_warn_argv(values) __exec_warn_array("argv", values)

#define exec_warn_envp(values) __exec_warn_array("envp", values)

声明但未实现的函数

除了prctl以及exec函数族以外,笔者整理了posix-process中提供了声明但是未实现的函数。以供日后参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
int system(const char *command);
FILE *popen(const char *command, const char *type __unused);
int pclose(FILE *stream __unused);
int wait(int *status __unused);
pid_t waitpid(pid_t pid __unused, int *wstatus __unused, int options __unused);
pid_t wait3(int *wstatus __unused, int options __unused,

        struct rusage *rusage __unused);
UK_SYSCALL_R_DEFINE(pid_t, wait4, pid_t, pid, int *, wstatus,

            int, options, struct rusage *, rusage);
int nice(int inc);
int tcsetpgrp(int fd __unused, pid_t pgrp);

已实现的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 由于Unikraft特性导致实现很简单或者是无法实现。
UK_SYSCALL_R_DEFINE(int, getpid); // return 1
UK_SYSCALL_R_DEFINE(pid_t, getppid); //return 0
UK_SYSCALL_R_DEFINE(pid_t, setsid);
UK_SYSCALL_R_DEFINE(pid_t, getsid, pid_t, pid);
UK_SYSCALL_R_DEFINE(int, setpgid, pid_t, pid, pid_t, pgid);
UK_SYSCALL_R_DEFINE(pid_t, getpgid, pid_t, pid);
UK_SYSCALL_R_DEFINE(pid_t, getpgrp);

UK_SYSCALL_R_DEFINE(int, getpriority, int, which, id_t, who);
UK_SYSCALL_R_DEFINE(int, setpriority, int, which, id_t, who, int, prio);

// getter setter都是用prlimit64构建出来的
UK_LLSYSCALL_R_DEFINE(int, prlimit64, int, pid, unsigned int, resource,

              struct rlimit *, new_limit, struct rlimit *, old_limit);
UK_SYSCALL_R_DEFINE(int, getrlimit, int, resource, struct rlimit *, rlim);
UK_SYSCALL_R_DEFINE(int, setrlimit, int, resource, const struct rlimit *, rlim);
UK_SYSCALL_R_DEFINE(int, getrusage, int, who,

            struct rusage *, usage);
int prlimit(pid_t pid, int resource, const struct rlimit *new_limit,

        struct rlimit *old_limit);

页面管理

MMU

MMU(Memory Management Unit),又叫内存管理单元,MMU是分页技术的硬件实现。CPU通过MMU,将虚拟地址VA转化为物理地址PA。
至于MMU采取的地址翻译规则取决于虚拟内存采用的组织机制,包括:分段机制和分页机制。CPU、MMU、主存的交互过程如下图所示,cache并未在图中画出。

TLB是MMU的一部分,TLB的本质就是cache,MMU在进行地址翻译前,会去查询TLB。如果TLB命中,那么就直接获取VA对应的PA,然后访问主存。否则进行地址翻译过程。
因此缺页中断也就分为了两种

  • 软缺页中断:此时进程所需要的页面已经在物理内存中,Page Fault Handler会让MMU建立对应的映射关系。
  • 硬缺页中断:此时进程所需要的页面不在物理内存中,此时将从磁盘中读取对应的页面,同时建立映射关系。
    缺页中断发生后,Page Fault Handler会判断缺页类型,然后进行缺页处理。与其他中断不同的是,Page Fault Handler在处理完后,会重新执行发生缺页中断的语句。