跳转至

进程管理

task_struct

调度与进程切换

与调度相关的:

在 task_struct 中有几个字段是表示进程优先级的,在进程调度的时候会根据这几个字段来决定优先让哪个任务(进程或线程)开始执行

  • static_prio: 用来保存静态优先级,可以调用 nice 系统直接来修改取值范围为 100~139
  • rt_priority: 用来保存实时优先级,取值范围为 0~99
  • prio: 用来保存动态优先级
  • normal_prio: 它的值取决于静态优先级和调度策略
struct task_struct {
 ......

 /*
 *调度类。用 sched_class 对调度器进行抽象 
 *Stop调度器:stop_sched_class
 *Deadline调度器:dl_sched_class
 *RT调度器:rt_sched_class
 *CFS调度器:cfs_sched_class
 *IDLE-Task调度器:idle_sched_class
 */
 const struct sched_class *sched_class;
 //CFS调度实体
 struct sched_entity  se;
 //RT调度实体
 struct sched_rt_entity  rt;

 ......

 #ifdef CONFIG_CGROUP_SCHED
 //任务组(在每个CPU上都会维护一个CFS调度实体、CFS运行队列; RT调度实体,RT运行队列)
 struct task_group  *sched_task_group;
 #endif
 //DL调度实体
 struct sched_dl_entity  dl;

 ......

 /*
 *进程的调度策略,有6种。
 *限期进程调度策略:SCHED_DEADLINE。DL调度器
 *实时进程调度策略:SCHED_FIFO,SCHED_RR。RT调度器
 *普通进程调度策略:SCHED_NORMAL,SCHED_BATCH,SCHED_IDLE。CFS调度器
 */
 unsigned int   policy;

 ......
}

进程地址空间

struct mm_struct {
 struct vm_area_struct * mmap;  /* list of VMAs */
 struct rb_root mm_rb;

 unsigned long mmap_base;  /* base of mmap area */
 unsigned long task_size;  /* size of task vm space */
 unsigned long start_code, end_code, start_data, end_data;
 unsigned long start_brk, brk, start_stack;
 unsigned long arg_start, arg_end, env_start, env_end;
}

用户mm

内核mm

进程文件系统信息

//file:include/linux/fs_struct.h
struct fs_struct {
 ...
 struct path root, pwd;
};

//file:include/linux/path.h
struct path {
 struct vfsmount *mnt;
 struct dentry *dentry;
};

fs_struct包含两个path对象,每个path中都指向一个struct dentry

进程打开文件信息

namespace

用于隔离内核资源

具体实现:把一个或者多个进程的相关资源指定在同一个namespace中

struct nsproxy {
 atomic_t count;
 struct uts_namespace *uts_ns;
 struct ipc_namespace *ipc_ns;
 struct mnt_namespace *mnt_ns;
 struct pid_namespace *pid_ns;
 struct net       *net_ns;
};

命名空间包括PID命名空间、挂载点命名空间、网络命名空间等多个

fork()

do_fork()

SYSCALL_DEFINE0(fork)
{
 return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}

do_fork传入的第一个参数是一个flag选项,可以传入的值:

#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200 
#define CLONE_FILES 0x00000400 

这里传入的是一个SIGCHILD(子进程在终止后向父进程发送SIGCHILD信号通知)

long do_fork(unsigned long clone_flags,
    unsigned long stack_start,
    unsigned long stack_size,
    int __user *parent_tidptr,
    int __user *child_tidptr)
{
 //复制一个 task_struct 出来
 struct task_struct *p;
 p = copy_process(clone_flags, stack_start, stack_size,
    child_tidptr, NULL, trace);

 //子任务加入到就绪队列中去,等待调度器调度
 wake_up_new_task(p);
 ...
}

在创建完毕后,调用 wake_up_new_task 将新创建的任务添加到就绪队列中,等待调度器调度执行

copy_process()

static struct task_struct *copy_process(...)
{
 //3.1 复制进程 task_struct 结构体
 struct task_struct *p;
 p = dup_task_struct(current);
 ...

 //3.2 拷贝 files_struct
 retval = copy_files(clone_flags, p);

 //3.3 拷贝 fs_struct
 retval = copy_fs(clone_flags, p);

 //3.4 拷贝 mm_struct
 retval = copy_mm(clone_flags, p);

 //3.5 拷贝进程的命名空间 nsproxy
 retval = copy_namespaces(clone_flags, p);

 //3.6 申请 pid && 设置进程号
 pid = alloc_pid(p->nsproxy->pid_ns);
 p->pid = pid_nr(pid);
 p->tgid = p->pid;
 if (clone_flags & CLONE_THREAD)
  p->tgid = current->tgid;

 ......
}

复制进程task_struct

  • dup_task_struct 时传入的参数是 current,它表示的是当前进程
  • dup_task_struct 里,会申请一个新的 task_struct 内核对象,然后将当前进程复制给它
  • 这次拷贝只会拷贝 task_struct 结构体本身,它内部包含的 mm_struct 等成员只是复制了指针,仍然指向和 current 相同的对象

dup_task_struct()

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
 //申请 task_struct 内核对象
 tsk = alloc_task_struct_node(node);

 //复制 task_struct
 err = arch_dup_task_struct(tsk, orig);
 ...
}
static struct kmem_cache *task_struct_cachep;

//申请 task_struct 内核对象
static inline struct task_struct *alloc_task_struct_node(int node)
{
 return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}
//复制 task_struct
int arch_dup_task_struct(struct task_struct *dst,
         struct task_struct *src)
{
 *dst = *src;
 return 0;
}

复制files_struct

static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{
 struct files_struct *oldf, *newf;
 oldf = current->files;

 if (clone_flags & CLONE_FILES) { //判断是否有CLONE_FILES,如果有的话就不执行dup_fd
  atomic_inc(&oldf->count); //增加引用计数
  goto out;
 }
 newf = dup_fd(oldf, &error);
 tsk->files = newf;
 ...
}

由于前面fork()的时候没有传入CLONE_FILES标志,所以这里会执行到dup_fd函数

struct files_struct *dup_fd(struct files_struct *oldf, ...)
{
 //为新 files_struct 申请内存
 struct files_struct *newf;
 newf = kmem_cache_alloc(files_cachep, GFP_KERNEL);

 //初始化 & 拷贝
 new_fdt->max_fds = NR_OPEN_DEFAULT;
 ...
}

复制fs_struct

static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
{
 struct fs_struct *fs = current->fs;
 if (clone_flags & CLONE_FS) { //在创建进程的时候,没有传递 CLONE_FS 这个标志,所会进入到 copy_fs_struct 函数中申请新的 fs_struct 并进行赋值
  fs->users++;
  return 0;
 }
 tsk->fs = copy_fs_struct(fs);
 return 0;
}
struct fs_struct *copy_fs_struct(struct fs_struct *old)
{
 //申请内存
 struct fs_struct *fs = kmem_cache_alloc(fs_cachep, GFP_KERNEL);

 //赋值
 fs->users = 1;
 fs->root = old->root;
 fs->pwd = old->pwd;
 ...
 return fs;
}

复制mm_struct

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
 struct mm_struct *mm, *oldmm;
 oldmm = current->mm; //表示当前的mm

 if (clone_flags & CLONE_VM) { //do_fork 被调用时也没有传 CLONE_VM,所以会调用 dup_mm 申请一个新的地址空间出来
  atomic_inc(&oldmm->mm_users);
  mm = oldmm; //新的为当前的
  goto good_mm;
 }
 mm = dup_mm(tsk);
good_mm:
 return 0; 
}
struct mm_struct *dup_mm(struct task_struct *tsk)
{
 struct mm_struct *mm, *oldmm = current->mm;
 mm = allocate_mm();
 memcpy(mm, oldmm, sizeof(*mm));
 ...
}

在 dup_mm 中,通过 allocate_mm 申请了新的 mm_struct,而且还将当前进程地址空间 current->mm 拷贝到新的 mm_struct 对象里了

复制进程命名空间

申请pid

static struct task_struct *copy_process(...)
{
 ...
 //申请pid
 pid = alloc_pid(p->nsproxy->pid_ns);

 //赋值
 p->pid = pid_nr(pid);
 p->tgid = p->pid;
 ...
}
struct pid *alloc_pid(struct pid_namespace *ns)
{
 //申请 pid 内核对象
 pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
 if (!pid)
  goto out;

 //调用到alloc_pidmap来分配一个空闲的pid编号
 //注意,在每一个命令空间中都需要分配进程号
 tmp = ns;
 pid->level = ns->level;
 for (i = ns->level; i >= 0; i--) {
  nr = alloc_pidmap(tmp);
  pid->numbers[i].nr = nr;
  ...
 }
 ...
 return pid
}
  • 这里PID是一个结构体,先使用kmem_cache_alloc将其申请出来
  • 接下来调用 alloc_pidmap 到 pid 命名空间中申请一个 pid 号出来,申请完后赋值记录

操作系统是如何记录使用过的进程号呢?

在Linux内部,为了节约内存,进程号是通过 bitmap 来管理的

在每一个 pid 命名空间内部,会有一个或者多个页面来作为 bitmap。其中每一个 bit 位(注意是 bit 位,不是字节)的 0 或者 1 的状态来表示当前序号的 pid 是否被占用

#define BITS_PER_PAGE  (PAGE_SIZE * 8)
#define PIDMAP_ENTRIES  ((PID_MAX_LIMIT+BITS_PER_PAGE-1)/BITS_PER_PAGE)
struct pid_namespace {
 struct pidmap pidmap[PIDMAP_ENTRIES];
 ...
}

在 alloc_pidmap 中就是以 bit 的方式来遍历整个 bitmap,找到合适的未使用的 bit,将其设置为已使用,然后返回

static int alloc_pidmap(struct pid_namespace *pid_ns)
{
 ...
 map = &pid_ns->pidmap[pid/BITS_PER_PAGE];
}

进入就绪队列

long do_fork(...)
{
 //复制一个 task_struct 出来
 struct task_struct *p;
 p = copy_process(clone_flags, stack_start, ...);

 //子任务加入到就绪队列中去,等待调度器调度
 wake_up_new_task(p);
 ...
}


进程切换