linux源码分析--进程管理学习笔记

前言:最近在学eBPF追踪linux内核的进程/线程,首先要做的就是要先熟悉内核,才能知道要探测哪个函数

这篇文章是博主学习多篇技术博客记录的笔记,在写的时候参考了很多文章,因为每篇文章都有各自重点,为了学习的连贯性,博主将它们整理在这里,并加上一些自己的理解,下面是本文参考的文章:
task_struct 详解
linux 内核源码 fork 解读
linux源码分析 - 进程
Linux下fork()、vfork()、clone()和exec()的区别
linux设计与实现第三版-在线阅读
内核创建进程的流程

linux源码在线阅读

数据结构task_struct

linux定义了task_struct结构体来描述进程管理的所有信息,包括了进程的pid,状态,父子进程和打开文件等等信息,整个结构体比较大,在32位机器上占有1.7KB。

在内核中大部分处理进程的代码都通过task_stuct进行的

// 部分源码        
struct task_stuct{
            struct thread_info thread_info ;
            // 进程状态
            volatile long state ; 
			/*
			flags是进程当前的状态标志:
			0x00000002表示进程正在被创建; //通过宏定义实现
            0x00000004表示进程正准备退出; 
            0x00000040 表示此进程被fork出,但是并没有执行exec;
            0x00000400表示此进程由于其他进程发送相关信号而被杀死 。
			*/
            unsigned int flags ;
            // 指向内核栈的指针,通过它可以找到thread_info 
            void *stack ;
			
            struct list_head tasks ;

            struct mm_struct *mm, *active_mm;
            
            pid_t pid ; 
            
            pid_t tgid ;
            // 用于连接pid,tgid,pgrp,session哈希表
            struct pid_link pids[PIDTYPE_MAX] ;
            
            //指向创建其的父进程,如果不存在,则执行那个init进程
            struct task_struct  __rcu *real_parent ;
            //指向当前的父进程,通常和read_parent一致
            struct task_struct  __rcu *parent ;
	
            struct list_head children ;
            
            struct list_head  sibling;
			struct task_struct *group_leader;
            struct thread_struct thread ;
            // 当前目录
            struct fs_struct *fs ; 
            // 指向文件描述符,该进程打开的所有文件都会存在这个指针数组里。
            struct files_struct *files; 
            
            struct sginal_struct *signal ;

        }

系统每一个进程都有之对应的task_struct描述

thread_info

前置知识:
当进程由于中断或系统调用从用户态转到内核态时,进程所使用的栈也要从用户栈切换到内核栈,所以每个进程在内核中也有一个内核态的栈区,这个内存栈在进程陷入内核态时用来代替应用态的栈区进行使用。

为了节省空间,linux把内核栈和一个紧挨近pcb的的小数据结构thread_info放在一起,占用8kB的内存空间,thread_info结构体并不直接包含进程的相关字段。

thread_info就相当于进程在内核中的一个远房亲戚,内核中的pcb,各自都能通过一个指针指向对方。

struct thread_info {
	struct pcb_struct	pcb;		/* palcode state */
// 指向创建进程的task_struct结构体
	struct task_struct	*task;		/* main task structure */
	unsigned int		flags;		/* low level flags */
	unsigned int		ieee_state;	/* see fpu.h */

	mm_segment_t		addr_limit;	/* thread address space */
	unsigned		cpu;		/* current CPU */
	int			preempt_count; /* 0 => preemptable, <0 => BUG */
	unsigned int		status;		/* thread-synchronous flags */

	int bpt_nsaved;
	unsigned long bpt_addr[2];		/* breakpoint handling  */
	unsigned int bpt_insn[2];
};

这里写图片描述
可以看出来进程的内核栈是向下增长的,也就是栈底在高位地址,栈底在低位地址, thread_info 中的task指针和task_struct指针一一对应

thread_union

这两个结构体通过union thread_union联系在一起,下面是thread_union的源码:

union thread_union {
  #ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
          struct task_struct task;
     #endif
     #ifndef CONFIG_THREAD_INFO_IN_TASK
          struct thread_info thread_info;
     #endif
          unsigned long stack[THREAD_SIZE/sizeof(long)];
  };

组织方式

  • 内核把进程的列表存放在叫做任务队列task list 的双向循环链表中,链表每一项的类型都是task_struct。

在这里插入图片描述

  • 为了方便系统查找和使用,提高查找效率,内核专门使用了4个哈希表用于索引进程描述符,我们可以用pid,tgid,pgrp,session去找进程,这几个哈希表说明如下:
    在这里插入图片描述

创建进程的流程

linux提供系统调用fork()vfork()clone()函数创建进程,它们最终都会调用kernel_clone函数, 那么重点来了,我们只需要分析kernel_clone函数。

kernel_clone
/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 *
 * args->exit_signal is expected to be checked for sanity by the caller.
 */
pid_t kernel_clone(struct kernel_clone_args *args)
{
	u64 clone_flags = args->flags;
	struct completion vfork;
	struct pid *pid;
	struct task_struct *p;
	int trace = 0;
	pid_t nr;

	if ((args->flags & CLONE_PIDFD) &&
	    (args->flags & CLONE_PARENT_SETTID) &&
	    (args->pidfd == args->parent_tid))
		return -EINVAL;

	/*
	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */
	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);

	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	trace_sched_process_fork(current, p);

	pid = get_task_pid(p, PIDTYPE_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) {
		if (!wait_for_vfork_done(p, &vfork))
			ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
	}

	put_pid(pid);
	return nr;
}
copy_process()

从图中我们可以看出,kernel_clone调用了一个重要函数: copy_process函数:我们先来看看源码:


/* 部分源码
 * This creates a new process as a copy of the old one,
 * but does not actually start it yet.
 *
 * It copies the registers, and all the appropriate
 * parts of the process environment (as per the clone
 * flags). The actual kick-off is left to the caller.
 */
static __latent_entropy struct task_struct *copy_process(
					struct pid *pid,
					int trace,
					int node,
					struct kernel_clone_args *args)
{
	int pidfd = -1, retval;
	struct task_struct *p;
	struct multiprocess_signals delayed;
	struct file *pidfile = NULL;
	u64 clone_flags = args->flags;
	struct nsproxy *nsp = current->nsproxy;

	
	p = dup_task_struct(current, node);
	if (!p)
		goto fork_out;
	if (args->io_thread) {
		/*
		 * Mark us an IO worker, and block any signal that isn't
		 * fatal or STOP
		 */
		p->flags |= PF_IO_WORKER;
		siginitsetinv(&p->blocked, sigmask(SIGKILL)|sigmask(SIGSTOP));
	}

	/*
	 * This _must_ happen before we call free_task(), i.e. before we jump
	 * to any of the bad_fork_* labels. This is to avoid freeing
	 * p->set_child_tid which is (ab)used as a kthread's data pointer for
	 * kernel threads (PF_KTHREAD).
	 */
	p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? args->child_tid : NULL;
	/*
	 * Clear TID on mm_release()?
	 */
	p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? args->child_tid : NULL;

	ftrace_graph_init_task(p);

	rt_mutex_init_task(p);

	lockdep_assert_irqs_enabled();
	retval = -EAGAIN;
	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;

	/*
	 * If multiple threads are within copy_process(), then this check
	 * triggers too late. This doesn't hurt, the check is only there
	 * to stop root fork bombs.
	 */
	retval = -EAGAIN;
	if (data_race(nr_threads >= max_threads))
		goto bad_fork_cleanup_count;

	delayacct_tsk_init(p);	/* Must remain after dup_task_struct() */
	p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER | PF_IDLE | PF_NO_SETAFFINITY);
	p->flags |= PF_FORKNOEXEC;
	INIT_LIST_HEAD(&p->children);
	INIT_LIST_HEAD(&p->sibling);
	rcu_copy_process(p);
	p->vfork_done = NULL;
	spin_lock_init(&p->alloc_lock);

	init_sigpending(&p->pending);

	p->utime = p->stime = p->gtime = 0;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIME
	p->utimescaled = p->stimescaled = 0;
#endif
	prev_cputime_init(&p->prev_cputime);

#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
	seqcount_init(&p->vtime.seqcount);
	p->vtime.starttime = 0;
	p->vtime.state = VTIME_INACTIVE;
#endif

#ifdef CONFIG_IO_URING
	p->io_uring = NULL;
#endif

#if defined(SPLIT_RSS_COUNTING)
	memset(&p->rss_stat, 0, sizeof(p->rss_stat));
#endif

	p->default_timer_slack_ns = current->timer_slack_ns;

#ifdef CONFIG_PSI
	p->psi_flags = 0;
#endif

	task_io_accounting_init(&p->ioac);
	acct_clear_integrals(p);

	posix_cputimers_init(&p->posix_cputimers);

	p->io_context = NULL;
	audit_set_context(p, NULL);
	cgroup_fork(p);
#ifdef CONFIG_NUMA
	p->mempolicy = mpol_dup(p->mempolicy);
	if (IS_ERR(p->mempolicy)) {
		retval = PTR_ERR(p->mempolicy);
		p->mempolicy = NULL;
		goto bad_fork_cleanup_threadgroup_lock;
	}
#endif
#ifdef CONFIG_CPUSETS
	p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
	p->cpuset_slab_spread_rotor = NUMA_NO_NODE;
	seqcount_spinlock_init(&p->mems_allowed_seq, &p->alloc_lock);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
	memset(&p->irqtrace, 0, sizeof(p->irqtrace));
	p->irqtrace.hardirq_disable_ip	= _THIS_IP_;
	p->irqtrace.softirq_enable_ip	= _THIS_IP_;
	p->softirqs_enabled		= 1;
	p->softirq_context		= 0;
#endif

	p->pagefault_disabled = 0;



	/* Perform scheduler related setup. Assign this task to a CPU. */
	retval = sched_fork(clone_flags, p);
		

	retval = perf_event_init_task(p, clone_flags);
	

	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;
		}
	}



	/* ok, now we should be set up.. */
	p->pid = pid_nr(pid);
	if (clone_flags & CLONE_THREAD) {
		p->group_leader = current->group_leader;
		p->tgid = current->tgid;
	} else {
		p->group_leader = p;
		p->tgid = p->pid;
	}

	p->nr_dirtied = 0;
	p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
	p->dirty_paused_when = 0;

	p->pdeath_signal = 0;
	INIT_LIST_HEAD(&p->thread_group);
	p->task_works = NULL;

#ifdef CONFIG_KRETPROBES
	p->kretprobe_instances.first = NULL;
#endif

	

	p->start_time = ktime_get_ns();
	p->start_boottime = ktime_get_boottime_ns();

	/*
	 * Make it visible to the rest of the system, but dont wake it up yet.
	 * Need tasklist lock for parent etc handling!
	 */
	write_lock_irq(&tasklist_lock);

	klp_copy_process(p);

	spin_lock(&current->sighand->siglock);

	/*
	 * Copy seccomp details explicitly here, in case they were changed
	 * before holding sighand lock.
	 */
	copy_seccomp(p);


copy_process()函数完成的工作很有意思:

  1. 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
  2. 检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。
  3. 子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。进程描述符的成员值并不是继承而来的,主要是统计信息。进程描述符中的大多数数据都是共享的。
  4. 接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
  5. copy_process()调用copy_flag()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
  6. 调用alloc_pid()为新进程获取一个有效的PID。
  7. 根据传递给clone()的参数标志,copy_process拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,
    这些资源对每个进程是不同的,因此被拷贝到这里。
  8. 最后,copy_process()作扫尾工作并返回一个指向子进程的指针。 再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

内核有意选择子进程执行。因此一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

最后我们用一张图描述其工作:
在这里插入图片描述

fork,vfork,clone的区别

我们已经知道fork(),vfork(),clone() 三个函数的对应的系统调用是 sys_fork()、sys_clone()、sys_vfork(),并且最终都调用了kernel_clone函数
而它们的差别是参数的传递和一些基本准备的工作不同。

系统调用详细描述
forkfork创造的子进程是父进程的完整副本,复制了父进程的资源: task_struct,打开文件表,信号,命名空间虚拟地址空间(包括堆栈等)等。(写时复制)
vforkvfork创建的子进程与父进程共享虚拟地址空间,所以子进程的改变会影响父进程中的数据。vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。
clonevfork创建的子进程与父进程共享虚拟地址空间,所以子进程的改变会影响父进程中的数据。vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。

表格来源:https://blog.csdn.net/liushengxi_root/article/details/81332740

fork

fork调用的clone函数为:clone(SIGCHLD,0)
fork采用了写时拷贝(COW)技术

vfork

vfork的实现是:clone(CLONE_VFORK|CLONNE_VM|SIGCHLD,0)

clone

clone是linux创建线程设计的,

current

内核设计了一个current变量,是一个指针,用来指向当前的进程。
内核路径为: include\asm-generic\current.h

#ifndef __ASM_GENERIC_CURRENT_H
#define __ASM_GENERIC_CURRENT_H
 
#include <linux/thread_info.h>
 
#define get_current() (current_thread_info()->task) //获取当前进程的线程task
#define current get_current() //表示当前进程
 
#endif /* __ASM_GENERIC_CURRENT_H */

可以看到current实际上是调用了get_current()函数

static inline struct thread_info *current_thread_info(void) __attribute_const__;
 
static inline struct thread_info *current_thread_info(void)
{
	register unsigned long sp asm ("sp");
	return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值