linux内核—启动程序

目录

前言

创建新进程

创建方式

函数do_fork->__do_fork

copy_process

wake_up_new_task

装载程序


前言

在shell中执行./a.out程序的时候,内核是如何执行的?

ret = fork()
if(ret > 0)
{
    //父进程
}
else if(ret == 0)//子进程
{
    execve()
}
else
{
    //失败
}

shell进程首先创建子进程,然后子进程装载程序a.out

创建新进程


Linux 新进程是从一个已经存在的进程复制出来的。内核使用静态数据结构造出0号内核线程,0号内核线程查分成1号内核线程和2号内核线程(kthreadd线程)。

  • 1号内核线程初始化后装载应用程序,变成1号进程,其它进程都是1号进程或者她的子孙进程叉分生成的。
  • 其它内核线程是kthread线程叉分成的。

创建方式


1) fork 子进程是父进程的一个副本,采用了写时复制的技术
2)vfork 创建子进程,之后子进程立即调用execve,父进程会睡眠等在子进程装载新程序。现在采用fork,vfork已经被抛弃
3)clone 可以精确地控制子进程和父进程共享哪些资源。可供pthread库用来创建线程。

SYSCALL_DEFINE0(fork)
展开 asmlinkage long sys_fork(void)

SYSCALL_DEFINE后面的数组表示参数个数,0表示没有参数,超过6个,使用SYSCALL_DEFINEx

Asmlinkage 表示C语言函数可以被汇编代码调用。如果使用C++编译器,asmlinkage被定义为extern "C",如果使用C编译器,asmlinkage是空的宏。

系统调用名称以sys_开头。

创建新进程p和生成新进程的关系
1,新进程是进程p的子进程
2、Clone 传入标志位CLONE_PARENT,新进程和进程p拥有同一个父进程
3、clone传入CLONE_THREAD,新进程和进程p属于同一个线程组。

函数do_fork->__do_fork

  • 调用函数copy_process,创建新进程
  • 调用函数wake_up_new_task 唤醒新进程
  • 如果是vfork,当前进程等待子进程状态程序。
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;

	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);
	add_latent_entropy();

	if (IS_ERR(p))
		return PTR_ERR(p);

	trace_sched_process_fork(current, p);//如果用ptrace监控新进程,那么在创建进程后立即向其发送SIGSTOP信号,以便连接的调试器检查其数据

	pid = get_task_pid(p, PIDTYPE_PID);//获取PID
	nr = pid_vnr(pid);//

	if (clone_flags & CLONE_PARENT_SETTID)
		put_user(nr, args->parent_tid);

	if (clone_flags & CLONE_VFORK) {
		p->vfork_done = &vfork;
		init_completion(&vfork);
		get_task_struct(p);
	}

	wake_up_new_task(p);//唤醒新进程

	/* forking complete and child started to run, tell ptracer */
	if (unlikely(trace))
		ptrace_event_pid(trace, pid);

	if (clone_flags & CLONE_VFORK) {//如果是系统调用vfork
		if (!wait_for_vfork_done(p, &vfork))//启用子进程的完成机制
			ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
	}

	put_pid(pid);
	return nr;
}

copy_process

static __latent_entropy struct task_struct *copy_process(
					struct pid *pid,
					int trace,
					int node,
					struct kernel_clone_args *args)
{

	 *///非法检查
	//同时设置NEWS FS即 新进程属于新的挂载命令空间,同时和当前进程共享文件系统信息
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);

	//同时设置CLONE_NEWUSER CLONE_FS,即新进程属于新的用户命名空间,同时和当前进程共享文件系统信息。
	if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
		return ERR_PTR(-EINVAL);

	if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
		return ERR_PTR(-EINVAL);

	//新进程和当前进程共享信号处理程序,但不是共享虚拟内存
	if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
		return ERR_PTR(-EINVAL);

	//新进程和当前进程称为兄弟进程,并且当前进程是某个进程好命名空间中的1号进程
	if ((clone_flags & CLONE_PARENT) &&
				current->signal->flags & SIGNAL_UNKILLABLE)
		return ERR_PTR(-EINVAL);

	//新进程和当前进程属于同一个线程组,同时新进程不属于用户命名空间或者进程号命名空间。这种组合违法
	//说明同一个线程组的所有线程必须属于相同的用户命名空间和进程号命名空间
	if (clone_flags & CLONE_THREAD) {
		if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
		    (task_active_pid_ns(current) != nsp->pid_ns_for_children))
			return ERR_PTR(-EINVAL);
	}


	//把当前进程的进程描述符复制一份,为新进程分配内核栈
	p = dup_task_struct(current, node);
	if (!p)
		goto fork_out;


	//检查用户的进程数量限制,如果当前进程的用户创建的进程数量达到或者超过限制,并且用户不是根用户,
	//也没有忽略资源限制的权限CAP_SYS_RESOURCE和系统管理权限CAP_SYS_ADMIN,那么不允许创建新进程
	if (atomic_read(&p->real_cred->user->processes) >=
			task_rlimit(p, RLIMIT_NPROC)) {
		if (p->real_cred->user != INIT_USER &&
		    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
			goto bad_fork_free;
	}
	current->flags &= ~PF_NPROC_EXCEEDED;
	//复制或共享证书,证书存放进程的用户标识符,组标识符和访问权限。
	retval = copy_creds(p, clone_flags);
	if (retval < 0)
		goto bad_fork_free;

	retval = -EAGAIN;
	//检查线程数量,大于最大进程数量,不允许创建
	if (nr_threads >= max_threads)
		goto bad_fork_cleanup_count;


	//为新进程设置调度器相关参数
	retval = sched_fork(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_policy;

	retval = perf_event_init_task(p);
	if (retval)
		goto bad_fork_cleanup_policy;
	retval = audit_alloc(p);
	if (retval)
		goto bad_fork_cleanup_perf;
	/* copy all the process information */
	shm_init_task(p);
	retval = security_task_alloc(p, clone_flags);
	if (retval)
		goto bad_fork_cleanup_audit;
	//系统5个信号量的共享问题
	retval = copy_semundo(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_security;
	//打开文件列表,属于同一个线程组的线程才会共享打开文件表
	retval = copy_files(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_semundo;
	retval = copy_fs(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_files;
	retval = copy_sighand(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_fs;
	retval = copy_signal(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_sighand;
	retval = copy_mm(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_signal;
	retval = copy_namespaces(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_mm;
	retval = copy_io(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_namespaces;
	//复制寄存器
	retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
				 args->tls);
	if (retval)
		goto bad_fork_cleanup_io;

	stackleak_task_init(p);
	//进程号与进程的关系
	if (pid != &init_struct_pid) {
		//分配进程号
		pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
				args->set_tid_size);
		if (IS_ERR(pid)) {
			retval = PTR_ERR(pid);
			goto bad_fork_cleanup_thread;
		}
	}

	return p;
}

dup_task_struct 为新进程的进程描述符分配内存

把当前进程的进程描述符复制一份,为新进程分配内核栈。

内核栈布局 thread_info位置不同
        1、结构体thread_info占用内核栈空间,在内核栈顶部,成员task指向进程描述符。
        2、结构体thread_info没有占用内核栈空间,是进程描述符的第一个成员。

        如果是第二种布局,则打开宏CONFIG_THREAD_INFO_IN_TASK


ARM64采用第二种内核栈布局。
        好处:thread_info作为进程描述符的第一个成员,它的地址和进程描述符的地址相同。当线程在内核模式运行时,ARM64架构的内核使用用户栈指针寄存器SP_EL0存放当前进程的thread_info结构体的地址,通过这个寄存器即可以得到thread_info结构体的地址,也可以得到进程描述符的地址。

内核栈的长度是THREAD_SIZE,ARM64架构定义的内核栈长度16KB

结构体thread_info存放汇编代码需要直接访问底层数据。ARM64架构定义的结构体

struct thread_info{
    unsigned long flags;
    mm_segment_t addr_limit;
    int preempt_count;
}
  • flags:底层标志,
  •             _TIF_SIGPENDING 表示进程有需要处理的信号;
  •             _TIF_NEED_PESCHED 调度器需要重新调度进程。
  • addr_limit 进程可以访问的地址空间上限;
  • preempt_count 抢占计数器

检查用户的进程数限制

复制或共享证书copy_creds

检查进程数量限制

函数sched_fork 为新进程设置调度器相关的参数; 

复制或共享资源 copy_semundo 信号量; copy_files打开文件表; copy_fs 文件系统等

复制寄存器值 copy_thread_tls,复制房钱进程的寄存器值,并修改一部分寄存器的值。

进程有两处用来保存寄存器值:

1)从用户模式切换到内核模式时:把用户模式的各种寄存器保存在内核底部的结构体pt_regs中;

2)进程调度器调度进程时:切换出去的进程把寄存器值保存在进程描述符的成员thread中。

int copy_thread_tls(unsigned long clone_flags, unsigned long stack_start,
		unsigned long stk_sz, struct task_struct *p, unsigned long tls)
{
	struct pt_regs *childregs = task_pt_regs(p);

	memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));


	fpsimd_flush_task_state(p);

	if (likely(!(p->flags & PF_KTHREAD))) {
		*childregs = *current_pt_regs();
		childregs->regs[0] = 0;//0 X0是寄存器存放系统调用的返回值


		//把子进程的TPIDR_EL0 寄存器设置为当前tpidr_el0的值。其是用户读写线程标识符寄存器
		//thread库用来存放没线程数据的基准地址,存放每线程数据的区域通常被称为线程本地存储
		*task_user_tls(p) = read_sysreg(tpidr_el0);
		//如果使用系统调用clone创建线程时,指定了用户栈的起始地址,那么把新线程栈指针就存取设置为用户栈
		//的起始地址
		if (stack_start) {
			if (is_compat_thread(task_thread_info(p)))
				childregs->compat_sp = stack_start;
			else
				childregs->sp = stack_start;
		}

		if (clone_flags & CLONE_SETTLS)
			p->thread.uw.tp_value = tls;//clone的第四个参数tls指定线程本地存储的地址
	} else {
		memset(childregs, 0, sizeof(struct pt_regs));
		childregs->pstate = PSR_MODE_EL1h;//5 处理器状态 第0位 栈指针选择符,1:选择栈之战寄存器SP_EL1 2:3 异常级别,值1表示异常级别1
		if (IS_ENABLED(CONFIG_ARM64_UAO) &&
		    cpus_have_const_cap(ARM64_HAS_UAO))
			childregs->pstate |= PSR_UAO_BIT;

		if (arm64_get_ssbd_state() == ARM64_SSBD_FORCE_DISABLE)
			set_ssbs_bit(childregs);

		if (system_uses_irq_prio_masking())
			childregs->pmr_save = GIC_PRIO_IRQON;

		p->thread.cpu_context.x19 = stack_start;//函数地址,用来创建内核线程的函数kernel_thread的第一参数
		p->thread.cpu_context.x20 = stk_sz;//参数 ,用来创建内核线程的函数kernel_thread的第二参数
	}
	p->thread.cpu_context.pc = (unsigned long)ret_from_fork;//子进程的程序计数器,调度入口
	p->thread.cpu_context.sp = (unsigned long)childregs;//sp指向内核栈底部pt_regs起始位置

	ptrace_hw_copy_thread(p);

	return 0;
}

把新进程的进程描述符的成员thread.cpu_context清零,在调度进程时切换出去的进程使用这个成员保存通用寄存器的值。

1、用户进程处理        

        子进程把当前进程内核底部的pt_regs结构体复制一份。当前进程从用户模式切换到内核模式时,把用户模式的各种寄存器放在内核栈底的pt_regs结构体中。

        设置X0寄存器,其存放系统调用的返回值。

        设置子进程的TPIDR_EL0寄存器

        clone创建如果指定了用户栈的起始地址,把新线程栈指针寄存器SP_EL0设置为用户栈的起始地址。

2、内核线程处理

        把子进程内核底部的pt_regs结构体清零

        子进程处理器状态设置为PSR_MODE_EL1h

        设置x19,线程函数的地址,用来创建内核线程函数kernel_thread的第一个参数

        设置x20,传给线程函数的参数,用来创建内核线程函数kernel_thread的第二个参数

        设置子进程程序计数器为函数ret_from_fork。当子函数执行时,从此开始执行

wake_up_new_task

第一时间唤醒新创建的任务

该函数将对每个新创建的上下文执行一些初始调度程序统计内务处理

加入到运行队列上

void wake_up_new_task(struct task_struct *p)
{
	struct rq_flags rf;
	struct rq *rq;

	raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
	p->state = TASK_RUNNING;
#ifdef CONFIG_SMP
	p->recent_used_cpu = task_cpu(p);
	__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif
	rq = __task_rq_lock(p, &rf);
	update_rq_clock(rq);
	post_init_entity_util_avg(p);

	activate_task(rq, p, ENQUEUE_NOCLOCK);
	trace_sched_wakeup_new(p);
	check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
	if (p->sched_class->task_woken) {
		rq_unpin_lock(rq, &rf);
		p->sched_class->task_woken(rq, p);
		rq_repin_lock(rq, &rf);
	}
#endif
	task_rq_unlock(rq, p, &rf);
}
  • 把新进程状态从TASK_NEW切换到TASK_RUNNING
  • 在SMP系统上,创建进程就是执行负载均衡的时机,为新进程选择一个负载最轻的处理器
  • 锁住运行队列
  • 更新运行队列时钟
  • 把公平队列的平均负载统计值,推算进程的平均负载统计值
  • 把新进程插入运行队列
  • 检查新进程是否可以抢占当前进程
  • 在SMP系统上,调度调度类的take_woken方法
  • 释放运行队列的锁

新进程第一次运行

ENTRY(ret_from_fork)
	bl	schedule_tail
	cbz	x19, 1f				// not a kernel thread
	mov	x0, x20
	blr	x19 //调用线程函数
1:	get_current_task tsk //用户进程x28 = sp_el0=当前进程的thread_info结构体的地址
	b	ret_to_user//返回用户模式
ENDPROC(ret_from_fork)
NOKPROBE(ret_from_fork)

新进程第一次运行,从函数ret_from_fork开始;

在copy_thread时,如果是内核线程,寄存器x19存放线程的函数地址,x20存放参数;如果是用进程,x19存放的值是0

调用schedule_tail,为上一个进程执行清理操作。(执行运行队列所有负载均衡回调,开启抢占,如果pthread库在调用clone创建线程设置标志CLONE_CHILD_SETTID,把新进程把自己的进程标识符写到指定位置)。

如果是用户进程,使用x28存放当前进程的thread_info结构体的地址,然后跳转到ret_to_user

如果是内核线程,调用线程函数,blr x19

装载程序

int execve (const char *filename , char • const argv[] , char • const envp[]) ;
int execveat (int dirfd, const char •pa 七hname , char •const argv[J , char •const envp
I 1, int flags) ;


最终调用以下函数

static int __do_execve_file(int fd, struct filename *filename,
			    struct user_arg_ptr argv,
			    struct user_arg_ptr envp,
			    int flags, struct file *file)
{
	retval = prepare_bprm_creds(bprm);
	if (retval)
		goto out_free;

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

	if (!file)
		file = do_open_execat(fd, filename, flags);


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

	retval = prepare_arg_pages(bprm, argv, envp);
	if (retval < 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;
}

do_open_execat打开可执行文件

sched_exec 程序装载是实现处理器负载均衡的机会,此时进程在内存和缓存中的数据最少。选择负载轻的处理器,然后唤醒当前处理器的迁移线程,把当前进程睡眠等迁移线程把自己迁移到目标处理器

bprmm_mm_init 创建新的内存描述符,分配临时的用户栈

调用函数prepare_binprm 设置进程证书,然后读取文件前面的128字节到缓冲区

依次把文件名,环境字符串和参数字符串压到用户栈

exec_binprm,-> search_binary_handler 尝试注册过的每种二进制格式的处理程序,直到某个处理程序识别正在装载的程序为止。

二进制格式 

struct linux_binfmt {
	struct list_head lh;
	struct module *module;
	int (*load_binary)(struct linux_binprm *);
	int (*load_shlib)(struct file *);
	int (*core_dump)(struct coredump_params *cprm);
	unsigned long min_coredump;	/* minimal dump size */
} __randomize_layout;

每种二进制格式必须使用register_binfmt向内核注册。

  • load_binary加载普通程序
  • load_sglib加载共享库
  • core_dump 进程退出时生成的核心转存文件

装载ELF程序 

ELF文件:是可以执行与可链接格式,主要分为以下4中类型

  • 目标文件 .o
  • 可执行文件
  • 共享库 .so
  • 核心转存文件

load_elf_binary 装载程序步骤 检测ELF首部,读取程序首部表;设置内存映射布局;确定用户栈;可加载的段,未初始化程序段映射到虚拟地址空间;找到程序入口;获得环境变量;设置程序计数器和栈指针寄存器。

 参考

https://course.0voice.com/v1/course/intro?courseId=2&agentId=0

猜你喜欢

转载自blog.csdn.net/WANGYONGZIXUE/article/details/129115153