vlambda博客
学习文章列表

linux内核如何启动用户空间进程【01】




linux内核启动用户空间进程的整个过程比较长,故本部分分成两篇文章来叙述,本文为第一篇。

一、开篇引入

注:本文源码基于linux 内核版本:4.1.15



start_kernel()函数的最后会调用rest_init()函数,至此我们已经知道在rest_init()函数中会调用kernel_thread来创建init线程(可参考《》一文)。

kernel_init()为init线程的线程入口函数。在kernel_init()线程函数执行的最后,将启动第一个用户空间进程,具体操作执行详情如下代码所示:

 if (ramdisk_execute_command) {
  ret = run_init_process(ramdisk_execute_command);
  if (!ret)
   return 0;
  pr_err("Failed to execute %s (error %d)\n",
         ramdisk_execute_command, ret);
 }

 if (execute_command) {
  ret = run_init_process(execute_command);
  if (!ret)
   return 0;
  panic("Requested init %s failed (error %d).",
        execute_command, ret);
 }
 if (!try_to_run_init_process("/sbin/init") ||
     !try_to_run_init_process("/etc/init") ||
     !try_to_run_init_process("/bin/init") ||
     !try_to_run_init_process("/bin/sh"))
  return 0;

 panic("No working init found.  Try passing init= option to kernel. "
       "See Linux Documentation/init.txt for guidance.");

kernel_init()函数中,会对内核启动命令进行判断,如果有路径对应的命令输入,则执行路径下对应的用户空间程序。execute_command 的值可通过uboot 来传递,在 bootargs 中使用“init=xxxx”即可将值传给linux内核,例如“init=/linuxrc”表示根文件系统中的 linuxrc 就是要执行的用户空间的 init 程序。如果内核没有找到用户空间下的程序或指定用户空间下的程序无法执行,kernel_init则进行后续操作:执行linux内核中默认指定的用户空间路径下的程序,指定的执行路径如下所示: 

/sbin/init 、/etc/init  、/bin/init、/bin/sh。

如果以上linux内核指定的默认用户空间路径下的程序无法执行或不存在,那么linux内核将输出panic信息。

(注,本文的用户空间指根文件系统下的执行程序

try_to_run_init_process(const char * init_filename)函数本质是run_init_process()函数的封装,如下代码所示:

static int try_to_run_init_process(const char *init_filename)
{
 int ret;

 ret = run_init_process(init_filename);

 if (ret && ret != -ENOENT) {
  pr_err("Starting init: %s exists but couldn't execute it (error %d)\n",
         init_filename, ret);
 }

 return ret;
}

run_init_process(const char * init_filename)函数将调用do_execve()函数,如下所示:

static int run_init_process(const char *init_filename)
{
 argv_init[0] = init_filename;
 return do_execve(getname_kernel(init_filename),
  (const char __user *const __user *)argv_init,
  (const char __user *const __user *)envp_init);
}

在该函数下,除了使用init_filename参数,还使用到了两个字符串数组:argv_init和envp_init。具体定义如下:

static const char *argv_init[MAX_INIT_ARGS+2] = { "init"NULL, };
const char *envp_init[MAX_INIT_ENVS+2] = { "HOME=/""TERM=linux"NULL, };

二、do_execve函数分析

在run_init_process函数中会调用do_execve执行后续的启动用户空间进程的操作。该函数定义在 /fs/exec.c文件中,如下代码片段:

int do_execve(struct filename *filename,
 const char __user *const __user *__argv,
 const char __user *const __user *__envp)

{
 struct user_arg_ptr argv = { .ptr.native = __argv };
 struct user_arg_ptr envp = { .ptr.native = __envp };
 return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

在do_execve函数中,定义了两个结构体类型的变量:argv和envp。并使用__argc初始化argv结构体变量中的ptr.native成员值。使用__envp初始化envp结构体变量中的ptr.native成员值。然后将filename、argv、envp值传给函数do_execveat_common()。该函数为执行用户空间程序做了全部的工作,接下来,让我们来看看该函数。

三、do_execveat_common函数分析

do_execveat_common()函数用于启动一个新的进程,此函数同样定义在 /fs/exec.c文件中,如下代码片段(代码比较长):

static int do_execveat_common(int fd, struct filename *filename,
         struct user_arg_ptr argv,
         struct user_arg_ptr envp,
         int flags)

{
 char *pathbuf = NULL;
 struct linux_binprm *bprm;
 struct file *file;
 struct files_struct *displaced;
 int retval;

 if (IS_ERR(filename))
  return PTR_ERR(filename);


 if ((current->flags & PF_NPROC_EXCEEDED) &&
     atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
  retval = -EAGAIN;
  goto out_ret;
 }

 current->flags &= ~PF_NPROC_EXCEEDED;

 retval = unshare_files(&displaced);
 if (retval)
  goto out_ret;

 retval = -ENOMEM;
 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
 if (!bprm)
  goto out_files;

 retval = prepare_bprm_creds(bprm);
 if (retval)
  goto out_free;

 check_unsafe_exec(bprm);
 current->in_execve = 1;

 file = do_open_execat(fd, filename, flags);
 retval = PTR_ERR(file);
 if (IS_ERR(file))
  goto out_unmark;

 sched_exec();

 bprm->file = file;
 if (fd == AT_FDCWD || filename->name[0] == '/') {
  bprm->filename = filename->name;
 } else {
  if (filename->name[0] == '\0')
   pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
  else
   pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
         fd, filename->name);
  if (!pathbuf) {
   retval = -ENOMEM;
   goto out_unmark;
  }

  if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
   bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
  bprm->filename = pathbuf;
 }
 bprm->interp = bprm->filename;

 retval = bprm_mm_init(bprm);
 if (retval)
  goto out_unmark;

 bprm->argc = count(argv, MAX_ARG_STRINGS);
 if ((retval = bprm->argc) < 0)
  goto out;

 bprm->envc = count(envp, MAX_ARG_STRINGS);
 if ((retval = bprm->envc) < 0)
  goto out;

 retval = prepare_binprm(bprm);
 if (retval < 0)
  goto out;

 retval = copy_strings_kernel(1, &bprm->filename, bprm);
 if (retval < 0)
  goto out;

 bprm->exec = bprm->p;
 retval = copy_strings(bprm->envc, envp, bprm);
 if (retval < 0)
  goto out;

 retval = copy_strings(bprm->argc, argv, bprm);
 if (retval < 0)
  goto out;

 retval = exec_binprm(bprm);
 if (retval < 0)
  goto out;

 /* execve succeeded */
 current->fs->in_exec = 0;
 current->in_execve = 0;
 acct_update_integrals(current);
 task_numa_free(current);
 free_bprm(bprm);
 kfree(pathbuf);
 putname(filename);
 if (displaced)
  put_files_struct(displaced);
 return retval;

out:
 if (bprm->mm) {
  acct_arg_size(bprm, 0);
  mmput(bprm->mm);
 }

out_unmark:
 current->fs->in_exec = 0;
 current->in_execve = 0;

out_free:
 free_bprm(bprm);
 kfree(pathbuf);

out_files:
 if (displaced)
  reset_files_struct(displaced);
out_ret:
 putname(filename);
 return retval;
}

对于do_execveat_common()函数,内容较多且较复杂,该函数将在《linux内核如何启动用户空间进程【02】》中进行详细分析。本文已完啦!!

但linux内核启动用户空间进程之路未完,容小生慢慢道来。O(∩_∩)O哈哈~