Linux内核把进程称为任务(task)。在介绍进程之前,介绍一下linux源码的结构。
注意带“/”的都是目录,其他的是文件。
进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户空间虚拟地址空间。
进程有两种特殊形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。通用在不会引起混淆的情况下把用户线程简称为线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
Linux进程有四要素:
如果只具备前三条而缺少第四条,则称为“线程”。如果完全没有用户空间,就称为“内核线程”;而如果共享用户空间映射就称为“用户线程”。
内核为每个进程分配一个task_struct结构时。实际分配两个连续物理页面(8192字节),数据结构task_struct的大小约占1kb字节左右,进程的系统空间堆栈的大小约为7kb字节(不能扩展,是静态确定的)。
每个进程/线程都有一个对应的task_struct结构体。下面只介绍较为重要的结构。
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
#endif
/* -1 unrunnable, 0 runnable, >0 stopped: */
volatile long state; //进程状态
void *stack; //指向内核栈
//调度策略和优先级
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class; //调度类,注意是调度类包含进程
cpumask_t cpus_allowed; // 允许进程在哪些处理器运行
//指向内存描述符
//mm和active_mm指向同一内存。对于内核线程而言,mm是空指针;当内核线程运行时,active_mm指向从进程借用的内存描述符
struct mm_struct *mm;
struct mm_struct *active_mm;
pid_t pid; //全局进程号
pid_t tgid; //全局的线程组标识符
/*
* Pointers to the (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
/* Real parent process: */
struct task_struct __rcu *real_parent; //指向真实的父进程
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent; //指向父进程。如果进程被另一个进程使用ptrace()跟踪,那么父进程跟踪进程,否则和real_parent相同
struct task_struct *group_leader; //指向线程组的组长
struct pid_link pids[PIDTYPE_MAX]; //进程号、进程组标识符和会话标识符
char comm[TASK_COMM_LEN]; //进程名称
/* Filesystem information: */
struct fs_struct *fs; //文件系统信息,主要是进程的根目录和当前工作目录
/* Open file information: */
struct files_struct *files; //打开文件表
/* Namespaces: */
struct nsproxy *nsproxy; //命名空间
...
}
从上面的代码可以看出,task_struct结构体非常大。为了能用更少的寄存器就读取到进程描述符(task_struc),因此在内核栈底部增加了thread_info,并且把task_struct的指针存放在thread_info的第一个偏移位置,这样内核只需要通过内核栈就能访问到task_struct了。
对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:一个是内核态的进程堆栈stack,另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。这两个结构被紧凑的放在一个联合体中thread_union中。
union thread_union
{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
thread_info和内核栈虽然共用了thread_union结构, 但是thread_info大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info的存储空间时, 则会发生栈溢出。
具体thread_info结构
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
可以看出,thread_info与task_struct结构体可以相互索引。
在Linux内核中,新进程是从一个已经存在的进程复制出来的,内核使用静态数据结构造出0号内核线程,0号内核线程分叉生成1号内核线程和2号内核线程(kthreadd线程)。1号内核线程完成初始化以后装载用户程序,变成1号进程,其他进程都是1号进程或者它的子孙进程分叉生成的;其他内核线程是kthreadd线程分叉生成的。
目前,linux系统中有三个函数可以用来创建新的进程:
接下来,这三个函数会分别调用各自的系统调用函数,名称为"sys_"再加上各自的函数名,创建新进程的3个系统调用在文件kernel/fork.c
中,最后,它们都会调用_do_fork()。
在5.0版本后,linux不再使用该函数,而是改为了kernel_clone(),基本流程没有改变,明白了_do_fork,理解kernel_clone也就不难了。
贴出源码,主要是介绍流程。
long _do_fork(unsigned long clone_flags, //克隆标志
unsigned long stack_start, //只有创建线程时有用,用来指定线程的用户栈起始地址
unsigned long stack_size, //只有创建线程时有用,用来指定线程的用户栈长度
int __user *parent_tidptr, //只有创建线程时有用,如果clone_flag指定CLONE_PARENT_SETID,该参数存放新线程保存自己的进程标识符的位置
int __user *child_tidptr, //只有创建线程时有用,如果clone_flag指定CLONE_CHILD_SETID,该参数存放新线程保存自己的进程标识符的位置
unsigned long tls) //只有创建线程时有用,如果clone_flag指定标志位CLONE_SETTLS,那么参数tls指定新线程的线程本地存储的地址
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* 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 ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
// _do_fork()核心,创建新线程
p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
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, parent_tidptr);
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);
} else {
nr = PTR_ERR(p);
}
return nr;
}
如果有兴趣,可以对比一下,kernel_clone()的代码与此大体相似。
这里有一张_do_clone()的流程图。
这里提示一下,同一个线程组的所有线程必须属于相同的用户命名空间和进程号命名空间。
static __latent_entropy struct task_struct *copy_process(
unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace,
unsigned long tls,
int node)
{
int retval;
struct task_struct *p;
//同时设置CLONE_NEWNS、CLONE_FS,即新进程属于新的挂载命名空间,同时和当前进程共享文件系统信息
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
//新进程属于新的命名空间,同时和当前进程共享文件信息
if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
return ERR_PTR(-EINVAL);
/*
* Thread groups must share signals as well, and detached threads
* can only be started up within the thread group.
*/
//新进程和当前进程属于同一个线程组,但是不共享信号处理程序
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* Shared signal handlers imply shared VM. By way of the above,
* thread groups also imply shared VM. Blocking this case allows
* for various simplifications in other code.
*/
//新进程和当前进程共享信号处理程序,但是它不共享虚拟内存
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
...
p = dup_task_struct(current, node);
if (!p)
goto fork_out;
...
// 检查用户的进程数量限制
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 (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
...
// 为新进程设置调度器相关参数
retval = sched_fork(clone_flags, p);
/* copy all the process information */
// 复制或共享资源
shm_init_task(p);
retval = security_task_alloc(p, clone_flags);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_semundo(clone_flags, p); //只有属于同一个线程组的线程之间才会共享unix系统打开文件表,只有属于同一个线程组的线程之间才会共享打开文件表
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); //创建或共享io上下文
if (retval)
goto bad_fork_cleanup_namespaces;
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls); //复制寄存器值。不同处理器架构的寄存器不同,所以各处理器架构需要自己定义结构体pt_regs和thread_struct来实现函数copy_thread_tls()
if (retval)
goto bad_fork_cleanup_io;
...
}
同样贴出流程图。
在创建子进程时,linux采用的是写时复制技术。
写时复制核心思想:只有在不得不复制数据内容时才去复制数据内容。
具体解释一下,fork创建出的子进程,与父进程共享内存空间。也就是说,如果父进程或子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了(不用复制,直接引用父进程的物理空间)。事实上,虚拟空间两者不一样,但是子进程完全复制父进程的虚拟空间;而在物理空间上,两者是指向相同的地址的。
这里是刚开始对内核有了个入门的了解,如果看了没有感觉不要担心,后期还会继续介绍。