相关学习资料
linux内核设计与实现+原书第3版.pdf(.3章)
深入linux内核架构(中文版).pdf
深入理解linux内核中文第三版.pdf
《独辟蹊径品内核Linux内核源代码导读》
http://www.yanyulin.info/pages/2013/11/linux0.html
http://blog.csdn.net/ddna/article/details/4958058
http://www.cnblogs.com/coolgestar02/archive/2010/12/31/1922629.html
http://blog.sina.com.cn/s/blog_4ba5b45e0102e3to.html
http://www.kernel.org/
目录
. Linux/Unix进程创建相关基本知识
. Linux进程管理
. sys_fork()
. sys_execve()函数
. Copy On Write COW(写时复制)技术
. Linux Glibc提供的创建进程的7种API方式
. Glibc execve、fork API源代码分析
. 查看进程的启动过程工具
. Linux下线程创建
. Posix线程
1. Linux/Unix进程创建相关基本知识
0x1: linux和windows在进程创建上的区别
unix/linux的进程创建和Windows有很大不一样,windows对线程和进程的实现非常标准,windows内核有明确的线程和进程的概念。在windows API中,可以使用明确的API: CreateProcess和CreateThread来创建进程和线程,并且有一系列的API来操纵它们,但对于Linux来说,线程并不是一个强制性明确的概念
在Linux内核中并不存在真正意义上的线程概念,Linux将所有的执行实体(进程或线程)都称为"任务(task)",每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。但是Linux下不同的任务之间可以选择共享内存空间,因此在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也可以称之为这个进程中的线程
. windows
windows采用了createProcess()来进行新进程的创建,大致流程如下:
) 申请一块全新的内存(包括内核空间和用户空间)
) 打开新进程对应的磁盘文件,将文件内容复制到新申请的内存中
) 启动主线程从新进程的函数入口点(默认是main)开始顺序执行
在windows的哲学中,每一个新进程都是一个新的、独立的内存空间,进程之间彼此相对独立。
虽然在内核对象中也有父进程和子进程这些字段,但是这只是一个弱关系,windows中的父子进程并没有强制性的依赖关系。
关于windows的进程创建过程,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3458736.html . linux/unix
对于linu/unix的操作系统来说,它并不像windows那样采用"产生(spawn)"进程的机制。
而是将创建进程的步骤分解到两个单独的函数中去执行:
) fork()
fork()通过"拷贝"当前进程,创建一个子进程。这个时候的子进程和父进程的区别仅仅在于PID(进程号)、PPID(父进程号)、和某些资源和统计量
) exec()
exec()函数则负责读取可执行文件并将其载入地址空间开始运行
把这两个函数(fork、exec)组合起来的最终效果就等同于windows中的createProcess
需要明白的是,fork和exec并不是强制一定要按顺序执行的,实际上,可以单独只执行fork、或者单独执行exec、或者执行fork+exec。在调用fork和exec之间插入额外的代码执行也是可行的,fork和exec在原理上两个独立的概念
0x2: linux中的0号、1号进程
. 进程0
Linux引导中创建的第一个进程,完成加载系统后,演变为进程调度、交换及存储管理进程(也就是说0号进程自从创建完1号进程后就不会再次去创建其他进程了,之后由1号进程负责新子进程的创建)
Linux中1号进程是由0号进程来创建的,由于在创建进程时,程序一直运行在内核态,而进程运行在用户态,因此创建0号进程涉及到特权级的变化,即从特权级0变到特权级3,Linux是通过模拟中断返回来实现特权级的变化以及创建0号
进程,通过将0号进程的代码段选择子以及程序计数器EIP直接压入内核态堆栈,然后利用iret汇编指令中断返回跳转到0号进程运行。 . 进程1
init 进程,由0进程创建,完成系统的初始化。是系统中所有其它用户进程的祖先进程。
2. Linux进程管理
0x1: 进程概念
进程就是处于执行期的程序(目标码存放在某种存储介质上),从广义上讲,它包括
. 一般的可执行代码(即代码段)
. 打开的文件
. 挂起的信号
. 内核内部数据结构
. 处理器状态
. 一个或多个具有内存映射的内存地址空间
. 一个或多个执行线程(thread of execution)
. 存放全局变量的数据段
//进程就是正在执行的程序代码的"实时结果",内核需要有效而又透明地管理所有细节
0x2: 创建进程 && 创建新进程
在学习Linux进程创建相关知识的时候,我们需要对Linux下"进程创建"和"新进程创建"这两个概念进行区分,完整地说,Linux下进程创建有如下几个场景
. 从当前进程复制一份和父进程完全一样的新进程: 准确地说是复制了一份父进程作为新进程
从系统调用的角度来说,和进程创建相关的系统调用只有fork(),进程在调用fork()创建它的时刻开始存活,fork()通过"复制"(Linux下所有进程都是"复制"出来的)一个现有进程来创建一个新的进程,调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时,在返回到这个相同位置上,父进程恢复执行,子进程开始执行。
fork()系统调用从内核返回两次,一次返回到父进程、另一次返回到新产生的子进程
) 调用fork()
or
) 调用clone()
/*
就像一个细胞复制了一份和自己相同的新细胞,两个细胞同时运行
*/ . 运行新代码的新进程创建: 在调用fork的基础上,继续调用exec(),读取并载入新进程代码并继续运行
通常,创建新的进程都是为了立即执行新的、不同的代码,而接着调用exec这组函数就可以创建新的"地址空间",并把新的程序载入其中。在现代Linux内核中,fork()实际上是由clone()系统调用实现的
) fork()/clone() + exec()
/*
就像一个细胞复制了一份和自己相同的新细胞,并填充进了新的细胞核,两个细胞同时运行
*/ . 运行新进程: 直接将当前进程转变为一个包含不同代码的新进程
) exec()
/*
就像一个细胞使用新的蛋白质将自己的细胞核改变了,并继续运行
*/
0x3: 进程描述符及任务(task)结构
内核把进程的的列表存放在"任务队列(task list)"(这是一个双向循环链表)中,链表中的每一项都是类型为task_struct称为进程描述符(process descriptor)的结构,该结构中包含了具体进程的所有相关信息,例如
. 打开的文件
. 进程的地址空间
. 挂起的信号
. 进程的状态
..
关于task_struct数据结构的相关知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:0x1: struct task_struct
0x4: Linux进程创建方法
从程序员的角度来说,Linux下实现进程创建可以通过以下方法
. 通过系统提供的系统调用
) fork()/clone(): 复制一份新进程
) exec(): 运行新进程
) fork()/clone() + exec(): 复制并运行一个新进程(父进程和子进程运行不同的代码)
. 通过glibc提供的API函数: exec系列函
) exec系列函数: glibc实现对系统调用exec()的一层包装
) fork api
我们接下来先了解内核态的fork、execve系统调用开始,然后再学习用户态Glibc提供的进程创建相关API
3. sys_fork()
使用fork创建的进程被称为原父进程(parents process)的子进程(child process)。从用户的角度来看,子进程是父进程的一个精确副本,两个进程只是PID不同,fork系统调用从内核态返回2次,PID分别为
. 子进程: PID =
. 父进程: PID = 子进程的PID
//程序可以通过检测fork的返回值来判断当前进程是父进程还是子进程
从整体上来看,一次fork调用包括了以下几步
. 为子进程分配和初始化一个新的task_struct结构
) 从父进程中复制: 包括所有从父进程继承而来的特权和限制
1.1) 进程组和会话信息
1.2) 信号状态(忽略、捕获、阻塞信号的掩码)
1.3) kg_nice调度参数
1.4) 对父进程凭据的引用
1.5) 对父进程打开文件的引用(即文件句柄表。以及相关引用数据结构,使用这些数据结构可以操作对应的文件)
1.6) 对父进程限制(resources limitation)的引用
) 清零
2.1) 最近CPU利用率
2.2) 等待通道
2.3) 交换和睡眠时间
2.4) 定时器
2.5) 跟踪机制
2.6) 挂起信号的信息
) 显式地进行初始化
3.1) 包括所有进程的链表的入口
3.2) 父进程的子进程链表的入口以及指向其父进程的返回指针
3.3) 父进程的进程组链表的入口
3.4) 散列结构的入口,该结构使得进程可以通过其PID进行查找
3.5) 指向进程统计结构的指针,该结构位于用户结构中
3.6) 指向进程信号处理结构的指针,该结构位于用户结构中
3.7) 该进程的新PID
. 复制父进程的地址空间
在复制一个进程的映像时,内核通过vm_forkproc()来调用内存管理机制。vm_forkproc()例程的参数是一个指向一个已经初始化过的子进程的task_struct的指针,它的任务是为该子进程分配其执行所需的全部资源。vm_forkproc()调用在子进程中通过另一条直接进入用户态的执行线路返回,而在父进程中沿着正常的执行线路返回(即一次调用、2次返回)
将父进程的上下文复制给子进程,包括
) 线程结构
) 父进程的register寄存器状态,fork系统调用结束后,父进程进程从同一个代码位置开始继续执行
) 虚拟内存资源,只是复制了一份引用,copy on write机制
. 调度子进程运行(execve)
子进程最终创建完毕之后,就被放入运行队列,这样调度程序就知道这个新进程了
Fork的系统调用代码在\linux-2.6.32.63\arch\x86\kernel\process.c中
/*
Sys_fork系统调用通过 do_fork()函数实现,通过对do_fork()函数传递不同的clone_flags来实现:
1. fork
2. clone
3. vfork
*/
int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, , NULL, NULL);
}
我们继续跟踪do_fork()的代码
\linux-2.6.32.63\kernel\fork.c
/*
1. clone_flags: 指定了子进程结束时,需要向父进程发送的信号,通常这个信号是SIGCHLD,同时clone_flags还指定子进程需要共享父进程的哪些资源
2. stack_start: 子进程用户态堆栈起始地址。通常设置为0,父进程会复制自己的堆栈指针,当子进程对堆栈进行写入时,缺页中断处理程序会设置新的物理页面(即copy on write 写时复制)
3. regs: pt_regs结构,保存了进入内核态时的存储器的值,父进程会将寄存器状态完整的复制给子进程
4. stack_size: 默认为0
5. parent_tidptr: 用户态内存指针,当CLONE_PARENT_SETTID被设置时,内核会把新建立的子进程ID通过parent_tidptr返回
6. child_tidptr: 用户态内存指针,当CLONE_CHILD_SETTID被设置时,内核会把新建立的子进程ID通过child_tidptr返回
*/
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr)
{
struct task_struct *p;
int trace = ;
long nr; /*
* Do some preliminary argument and permissions checking before we actually start allocating stuff
*/
if (clone_flags & CLONE_NEWUSER)
{
if (clone_flags & CLONE_THREAD)
return -EINVAL;
/* hopefully this check will go away when userns support is
* complete
*/
if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) || !capable(CAP_SETGID))
return -EPERM;
} /*
We hope to recycle these flags after 2.6.26
采用向下兼容的模式,2.6.26之后,将CLONE_STOPPED废除
*/
if (unlikely(clone_flags & CLONE_STOPPED))
{
static int __read_mostly count = ; if (count > && printk_ratelimit())
{
char comm[TASK_COMM_LEN]; count--;
printk(KERN_INFO "fork(): process `%s' used deprecated clone flags 0x%lx\n", get_task_comm(comm, current), clone_flags & CLONE_STOPPED);
}
} /*
When called from kernel_thread, don't do user tracing stuff.
*/
if (likely(user_mode(regs)))
trace = tracehook_prepare_clone(clone_flags); /*
Do_fork()函数的核心是copy_process()函数,该函数完成了进程创建的绝大部分工作
分配子进程的task_struct结构,并复制父进程的资源
*/
p = copy_process(clone_flags, stack_start, regs, stack_size, child_tidptr, NULL, trace);
/*
* 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; trace_sched_process_fork(current, p);
/*
/source/include/linux/sched.h
/source/kernel/pid.c
设置pid namespace,不同的namespace中,可以建立相同的pid的进程
*/
nr = task_pid_vnr(p); if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr); /*
CLONE_VFORK要求父进程进入子进程,现在初始化一个等待对象
*/
if (clone_flags & CLONE_VFORK)
{
p->vfork_done = &vfork;
init_completion(&vfork);
} audit_finish_fork(p);
tracehook_report_clone(regs, clone_flags, nr, p); /*
We set PF_STARTING at creation in case tracing wants to use this to distinguish a fully live task from one that hasn't gotten to tracehook_report_clone() yet.
Now we clear it and set the child going.
*/
p->flags &= ~PF_STARTING; /*
如果被设置了CLONE_STOPPED标志,则向进程发送SIGSTOP信号
*/
if (unlikely(clone_flags & CLONE_STOPPED))
{
/*
We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
__set_task_state(p, TASK_STOPPED);
}
else
{
//如果没有设置CLONE_STOPPED标志,就把进程加入就绪队列
wake_up_new_task(p, clone_flags);
} tracehook_report_clone_complete(trace, regs, clone_flags, nr, p); if (clone_flags & CLONE_VFORK)
{
freezer_do_not_count();
//当前进程进入之前初始化好等待队列
wait_for_completion(&vfork);
freezer_count();
tracehook_report_vfork_done(p, nr);
}
} else {
nr = PTR_ERR(p);
}
return nr;
}
Do_fork()函数的核心是copy_process()函数,该函数完成了进程创建的绝大部分工作
继续跟踪copy_process()
\linux-2.6.32.63\kernel\fork.c
/*
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 struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;
int cgroup_callbacks_done = ; /*
1. 对传入的clone_flag进行检查
*/
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|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); if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL); /*
LSM安全框架检查,利用它可以在进程建立之前检查是否允许检查,利用这个内核框架,可以开发出进程监控功能。默认调用dummy_task_create函数,这是一个空函数
*/
retval = security_task_create(clone_flags);
if (retval)
goto fork_out; retval = -ENOMEM;
/*
2. 调用了dup_task_struct()函数,该函数的主要作用是
1) 为子进程创建一个新的内核栈
2) 复制父进程的task_struct结构和thread_info结构,这里只是对结构完整的复制,所以子进程的进程描述符跟父进程完全一样
*/
p = dup_task_struct(current);
if (!p)
goto fork_out; ftrace_graph_init_task(p); rt_mutex_init_task(p); #ifdef CONFIG_PROVE_LOCKING
DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
retval = -EAGAIN;
/*
检查进程的资源限制
*/
if (atomic_read(&p->real_cred->user->processes) >= p->signal->rlim[RLIMIT_NPROC].rlim_cur)
{
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->real_cred->user != INIT_USER)
goto bad_fork_free;
} /*
复制父进程的cred信号,这个结构保存的是进程的身份权限信息(例如UID)
*/
retval = copy_creds(p, clone_flags);
if (retval < )
goto bad_fork_free; retval = -EAGAIN;
/*
3. 检查创建的进程是否超过了系统进程总量
*/
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count; if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count; p->did_exec = ;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
//复制clone_flags到子进程的task_struct结构中
copy_flags(clone_flags, p);
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); /*
4. 开始对子进程task_struct结构的初始化过程
*/
p->utime = cputime_zero;
p->stime = cputime_zero;
p->gtime = cputime_zero;
p->utimescaled = cputime_zero;
p->stimescaled = cputime_zero;
p->prev_utime = cputime_zero;
p->prev_stime = cputime_zero; p->default_timer_slack_ns = current->timer_slack_ns; task_io_accounting_init(&p->ioac);
acct_clear_integrals(p); posix_cpu_timers_init(p); p->lock_depth = -; /* -1 = no lock */
do_posix_clock_monotonic_gettime(&p->start_time);
p->real_start_time = p->start_time;
monotonic_to_bootbased(&p->real_start_time);
p->io_context = NULL;
p->audit_context = 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_cgroup;
}
mpol_fix_fork_child_flag(p);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
p->irq_events = ;
#ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
p->hardirqs_enabled = ;
#else
p->hardirqs_enabled = ;
#endif
p->hardirq_enable_ip = ;
p->hardirq_enable_event = ;
p->hardirq_disable_ip = _THIS_IP_;
p->hardirq_disable_event = ;
p->softirqs_enabled = ;
p->softirq_enable_ip = _THIS_IP_;
p->softirq_enable_event = ;
p->softirq_disable_ip = ;
p->softirq_disable_event = ;
p->hardirq_context = ;
p->softirq_context = ;
#endif
#ifdef CONFIG_LOCKDEP
p->lockdep_depth = ; /* no locks held yet */
p->curr_chain_key = ;
p->lockdep_recursion = ;
#endif #ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif p->bts = NULL; /* Perform scheduler related setup. Assign this task to a CPU. */
sched_fork(p, clone_flags); retval = perf_event_init_task(p);
if (retval)
goto bad_fork_cleanup_policy; if ((retval = audit_alloc(p)))
goto bad_fork_cleanup_policy;
/*
copy all the process information
根据clone_flags复制父进程的资源到子进程,对于clone_flags指定共享的资源,父子进程间共享这些资源,仅仅设置子进程的相关指针,并增加资源数据结构的引用计数
*/
if ((retval = copy_semundo(clone_flags, p)))
goto bad_fork_cleanup_audit;
if ((retval = copy_files(clone_flags, p)))
goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p)))
goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p)))
goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p)))
goto bad_fork_cleanup_sighand;
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
if ((retval = copy_namespaces(clone_flags, p)))
goto bad_fork_cleanup_mm;
if ((retval = copy_io(clone_flags, p)))
goto bad_fork_cleanup_namespaces;
//复制父进程的内核态堆栈到子进程
retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_io; if (pid != &init_struct_pid)
{
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns);
if (!pid)
goto bad_fork_cleanup_io; if (clone_flags & CLONE_NEWPID)
{
retval = pid_ns_prepare_proc(p->nsproxy->pid_ns);
if (retval < )
goto bad_fork_free_pid;
}
} p->pid = pid_nr(pid);
/*
5. 如果设置了同在一个线程组则继承TGID。对于普通进程来说TGID和PID相等,对于线程来说,同一线程组内的所有线程的TGID都相等,这使得这些多线程可以通过调用getpid()获得相同的PID
如果建立的是轻权进程,那么父子进程在同一个线程组中,就设置子进程的tgid
*/
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid; //创建新的namespace
if (current->nsproxy != p->nsproxy)
{
retval = ns_cgroup_clone(p, pid);
if (retval)
goto bad_fork_free_pid;
} p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
/*
* Clear TID on mm_release()?
*/
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
#ifdef CONFIG_FUTEX
p->robust_list = NULL;
#ifdef CONFIG_COMPAT
p->compat_robust_list = NULL;
#endif
INIT_LIST_HEAD(&p->pi_state_list);
p->pi_state_cache = NULL;
#endif if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
p->sas_ss_sp = p->sas_ss_size = ; clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
clear_all_latency_tracing(p); /*
父进程是否要求子进程退出时发送信号
ok, now we should be set up..
*/
p->exit_signal = (clone_flags & CLONE_THREAD) ? - : (clone_flags & CSIGNAL);
p->pdeath_signal = ;
//子进程默认的退出状态
p->exit_state = ; p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group); cgroup_fork_callbacks(p);
cgroup_callbacks_done = ; /* Need tasklist lock for parent etc handling! */
write_lock_irq(&tasklist_lock); /*
CLONE_PARENT re-uses the old parent
*/
if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
{
//把子进程的real_parent设置为父进程的real_parent
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
}
else
{
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
} spin_lock(¤t->sighand->siglock); recalc_sigpending();
if (signal_pending(current)) {
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -ERESTARTNOINTR;
goto bad_fork_free_pid;
} if (clone_flags & CLONE_THREAD) {
atomic_inc(¤t->signal->count);
atomic_inc(¤t->signal->live);
p->group_leader = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
} if (likely(p->pid))
{
//把子进程添加到父进程的子进程链表中,这样组成了兄弟进程链表
list_add_tail(&p->sibling, &p->real_parent->children);
tracehook_finish_clone(p, clone_flags, trace); if (thread_group_leader(p))
{
if (clone_flags & CLONE_NEWPID)
p->nsproxy->pid_ns->child_reaper = p; p->signal->leader_pid = pid;
tty_kref_put(p->signal->tty);
p->signal->tty = tty_kref_get(current->signal->tty);
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
attach_pid(p, PIDTYPE_SID, task_session(current));
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++;
}
attach_pid(p, PIDTYPE_PID, pid);
nr_threads++;
} total_forks++;
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
proc_fork_connector(p);
cgroup_post_fork(p);
perf_event_fork(p);
return p;
/*
出错退出
*/
bad_fork_free_pid:
if (pid != &init_struct_pid)
free_pid(pid);
bad_fork_cleanup_io:
if (p->io_context)
exit_io_context(p);
bad_fork_cleanup_namespaces:
exit_task_namespaces(p);
bad_fork_cleanup_mm:
if (p->mm)
mmput(p->mm);
bad_fork_cleanup_signal:
if (!(clone_flags & CLONE_THREAD))
__cleanup_signal(p->signal);
bad_fork_cleanup_sighand:
__cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
exit_fs(p); /* blocking */
bad_fork_cleanup_files:
exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
exit_sem(p);
bad_fork_cleanup_audit:
audit_free(p);
bad_fork_cleanup_policy:
perf_event_free_task(p);
#ifdef CONFIG_NUMA
mpol_put(p->mempolicy);
bad_fork_cleanup_cgroup:
#endif
cgroup_exit(p, cgroup_callbacks_done);
delayacct_tsk_free(p);
module_put(task_thread_info(p)->exec_domain->module);
bad_fork_cleanup_count:
atomic_dec(&p->cred->user->processes);
exit_creds(p);
bad_fork_free:
free_task(p);
fork_out:
return ERR_PTR(retval);
}
继续跟踪dup_task_struct()
\linux-2.6.32.63\kernel\fork.c
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
unsigned long *stackend; int err; prepare_to_copy(orig); /*
1. 通过alloc_task_struct()函数创建内核栈和task_struct结构空间
*/
tsk = alloc_task_struct();
if (!tsk)
return NULL;
/*
2. 分配thread_info结构空间
*/
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
/*
3. 为整个task_struct结构复制
*/
err = arch_dup_task_struct(tsk, orig);
if (err)
goto out; tsk->stack = ti; err = prop_local_init_single(&tsk->dirties);
if (err)
goto out;
/*
4. 调用setup_thread_stack()函数为thread_info结构复制
*/
setup_thread_stack(tsk, orig);
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC; /* for overflow detection */ #ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = get_random_int();
#endif /*
更新该用户的user_struct结构,累加相应的计数器,由atomic_inc()函数完成
*/
atomic_set(&tsk->usage,);
atomic_set(&tsk->fs_excl, );
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = ;
#endif
tsk->splice_pipe = NULL; account_kernel_stack(ti, ); return tsk; out:
free_thread_info(ti);
free_task_struct(tsk);
return NULL;
}
copy_process()完成的工作主要是进行必要的检查、初始化、复制必要的数据结构。这里我们重点分析两个函数
. copy_mm(): 涉及到父子进程的copy on write,以及共享内核虚拟地址的实现
. copy_thread(): 涉及到父子进程返回的实现(一次调用、2次返回)
//复制父进程的内核态堆栈到子进程
retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);
应用程序通过fork()系统调用进入内核空间,其内核态堆栈上保存着该进程的"进程上下文(寄存器状态)",通过copy_thread将复制父进程的内核态堆栈上的"进程上下文"到子进程中,同时把子进程堆栈上的EAX设置为0。由于父子进程的代码和数据是共享的,所以在返回后将接着执行,所以会发现以下现象
. 父子进程从同一个代码位置开始继续执行: 因为它们的"进程上下文"相同
. 父进程调用fork()返回子进程的PID: 父进程是正常调用
. 子进程返回0,因为内核态的EAX被设置为了0
. 父子进程不一定同时开始执行,但会有从内核态返回2次,一次是父进程,一次是子进程
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
内核调用copy_mm()来建立子进程的内存区域
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval; tsk->min_flt = tsk->maj_flt = ;
tsk->nvcsw = tsk->nivcsw = ;
#ifdef CONFIG_DETECT_HUNG_TASK
tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif tsk->mm = NULL;
tsk->active_mm = NULL; /*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return ; /*
如果要共享mm,则增加父进程mm的引用计数,同时把子进程的mm设置为current->mm
*/
if (clone_flags & CLONE_VM)
{
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
} retval = -ENOMEM;
/*
复制mm_struct的工作由dup_mm()来完成,这个函数会复制父进程的页表到子进程,这样父子进程就共享同样的物理页面,同时也共享了整个内核空间。
但是对于可写的用户空间对应的页表,dup_mm()会把它们设置为"只读",这样当进程(父进程或子进程)对它进行写入时,do_page_fault()函数将分配新的物理页面,为进程复制一份私有数据,这就是copy on write的机制
当父子进程的任何一个返回用户态首次对堆栈进行写入操作时,父子进程就会有各自独立的用户态堆栈了,但是对于代码段,它们却始终共享同一份物理页面,除非子进程调用exec()系列
*/
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem; good_mm:
/* Initializing for Swap token stuff */
mm->token_priority = ;
mm->last_interval = ; tsk->mm = mm;
tsk->active_mm = mm;
return ; fail_nomem:
return retval;
}
继续跟进dup_mm
/*
Allocate a new mm structure and copy contents from the mm structure of the passed in task structure.
*/
struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
int err; if (!oldmm)
return NULL;
//分配mm_struct结构
mm = allocate_mm();
if (!mm)
goto fail_nomem;
//复制mm_struct结构
memcpy(mm, oldmm, sizeof(*mm)); /* Initializing for Swap token stuff */
mm->token_priority = ;
mm->last_interval = ; /*
初始化,同时分配页表
mm_init()初始化mm_struct结构中的自旋锁、链表等资源,然后调用mm_alloc_pgd()函数分配页表,同时把父进程的内核虚拟地址对应的页表项复制到子进程的页表,因此父子进程共享了内核态地址空间
*/
if (!mm_init(mm, tsk))
goto fail_nomem; if (init_new_context(tsk, mm))
goto fail_nocontext; dup_mm_exe_file(oldmm, mm); //拷贝vm_area_struct结构
err = dup_mmap(mm, oldmm);
if (err)
goto free_pt; mm->hiwater_rss = get_mm_rss(mm);
mm->hiwater_vm = mm->total_vm; if (mm->binfmt && !try_module_get(mm->binfmt->module))
goto free_pt; return mm; free_pt:
/* don't put binfmt in mmput, we haven't got module yet */
mm->binfmt = NULL;
mmput(mm); fail_nomem:
return NULL; fail_nocontext:
/*
* If init_new_context() failed, we cannot use mmput() to free the mm
* because it calls destroy_context()
*/
mm_free_pgd(mm);
free_mm(mm);
return NULL;
}
继续跟进mm_alloc_pgd()
\linux-2.6.32.63\arch\x86\mm\pgtable.c
static inline int mm_alloc_pgd(struct mm_struct * mm)
{
mm->pgd = pgd_alloc(mm);
if (unlikely(!mm->pgd))
return -ENOMEM;
return ;
} pgd_t *pgd_alloc(struct mm_struct *mm)
{
pgd_t *pgd;
pmd_t *pmds[PREALLOCATED_PMDS]; pgd = (pgd_t *)__get_free_page(PGALLOC_GFP); if (pgd == NULL)
goto out; mm->pgd = pgd; if (preallocate_pmds(pmds) != )
goto out_free_pgd; if (paravirt_pgd_alloc(mm) != )
goto out_free_pmds; /*
Make sure that pre-populating the pmds is atomic with respect to anything walking the pgd_list,
so that they never see a partially populated pgd.
*/
spin_lock(&pgd_lock); pgd_ctor(pgd);
pgd_prepopulate_pmd(mm, pgd, pmds); spin_unlock(&pgd_lock); return pgd; out_free_pmds:
free_pmds(pmds);
out_free_pgd:
free_page((unsigned long)pgd);
out:
return NULL;
}
至此,页表分配完毕,同时内核态地址空间的映射关系已经建立。我们继续学习dup_mmap()是如何处理用户态地址空间的相关数据结构的,它主要完成以下几件事
. 分配并复制vm_area_struct结构
. 根据vm_area_struct结构的属性标志设置页表项,把可写入的内存片段设置为只读
. 当某个进程(父进程、或子进程)对其进行写入时,do_page_fault()将分配新的物理页面,为该进程建立私有数据,同时修改页表,指向新的物理页面
err = dup_mmap(mm, oldmm);
static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
struct vm_area_struct *mpnt, *tmp, *prev, **pprev;
struct rb_node **rb_link, *rb_parent;
int retval;
unsigned long charge;
struct mempolicy *pol; down_write(&oldmm->mmap_sem);
flush_cache_dup_mm(oldmm);
/*
* Not linked in yet - no deadlock potential:
*/
down_write_nested(&mm->mmap_sem, SINGLE_DEPTH_NESTING); mm->locked_vm = ;
mm->mmap = NULL;
mm->mmap_cache = NULL;
mm->free_area_cache = oldmm->mmap_base;
mm->cached_hole_size = ~0UL;
mm->map_count = ;
cpumask_clear(mm_cpumask(mm));
mm->mm_rb = RB_ROOT;
rb_link = &mm->mm_rb.rb_node;
rb_parent = NULL;
pprev = &mm->mmap;
retval = ksm_fork(mm, oldmm);
if (retval)
goto out; prev = NULL;
//处理每一个vm_area_struct结构
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next)
{
struct file *file;
//不需要复制
if (mpnt->vm_flags & VM_DONTCOPY)
{
long pages = vma_pages(mpnt);
mm->total_vm -= pages;
vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
-pages);
continue;
}
charge = ;
//需要安全计数检查
if (mpnt->vm_flags & VM_ACCOUNT)
{
unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
if (security_vm_enough_memory(len))
goto fail_nomem;
charge = len;
}
//为子进程分配新的vm_area_struct结构
tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
if (!tmp)
goto fail_nomem;
//复制整个结构
*tmp = *mpnt;
pol = mpol_dup(vma_policy(mpnt));
retval = PTR_ERR(pol);
if (IS_ERR(pol))
goto fail_nomem_policy;
vma_set_policy(tmp, pol);
tmp->vm_flags &= ~VM_LOCKED;
tmp->vm_mm = mm;
tmp->vm_next = tmp->vm_prev = NULL;
anon_vma_link(tmp);
file = tmp->vm_file; //如果这篇内存对应的是一个文件映射,则设置文件相关信息,增加文件的引用计数等
if (file)
{
struct inode *inode = file->f_path.dentry->d_inode;
struct address_space *mapping = file->f_mapping; get_file(file);
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
spin_lock(&mapping->i_mmap_lock);
if (tmp->vm_flags & VM_SHARED)
mapping->i_mmap_writable++;
tmp->vm_truncate_count = mpnt->vm_truncate_count;
flush_dcache_mmap_lock(mapping);
/* insert tmp into the share list, just after mpnt */
vma_prio_tree_add(tmp, mpnt);
flush_dcache_mmap_unlock(mapping);
spin_unlock(&mapping->i_mmap_lock);
} /*
* Clear hugetlb-related page reserves for children. This only
* affects MAP_PRIVATE mappings. Faults generated by the child
* are not guaranteed to succeed, even if read-only
*/
if (is_vm_hugetlb_page(tmp))
reset_vma_resv_huge_pages(tmp); /*
Link in the new vma and copy the page table entries.
*/
//把新的vm_area_struct结构添加到子进程
*pprev = tmp;
pprev = &tmp->vm_next;
tmp->vm_prev = prev;
prev = tmp;
//添加红黑树
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb; mm->map_count++;
/*
分配设置页表,并不需要分配物理页面
copy_page_range()函数,需要为vm_area_struct结构指定的内存区域分配并设置页表,同时把页表的物理地址设置到页目录(即上级页表中)
*/
retval = copy_page_range(mm, oldmm, mpnt); if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp); if (retval)
goto out;
}
/* a new mm has just been created */
arch_dup_mmap(oldmm, mm);
retval = ;
out:
up_write(&mm->mmap_sem);
flush_tlb_mm(oldmm);
up_write(&oldmm->mmap_sem);
return retval;
fail_nomem_policy:
kmem_cache_free(vm_area_cachep, tmp);
fail_nomem:
retval = -ENOMEM;
vm_unacct_memory(charge);
goto out;
}
继续跟进copy_page_range()
\linux-2.6.32.63\mm\memory.c
static int copy_pte_range(struct mm_struct *dst_mm, struct mm_struct *src_mm, pmd_t *dst_pmd, pmd_t *src_pmd, struct vm_area_struct *vma, unsigned long addr, unsigned long end)
{
pte_t *orig_src_pte, *orig_dst_pte;
pte_t *src_pte, *dst_pte;
spinlock_t *src_ptl, *dst_ptl;
int progress = ;
int rss[]; again:
rss[] = rss[] = ;
dst_pte = pte_alloc_map_lock(dst_mm, dst_pmd, addr, &dst_ptl);
if (!dst_pte)
return -ENOMEM;
src_pte = pte_offset_map_nested(src_pmd, addr);
src_ptl = pte_lockptr(src_mm, src_pmd);
spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
orig_src_pte = src_pte;
orig_dst_pte = dst_pte;
arch_enter_lazy_mmu_mode(); do {
/*
* We are holding two locks at this point - either of them
* could generate latencies in another task on another CPU.
*/
if (progress >= ) {
progress = ;
if (need_resched() ||
spin_needbreak(src_ptl) || spin_needbreak(dst_ptl))
break;
}
if (pte_none(*src_pte)) {
progress++;
continue;
}
/*
由于为了支持多级分页,从代码上看copy_pte_range比较繁琐,在多级分页中,copy_pte_range需要不断地为vm_start、vm_end指定的虚拟地址设置页表,最终它调用copy_one_pte设置页表项
*/
copy_one_pte(dst_mm, src_mm, dst_pte, src_pte, vma, addr, rss);
progress += ;
} while (dst_pte++, src_pte++, addr += PAGE_SIZE, addr != end); arch_leave_lazy_mmu_mode();
spin_unlock(src_ptl);
pte_unmap_nested(orig_src_pte);
add_mm_rss(dst_mm, rss[], rss[]);
pte_unmap_unlock(orig_dst_pte, dst_ptl);
cond_resched();
if (addr != end)
goto again;
return ;
}
继续跟进copy_one_pte()
/*
copy one vm_area from one task to the other. Assumes the page tables already present in the new task to be cleared in the whole range covered by this vma.
*/
static inline void copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm, pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma, unsigned long addr, int *rss)
{
unsigned long vm_flags = vma->vm_flags;
pte_t pte = *src_pte;
struct page *page; //pte contains position in swap or file, so copy.
/*
虚拟地址对应的页表被交换到磁盘上,需要注意的是,缺页中断可以从磁盘交换分区调入内存,但是缺页中断自身所用的内存及其页表是不可交换的,因此内核空间使用的页表是不可交换的
*/
if (unlikely(!pte_present(pte))) {
if (!pte_file(pte)) {
swp_entry_t entry = pte_to_swp_entry(pte); swap_duplicate(entry);
/* make sure dst_mm is on swapoff's mmlist. */
if (unlikely(list_empty(&dst_mm->mmlist))) {
spin_lock(&mmlist_lock);
if (list_empty(&dst_mm->mmlist))
list_add(&dst_mm->mmlist,
&src_mm->mmlist);
spin_unlock(&mmlist_lock);
}
if (is_write_migration_entry(entry) && is_cow_mapping(vm_flags))
{
/*
* COW mappings require pages in both parent
* and child to be set to read.
*/
make_migration_entry_read(&entry);
pte = swp_entry_to_pte(entry);
set_pte_at(src_mm, addr, src_pte, pte);
}
}
goto out_set_pte;
} /*
If it's a COW mapping, write protect it both in the parent and the child
如果是可写内存区域,则利用页表把该段内存区域设置为只读,以实现copy on write机制
*/
if (is_cow_mapping(vm_flags))
{
ptep_set_wrprotect(src_mm, addr, src_pte);
pte = pte_wrprotect(pte);
} /*
* If it's a shared mapping, mark it clean in
* the child
*/
if (vm_flags & VM_SHARED)
pte = pte_mkclean(pte);
pte = pte_mkold(pte); page = vm_normal_page(vma, addr, pte);
if (page) {
get_page(page);
page_dup_rmap(page);
rss[PageAnon(page)]++;
} out_set_pte:
set_pte_at(dst_mm, addr, dst_pte, pte);
}
继续回到copy_process中,当父进程进行系统调用时,在父进程的内核态保存了进程的"进程上下文(通用寄存器)",这是一个pt_regs结构,copy_thread()会复制父进程的pt_regs结构到子进程的内核态堆栈
\linux-2.6.32.63\arch\x86\kernel\process_32.c
int copy_thread(unsigned long clone_flags, unsigned long sp, unsigned long unused, struct task_struct *p, struct pt_regs *regs)
{
struct pt_regs *childregs;
struct task_struct *tsk;
int err; //内核态堆栈
childregs = task_pt_regs(p);
//父进程内核态堆栈中的pt_regs复制到子进程的内核态堆栈
*childregs = *regs;
//子进程的pt_regs结构的eax设置为0,所以子进程的fork()"返回"的值为0
childregs->ax = ;
//调整子进程内核态堆栈指针
childregs->sp = sp; p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+); //设置子进程的thread.eip,这样当子进程被调度运行时,就直接从ret_from_fork返回,这也就是"一次调用、2次返回的原理"
p->thread.ip = (unsigned long) ret_from_fork; task_user_gs(p) = get_user_gs(regs); tsk = current;
// I/O权限位
if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP)))
{
p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = ;
return -ENOMEM;
}
set_tsk_thread_flag(p, TIF_IO_BITMAP);
} err = ; /*
Set a new TLS for the child thread
线程本地存储机制
*/
if (clone_flags & CLONE_SETTLS)
err = do_set_thread_area(p, -,
(struct user_desc __user *)childregs->si, ); if (err && p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max = ;
} clear_tsk_thread_flag(p, TIF_DS_AREA_MSR);
p->thread.ds_ctx = NULL; clear_tsk_thread_flag(p, TIF_DEBUGCTLMSR);
p->thread.debugctlmsr = ; return err;
}
这样,子进程建立的工作就结束了,当调度到这个进程时,它将从ret_from_fork开始执行,然后跳转到syscall_exit,即"1次调用fork、两次从内核态返回到用户空间",其用户空间的返回地址保存在内核态堆栈的pt_regs结构中,这个返回地址和父进程是一致的
最后,再回到do_fork函数,如果copy_process()函数成功返回,新创建子进程被唤醒并让其投入运行。内核有意选择子进程首先执行,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入
0x1: 创建线程
首先说明Linux下的进程与线程比较相近,很大的原因是它们都需要相同的数据结构来表示,即task_struct。区别在于进程有独立的用户空间,而线程是共享的用户空间
对于Linux下线程创建的理解,我们需要抓住以下几个重点
. Linux系统的线程实现很特别,它对线程和进程并不特别区分,对Linux而言,线程只不过是一种特殊的进程罢了
. 父进程复制子进程的API包括三种(用户态)
) fork
) clone
) vfork
//这三个API的内部实际都是调用一个内核内部函数do_fork,只是填写的参数不同而已,通过flags参数来指明父、子进程需要共享的资源
. 从内核的角度来说,Linux并没有线程这个概念,Linux把所有线程都当作任务(task_struct)来实现,内核并没有准备特殊的调度算法或是定义特殊的数据结构来表现线程,线程仅仅被视为一个与其他进程共享某些资源的进程,每个线程都有属于自己独立的task_strcut,所以在内核中,线程就是一个普通的进程,在Linux下,区分线程和进程的关系是父子进程资源上的共享程度,从这个意义上来说,线程只是一种进程间共享资源的手段
线程的创建和普通进程的创建类似(本质上就是进程创建),只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源(资源的共享是Linux线程的核心概念)
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, );
/*
上面的代码和调用fork差不多,只是父子进程共享地址空间、文件系统资源、文件描述符、信号处理程序
在这种情况下,新建的进程和它的父进程就是流行的所谓线程
*/
传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类
#define CSIGNAL 0x000000ff /* 父子进程共享信号处理函数及被阻断的信号 */
#define CLONE_VM 0x00000100 /* 父子进程共享地址空间 */
#define CLONE_FS 0x00000200 /* 父子进程共享文件系统信息 */
#define CLONE_FILES 0x00000400 /* 父子进程共享打开的文件 */
#define CLONE_SIGHAND 0x00000800 /* 父子进程共享信号处理信息 */
#define CLONE_PTRACE 0x00002000 /* 继续调试子进程,即如果父进程正在处于被调试状态,要求子进程也处于被调试状态 */
#define CLONE_VFORK 0x00004000 /* 建立子进程后,父进程保持阻塞状态,直到子进程退出或者调用execve() */
#define CLONE_PARENT 0x00008000 /* 指定子进程与父进程拥有同一个父进程 */
#define CLONE_THREAD 0x00010000 /* 父子进程放入相同的线程组,这样子进程的tgid、group_leader都会做相应的设置,可以把它理解为同一个进程中的多个线程 */
#define CLONE_NEWNS 0x00020000 /* 为子进程创建新的命名空间 */
#define CLONE_SYSVSEM 0x00040000 /* 父子进程共享SystemV IPC Semaphore语义 */
#define CLONE_SETTLS 0x00080000 /* 为子进程设置独立的线程本地存储(TLS) */
#define CLONE_PARENT_SETTID 0x00100000 /* 设置父进程的TID */
#define CLONE_CHILD_CLEARTID 0x00200000 /* 清除子进程的TID */
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
#define CLONE_UNTRACED 0x00800000 /* 防止跟踪进程在子进程上强制执行CLONE_PTRACE,建立一个不允许被调试的进程,通常内核态线程会设置此标志 */
#define CLONE_CHILD_SETTID 0x01000000 /* 设置子进程的TID */
#define CLONE_STOPPED 0x02000000 /* 以TASK_STOPPED状态开始子进程,将来由掐进程来改变这种状态 */
#define CLONE_NEWUTS 0x04000000 /* New utsname group */
#define CLONE_NEWIPC 0x08000000 /* New ipcs */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */
#define CLONE_IO 0x80000000 /* Clone io context */
4. sys_execve()函数
我们知道,所有的exec家族的函数最终都是调用了sys_execve()这个系统调用来实现的,exce调用并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段
\arch\x86\kernel\process_32.c
int sys_execve(struct pt_regs *regs)
{
int error;
char *filename;
/*
1. 将可执行文件的名称装入到一个新分配的页面中
*/
filename = getname((char __user *) regs->bx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out; /*
调用do_execve()执行可执行文件
*/
error = do_execve(filename,
(char __user * __user *) regs->cx,
(char __user * __user *) regs->dx,
regs);
if (error == ) {
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
putname(filename);
out:
return error;
}
继续跟踪do_execve()
linux-2.6.32.63\fs\exec.c
/*
sys_execve() executes a new program.
1. filename: 可执行文件名称
2. argv: 指向进程参数的指针数组
3. envp: 指向进程环境变量的指针数组
4. regs: 寄存器集合 内核在访问用户空间内存时需要十分谨慎,而__user注释允许自动化工具来检测是否有相关事宜都处理得当
*/
int do_execve(char * filename, char __user *__user *argv, char __user *__user *envp, struct pt_regs * regs)
{
//将运行可执行文件时所需的信息组织到一起
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
bool clear_in_exec;
int retval; retval = unshare_files(&displaced);
if (retval)
goto out_ret; retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files; /*
1. LSM Hook Point 1:
retval = prepare_bprm_creds(bprm);
prepare_exec_creds->security_prepare_creds
int security_prepare_creds(struct cred *new, const struct cred *old, gfp_t gfp)
{
return security_ops->cred_prepare(new, old, gfp);
}
*/
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free; retval = check_unsafe_exec(bprm);
if (retval < )
goto out_free;
clear_in_exec = retval;
current->in_execve = ;
//找到并打开给定的可执行程序文件,open_exec()返回file结构指针,代表着读入可执行文件的上下文
file = open_exec(filename);
//强制转换
retval = PTR_ERR(file);
//判断open_exec()返回的是否是无效指针
if (IS_ERR(file))
goto out_unmark; sched_exec(); //要执行的文件
bprm->file = file;
//要执行的文件的名字
bprm->filename = filename;
bprm->interp = filename; /*
处理若干管理型任务
1. mm_alloc生成一个新的mm_struct实例来管理进程地址空间
2. init_new_context是一个特定于体系结构的函数,用于初始化该实例
3. __bprm_mm_init建立初始的栈
*/
retval = bprm_mm_init(bprm);
if (retval)
goto out_file; //统计命令汗参数的个数
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc) < )
goto out;
//统计环境变量参数的个数
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc) < )
goto out; //新进程的各个参数(euid、egid、参数列表、环境、文件名..)会被合并成一个类型为linux_biprm的结构,用于之后传递给内核函数 /*
可执行文件中读入开头的128个字节到linux_binprm结构brmp中的缓冲区,用于之后内核根据这头128字节判断应该调用哪个解析引擎来处理当前文件
prepare_binprm用于提供一些父进程相关的值
*/
retval = prepare_binprm(bprm);
if (retval < )
goto out; //文件名拷贝到新分配的页面中
retval = copy_strings_kernel(, &bprm->filename, bprm);
if (retval < )
goto out; bprm->exec = bprm->p;
//将环境变量拷贝到新分配的页面中
retval = copy_strings(bprm->envc, envp, bprm);
if (retval < )
goto out; //将命令行参数拷贝到新分配的页面中
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < )
goto out; //所有准备工作已经完成,所有必要的信息都已经搜集到了linux_binprm结构中的bprm中
current->flags &= ~PF_KTHREAD; //调用search_binary_handler()装入并运行目标程序,根据读入数据结构linux_binprm内的二进制文件128字节头中的关键字,决定调用哪种加载函数
/*
. LSM Hook Point 2:
retval = security_bprm_check(bprm);
int security_bprm_check(struct linux_binprm *bprm)
{
return security_ops->bprm_check_security(bprm);
} search_binary_handler用于在do_execve结束时查找一种适当的二进制格式,用于所要执行的特定文件(通常根据文件头的一个"魔数")
二进制格式处理程序负责将新程序的数据加载到旧的地址空间中,通常它们执行以下操作
1. 释放原进程使用的"所有"资源
2. 将应用程序映射到虚拟地址空间中,必须考虑下列段的处理(涉及的变量是task_struct的成员,由二进制格式处理程序设置为正确的值)
1) text段包含程序的可执行代码,start_code、end_code指定该段在地址空间中驻留的区域
2) 预先初始化的数据(在编译时指定了具体值的变量)位于start_data、end_data之间,映射自可执行文件的对应段
3) 堆(heap)用于动态内存分配,也置于虚拟地址空间中,start_brk、brk指定了其边界
4) 栈的位置由start_stack定义,向下增长
5) 程序的参数和环境也映射到虚拟地址空间中,位于arg_start、arg_end之间,以及env_start、env_end之间
3. 设置进程的指令指针和其他特定于体系结构的寄存器,以便在调度器选择该进程时开始执行程序的main函数
*/
retval = search_binary_handler(bprm, regs);
if (retval < )
goto out; /* execve succeeded */
current->fs->in_exec = ;
current->in_execve = ;
acct_update_integrals(current); free_bprm(bprm);
if (displaced)
put_files_struct(displaced);
return retval; out:
if (bprm->mm) {
acct_arg_size(bprm, );
mmput(bprm->mm);
} out_file:
//发生错误,返回inode,并释放资源
if (bprm->file)
{
//调用allow_write_access()防止其他进程在读入可执行文件期间通过内存映射改变它的内容
allow_write_access(bprm->file);
//递减file文件中的共享计数
fput(bprm->file);
} out_unmark:
if (clear_in_exec)
current->fs->in_exec = ;
current->in_execve = ; out_free:
free_bprm(bprm); out_files:
if (displaced)
reset_files_struct(displaced);
out_ret:
return retval;
}
5. Copy On Write COW(写时复制)技术
fork产生新任务的速度非常快,因为fork并不复制原任务的内存空间,而是和原任务一起共享一个"写时复制(Copy On Write)的内存空间",关于写时复制,我们需要重点理解它的概念和存在的意义
. 两个任务(task)可以同时*地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其他的任务使用
. 从产生的意义的角度来理解,写时复制和动态链接库的延迟绑定技术有异曲同工之妙,正常来说,执行了fork之后,操作系统应该将父进程(父任务)的内存空间复制一份给子进程,但是这个复制过程可能需要消耗较多的时间,而且在于子进程也并不一定会对这块内存进行"写操作",所以操作系统采用了一种"延迟复制"的思想,即等到子进程确实需要修改的时候再进行从父进程到子进程内存空间的复制
/*
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符,在一般情况下,进程创建后都会马上运行一个可执行的文件(ELF文件),这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据),这个技术大大加快的Linux进程创建的速度
*/
写 入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资 源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫者都是透明的(transparently)。此作法主要的优点是如果呼叫者 并没有修改该资源,就不会有副本(private copy)被建立
写时复制是“延迟计算(lazy evaluation)”这一计算技术(evaluation technique)的一个例子,内存管理器广泛地使用了延迟计算的技术。延迟计算使得只有当绝对需要时才执行一个昂贵的操作--如果该操作从来也不需要 的话,则它不会浪费任何一点时间。
Relevant Link:
http://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD
http://cookies5000.blog.163.com/blog/static/995922052009223112797/
http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html
http://www.programlife.net/copy-on-write.html
6. Linux下创建进程的7种API方式
#include <unistd.h>
. int execl(const char *path, const char *arg, ...);
. int execlp(const char *file, const char *arg, ...);
. int execle(const char *path, const char *arg, ..., char *const envp[]);
. int execv(const char *path, char *const argv[]);
. int execvp(const char *file, char *const argv[]);
. int execve(const char *path, char *const argv[], char *const envp[]);
这些都是用以执行一个可执行文件的函数,它们统称为"exec函数",它们的差异在于对命令行参数和环境变量参数的传递方式不同
以上函数的本质都是调用\arch\x86\kernel\process_32.c文件中实现的系统调用sys_execve()来执行一个可执行文件
exec系列函数共有7函数可供使用,这些函数的区别在于
. 使用"路径"指示新程序的位置
) 如果是使用文件名,则在系统的PATH环境变量所描述的路径中搜索该程序
. 使用"文件名"指示新程序的位置
. 使用参数列表的方式作为传入参数
. 使用argv[]数组的方式传入参数
0x1: int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
execl()函数用来执行参数pathname字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须是空指针以标志参数列表为空.
//File: execl.c
#include <unistd.h> main()
{
// 执行/bin目录下的ls, 第一参数为程序名ls, 第二个参数为"-al", 第三个参数为"/etc/passwd"
execl("/bin/ls", "ls", "-al", "/etc/passwd", (char *) ); //最后一个参数传入NULL也是可以的
execl("/bin/ls", "ls", "-al", "/etc/", NULL);
}
0x2: int execv(const char *pathname, char *const argv[]);
execv()函数函数用来执行参数path字符串所指向的程序,第二个为数组指针维护的程序参数列表,该数组的最后一个成员必须是空指针.
#include <unistd.h> int main()
{
char *argv[] = {"ls", "-l", "/etc", (char *)};
execv("/bin/ls", argv);
return ;
}
0x3: int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
execle()函数用来执行参数pathname字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须指向一个新的环境变量数组,即新执行程序的环境变量.
#include <unistd.h> int main(int argc, char *argv[], char *env[])
{
execle("/bin/ls", "ls", "-l", "/etc", (char *),env);
return ;
}
0x4: int execve(const char *pathname, char *const argv[], char *const envp[]);
execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组
#include<unistd.h> main()
{
char * argv[ ]={"ls", "-al", "/etc/passwd", (char *)};
char * envp[ ]={"PATH=/bin", };
execve("/bin/ls", argv, envp);
}
0x5: int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
execlp()函数会从PATH环境变量所指的目录中查找文件名为第一个参数filename指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须是空指针.
#include <unistd.h> int main()
{
execlp("ls", "ls", "-l", "/etc", (char *));
return ;
}
0x6: int execvp(const char *filename, char *const argv[]);
execvp()函数会从PATH环境变量所指的目录中查找文件名为第一个参数file指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个成员必须是空指针.
#include <unistd.h> int main()
{
char *argv[] = {"ls", "-l", "/etc", (char *)};
execvp("ls", argv);
return ;
}
0x7: int fexecve(int fd, char *const argv[], char *const envp[]);
fexecve()执行的任务与execve()相同,所不同的是执行的文件通过文件描述符fd指定,而不是通过路径。文件描述符fd必需以只读方式打开,并且调用者必需有执行相应文件的权限,在 Linux系统里,fexecve()的实现使用了proc()文件系统,所以 /proc 必需被挂载并在调用时可用
值得注意的是fexecve的打开对象是文件描述符(file discriptor fd),在Linux下,文件描述符可以是通过open打开的可执行文件、也可以是通过父进程继承的命名管道(named pipe)
//lgo.c
#include <stdio.h>
#include <unistd.h> int main(int argc, char **argv)
{
extern char **environ;
(void) argc;
fexecve(, argv, environ);
perror("fexecve");
return ;
}
Relevant Link:
http://www.2cto.com/os/201410/342362.html
http://cpp.ezbty.org/import_doc/linux_manpage/fexecve.3.html
http://*.com/questions/13690454/how-to-compile-and-execute-from-memory-directly
http://security.stackexchange.com/questions/20974/how-a-malware-executes-remote-payload
值得注意的是,glibc提供的这7种进程执行的API,只是起到一个适配转接的作用,最终在内部都会调用到同一个函数"__execve"
\glibc-2.18\posix\execle.c\
/* Execute PATH with all arguments after PATH until a NULL pointer,
and the argument after that for environment. */
int execle (const char *path, const char *arg, ...)
{
#define INITIAL_ARGV_MAX 1024
size_t argv_max = INITIAL_ARGV_MAX;
const char *initial_argv[INITIAL_ARGV_MAX];
const char **argv = initial_argv;
va_list args;
argv[] = arg; va_start (args, arg);
unsigned int i = ;
while (argv[i++] != NULL)
{
if (i == argv_max)
{
argv_max *= ;
const char **nptr = realloc (argv == initial_argv ? NULL : argv,
argv_max * sizeof (const char *));
if (nptr == NULL)
{
if (argv != initial_argv)
free (argv);
return -;
}
if (argv == initial_argv)
/* We have to copy the already filled-in data ourselves. */
memcpy (nptr, argv, i * sizeof (const char *)); argv = nptr;
} argv[i] = va_arg (args, const char *);
} const char *const *envp = va_arg (args, const char *const *);
va_end (args); int ret = __execve (path, (char *const *) argv, (char *const *) envp);
if (argv != initial_argv)
free (argv); return ret;
}
libc_hidden_def (execle)
\glibc-2.18\sysdeps\unix\sysv\linux\execve.c
int __execve (file, argv, envp)
const char *file;
char *const argv[];
char *const envp[];
{
return INLINE_SYSCALL (execve, , file, argv, envp);
}
weak_alias (__execve, execve)
7. Glibc execve、fork API源代码分析
0x1: execve
\glibc-2.18\sysdeps\unix\sysv\linux\execve.c
int __execve (file, argv, envp)
const char *file;
char *const argv[];
char *const envp[];
{
return INLINE_SYSCALL (execve, , file, argv, envp);
}
weak_alias (__execve, execve)
0x2: fork
pid_t __libc_fork (void)
{
pid_t pid;
struct used_handler
{
struct fork_handler *handler;
struct used_handler *next;
} *allp = NULL; /* Run all the registered preparation handlers. In reverse order.
While doing this we build up a list of all the entries. */
struct fork_handler *runp;
while ((runp = __fork_handlers) != NULL)
{
/* Make sure we read from the current RUNP pointer. */
atomic_full_barrier (); unsigned int oldval = runp->refcntr; if (oldval == )
/* This means some other thread removed the list just after
the pointer has been loaded. Try again. Either the list
is empty or we can retry it. */
continue; /* Bump the reference counter. */
if (atomic_compare_and_exchange_bool_acq (&__fork_handlers->refcntr,
oldval + , oldval))
/* The value changed, try again. */
continue; /* We bumped the reference counter for the first entry in the
list. That means that none of the following entries will
just go away. The unloading code works in the order of the
list. While executing the registered handlers we are building a
list of all the entries so that we can go backward later on. */
while ()
{
/* Execute the handler if there is one. */
if (runp->prepare_handler != NULL)
runp->prepare_handler (); /* Create a new element for the list. */
struct used_handler *newp
= (struct used_handler *) alloca (sizeof (*newp));
newp->handler = runp;
newp->next = allp;
allp = newp; /* Advance to the next handler. */
runp = runp->next;
if (runp == NULL)
break; /* Bump the reference counter for the next entry. */
atomic_increment (&runp->refcntr);
} /* We are done. */
break;
} _IO_list_lock (); #ifndef NDEBUG
pid_t ppid = THREAD_GETMEM (THREAD_SELF, tid);
#endif /* We need to prevent the getpid() code to update the PID field so
that, if a signal arrives in the child very early and the signal
handler uses getpid(), the value returned is correct. */
pid_t parentpid = THREAD_GETMEM (THREAD_SELF, pid);
THREAD_SETMEM (THREAD_SELF, pid, -parentpid); #ifdef ARCH_FORK
pid = ARCH_FORK ();
#else
# error "ARCH_FORK must be defined so that the CLONE_SETTID flag is used"
pid = INLINE_SYSCALL (fork, );
#endif if (pid == )
{
struct pthread *self = THREAD_SELF; assert (THREAD_GETMEM (self, tid) != ppid); if (__fork_generation_pointer != NULL)
*__fork_generation_pointer += ; /* Adjust the PID field for the new process. */
THREAD_SETMEM (self, pid, THREAD_GETMEM (self, tid)); #if HP_TIMING_AVAIL
/* The CPU clock of the thread and process have to be set to zero. */
hp_timing_t now;
HP_TIMING_NOW (now);
THREAD_SETMEM (self, cpuclock_offset, now);
GL(dl_cpuclock_offset) = now;
#endif #ifdef __NR_set_robust_list
/* Initialize the robust mutex list which has been reset during
the fork. We do not check for errors since if it fails here
it failed at process start as well and noone could have used
robust mutexes. We also do not have to set
self->robust_head.futex_offset since we inherit the correct
value from the parent. */
# ifdef SHARED
if (__builtin_expect (__libc_pthread_functions_init, ))
PTHFCT_CALL (ptr_set_robust, (self));
# else
extern __typeof (__nptl_set_robust) __nptl_set_robust
__attribute__((weak));
if (__builtin_expect (__nptl_set_robust != NULL, ))
__nptl_set_robust (self);
# endif
#endif /* Reset the file list. These are recursive mutexes. */
fresetlockfiles (); /* Reset locks in the I/O code. */
_IO_list_resetlock (); /* Reset the lock the dynamic loader uses to protect its data. */
__rtld_lock_initialize (GL(dl_load_lock)); /* Run the handlers registered for the child. */
while (allp != NULL)
{
if (allp->handler->child_handler != NULL)
allp->handler->child_handler (); /* Note that we do not have to wake any possible waiter.
This is the only thread in the new process. The count
may have been bumped up by other threads doing a fork.
We reset it to 1, to avoid waiting for non-existing
thread(s) to release the count. */
allp->handler->refcntr = ; /* XXX We could at this point look through the object pool
and mark all objects not on the __fork_handlers list as
unused. This is necessary in case the fork() happened
while another thread called dlclose() and that call had
to create a new list. */ allp = allp->next;
} /* Initialize the fork lock. */
__fork_lock = LLL_LOCK_INITIALIZER;
}
else
{
assert (THREAD_GETMEM (THREAD_SELF, tid) == ppid); /* Restore the PID value. */
THREAD_SETMEM (THREAD_SELF, pid, parentpid); /* We execute this even if the 'fork' call failed. */
_IO_list_unlock (); /* Run the handlers registered for the parent. */
while (allp != NULL)
{
if (allp->handler->parent_handler != NULL)
allp->handler->parent_handler (); if (atomic_decrement_and_test (&allp->handler->refcntr)
&& allp->handler->need_signal)
lll_futex_wake (allp->handler->refcntr, , LLL_PRIVATE); allp = allp->next;
}
} return pid;
}
weak_alias (__libc_fork, __fork)
libc_hidden_def (__fork)
weak_alias (__libc_fork, fork)
8. 查看进程的启动过程工具
要想查看进程的启动过程,可以使用两个工具: strace和LD_DEBUG
source:
#include <stdlib.h>
#include <stdio.h> int main()
{
printf("hello world\n");
return 0;
}
编译程序:
gcc -o hello -O2 hello.c
strace -tt ./hello
05:47:11.645477 execve("./hello", ["./hello"], [/* 38 vars */]) = 0
05:47:11.646521 brk(0) = 0x82f8000
05:47:11.646660 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77fd000
05:47:11.646745 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
05:47:11.646929 open("/etc/ld.so.cache", O_RDONLY) = 3
05:47:11.647012 fstat64(3, {st_mode=S_IFREG|0644, st_size=50450, ...}) = 0
05:47:11.647176 mmap2(NULL, 50450, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77f0000
05:47:11.647223 close(3) = 0
05:47:11.647348 open("/lib/libc.so.6", O_RDONLY) = 3
05:47:11.647409 read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0@\356X\0004\0\0\0"..., 512) = 512
05:47:11.647496 fstat64(3, {st_mode=S_IFREG|0755, st_size=1906124, ...}) = 0
05:47:11.647605 mmap2(0x578000, 1665416, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x578000
05:47:11.647648 mprotect(0x708000, 4096, PROT_NONE) = 0
05:47:11.647693 mmap2(0x709000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x190) = 0x709000
05:47:11.647761 mmap2(0x70c000, 10632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x70c000
05:47:11.647819 close(3) = 0
05:47:11.648707 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ef000
05:47:11.648797 set_thread_area({entry_number:-1 -> 6, base_addr:0xb77ef6c0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0,
useable:1}) = 0
05:47:11.649201 mprotect(0x709000, 8192, PROT_READ) = 0
05:47:11.649272 mprotect(0x570000, 4096, PROT_READ) = 0
05:47:11.649326 munmap(0xb77f0000, 50450) = 0
05:47:11.649560 fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
05:47:11.649678 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77fc000
05:47:11.649754 write(1, "hello world\n", 12hello world
) = 12
05:47:11.649829 exit_group(0) = ?
LD_DEBUG=libs ./hello
26605: find library=libc.so.6 [0]; searching
26605: search cache=/etc/ld.so.cache
26605: trying file=/lib/libc.so.6
26605:
26605:
26605: calling init: /lib/libc.so.6
26605:
26605:
26605: initialize program: ./hello
26605:
26605:
26605: transferring control: ./hello
26605:
hello world
26605:
26605: calling fini: ./hello [0]
26605:
26605:
26605: calling fini: /lib/libc.so.6 [0]
26605:
9. Linux下线程创建
Linux下没有像windows那样明确的线程定义,或者从另一个角度来说,Linux下的线程更加切进"线程"本质的概念,即线程和进程的差别本质上是"资源共享程度"问题,进程和线程之间的界限从某种程度上来说也不应该是那么明确,Linux下创建线程来如下几种方法
. fork()+execve: 需要共享的资源: CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND
. Linux POSIX Thread
. LinuxThreads:was a partial implementation of POSIX Threads
. Native POSIX Thread Library(NPTL)
需要明白的是,Linux下线程创建的最底层的内核级支持还是fork,fork提供了丰富的flog共享参数,以此提供"父子进程(线程)"不同程度的共享,POSIX线程以及基于POSIX实现的线程库都是基于fork实现的,但是要注意的,Linux的线程不是简单的包装用户空间的fork,而是一种基于内核级支持的线程库实现
0x1: Linux POSIX Thread
POSIX(可移植操作系统接口)线程是提高代码响应和性能的有力手段
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h> void *thread_function(void *arg)
{
int i;
for ( i=; i<; i++)
{
printf("Thread says hi!\n");
sleep();
}
return NULL;
} int main(void)
{
pthread_t mythread; if ( pthread_create( &mythread, NULL, thread_function, NULL) )
{
printf("error creating thread.");
abort();
}
if ( pthread_join ( mythread, NULL ) )
{
printf("error joining thread.");
abort();
}
exit();
}
我们知道,当用 fork() 创建另一个新进程时,新进程是子进程,原始进程是父进程。这创建了可能非常有用的层次关系,尤其是等待子进程终止时。例如,waitpid() 函数让当前进程等待所有子进程终止。waitpid() 用来在父进程中实现简单的清理过程
而 POSIX 线程中不存在这种层次关系。虽然主线程可以创建一个新线程,新线程可以创建另一个新线程,POSIX 线程标准将它们视为等同的层次。所以等待子线程退出的概念在这里没有意义 POSIX 线程标准不记录任何"家族"信息
缺少家族信息有一个主要含意:如果要等待一个线程终止,就必须将线程的 tid 传递给 pthread_join()。线程库无法为您断定 tid
Relevant Link:
http://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/
0x2: LinuxThreads:was a partial implementation of POSIX Threads
In the Linux operating system, LinuxThreads was a partial implementation of POSIX Threads. It has since been superseded by the Native POSIX Thread Library (NPTL)
Relevant Link:
http://www.ibm.com/developerworks/cn/linux/l-threading.html
http://en.wikipedia.org/wiki/LinuxThreads
0x3: Native POSIX Thread Library(NPTL)
Native POSIX Thread Library(NPTL)是一个能够使使用POSIX Threads编写的程序在Linux内核上更有效地运行的软件
NPTL的解决方法与LinuxThreads类似,内核看到的首要抽象依然是一个进程,新线程是通过clone()系统调用产生的。但是NPTL需要特殊的内核支持来解决同步的原始类型之间互相竞争的状况。在这种情况下线程必须能够入眠和再复苏。用来完成这个任务的原始类型叫做futex
NPTL是一个所谓的"1 x 1线程函数库"。用户产生的线程与内核能够分配的对象之间的联系是一对一的。这是所有线程程序中最简单的
getconf GNU_LIBPTHREAD_VERSION
Relevant Link:
http://zh.wikipedia.org/wiki/Native_POSIX_Thread_Library
10. Posix线程
0x1: 线程创建
1. 线程与进程
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。对于Linux来说,线程其实是一个伪概念
. Linux下的所有新建进程都是通过父进程"复制"出来的,父子进程之间可以实现不同程度的资源共享
. 父子进程本来就是不同的进程,有自己的栈空间、执行序列是显然的
. Linux上区分进程、线程只是通过clone_flags,即资源共享程度来定义区分的
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移
2. 创建线程
POSIX通过pthread_create()函数创建线程,API定义如下
int pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);
与fork()调用创建一个进程的方法不同
. pthread_create()创建的线程并不具备与主线程(即调用pthread_create()的线程)同样的执行序列,而是使其运行start_routine(arg)函数
. thread参数接收返回创建的线程ID
. attr参数返回的是创建线程时设置的线程属性
. pthread_create()的返回值表示线程创建是否成功
. 尽管arg是void *类型的变量,但它同样可以作为任意类型的参数传给start_routine()函数
. start_routine()可以返回一个void *类型的返回值,而这个返回值也可以是其他类型,并由pthread_join()获取
我们从更本质的角度来看Linux下POSIX线程库的线程创建
. Linux中调用fork、clone系统调用都可以产生新进程,而在这两个系统调用内部都是调用的do_fork()内核函数实现的,只是所传的参数不同
. 而POSIX线程库本质上是对clone系统调用的封装,对pthread_create代码进行strace可以看到
/*clone(child_stack=0x7fe4b92daff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fe4b92db9d0, tls=0x7fe4b92db700, child_tidptr=0x7fe4b92db9d0) = 4634
*/
. fork和clone在这里的区别在于
) fork不能修改复制出来的子进程的栈空间,所以只能复制一份"完整的"父进程
) clone允许修改子进程的栈空间,所以允许创建任意入口函数的子进程,即我们可以在线程中部署不同的我们所需要的任务函数
3. 线程创建属性
pthread_create()中的attr参数是一个结构指针,结构中的元素分别对应着新线程的运行属性,主要包括以下几项
typedef struct
{
/*
线程的分离状态
detachstate表示新线程是否与进程中其他线程脱离同步
1. 如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态
2. 这个属性也可以在线程创建并运行以后用pthread_detach()来设置
3. 一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态
*/
int detachstate; /*
线程调度策略
schedpolicy表示新线程的调度策略,主要包括
1. SCHED_OTHER(正常、非实时): 缺省为SCHED_OTHER
2. SCHED_RR(实时、轮转法): 仅对超级用户有效
3. SCHED_FIFO(实时、先入先出): 仅对超级用户有效
运行时可以用过pthread_setschedparam()来改变
*/
int schedpolicy; /*
线程的调度参数
schedparam是一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效
并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0
*/
struct sched_param schedparam;
inheritsched有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。 //线程的继承性
int inheritsched; /*
线程的作用域
scope表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值
1. PTHREAD_SCOPE_SYSTEM: 表示与系统中所有线程一起竞争CPU时间,目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值
2. PTHREAD_SCOPE_PROCESS: 表示仅与同进程中的线程竞争CPU
*/
int scope;
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
pthread_attr_t结构中还有一些值,但不使用pthread_create()来设置,为了设置这些属性,POSIX定义了一系列属性设置函数,包括
. pthread_attr_init()
. pthread_attr_destroy()
. 与各个属性相关的pthread_attr_getxxx / pthread_attr_setxxx函数
4. 线程创建的Linux POSIX实现
我们知道,Linux的线程实现是内核外进行的,内核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用__clone()和fork(),最终都用不同的参数调用do_fork()核内API。当然,要想实现线程,没有核心对多进程(其实是轻量级进程)共享数据段的支持是不行的,因此,do_fork()提供了很多参数,包括
. CLONE_VM(共享内存空间)
. CLONE_FS(共享文件系统信息)
. CLONE_FILES(共享文件描述符表)
. CLONE_SIGHAND(共享信号句柄表)
. CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)
当使用fork系统调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境
而使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的"进程"拥有共享的运行环境,只有栈是独立的
Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread库使用一个管理线程(__pthread_manager(),每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号(比如Cancel),而主线程(pthread_create())的调用者则通过管道将请求信息传给管理线程。这在JVM For Linux上的实现也是类似的原理
0x2: 线程取消
1. 线程取消的定义
一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止
2. 线程取消的语义
线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定
. 或者忽略
. 或者立即终止
. 或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定
线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,也就是说设置一个CANCELED状态,线程继续运行,只有运行至Cancelation-point的时候才会退出
3. 取消点
根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作,但是CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
4. 程序设计方面的考虑
如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用
5 与线程取消相关的pthread函数
. int pthread_cancel(pthread_t thread);
发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止 . int pthread_setcancelstate(int state, int *oldstate);
设置本线程对Cancel信号的反应,state有两种值
) PTHREAD_CANCEL_ENABLE(缺省): 收到信号后设为CANCLED状态
) PTHREAD_CANCEL_DISABLE: 收到信号后忽略CANCEL信号继续运行
old_state如果不为NULL则存入原来的Cancel状态以便恢复 . int pthread_setcanceltype(int type, int *oldtype);
设置本线程取消动作的执行时机,type由两种取值
) PTHREAD_CANCEL_DEFFERED: 收到信号后继续运行至下一个取消点再退出
) PTHREAD_CANCEL_ASYCHRONOUS: 收到信号后继续立即执行取消动作(退出),仅当Cancel状态为Enable时有效
oldtype如果不为NULL则存入运来的取消动作类型值 . void pthread_testcancel(void);
检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回
0x3: 通过FLAG判断当前是否为线程新建
包括pthread_create建立的线程,或者是其他任何POSIX库、包括原生的建立线程的过程。其本质都是都是通过clone系统调用建立的
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ ); flags:
. CLONE_CHILD_CLEARTID
. CLONE_CHILD_SETTID
. CLONE_FILES
. CLONE_FS
. CLONE_IO
. CLONE_NEWIPC
. CLONE_NEWNET
. CLONE_NEWNS
. CLONE_NEWPID
. CLONE_NEWUTS
. CLONE_PARENT
. CLONE_PARENT_SETTID
. CLONE_PID
. CLONE_PTRACE
. CLONE_SETTLS
. CLONE_SIGHAND: 共享信号
. CLONE_STOPPED
. CLONE_SYSVSEM
. CLONE_THREAD: 声明所有的同一进程的线程要在一个线程组里面了
. CLONE_UNTRACED
. CLONE_VFORK
. CLONE_VM: 共享虚拟内存空间
从最基本的角度来看,要构成一个线程的要求
. 一个线程和同一进程的别的线程必须共享地址空间
. 按照POSIX的约定,线程们必须共享信号,因此,CLONE_SIGHAND也是必须的
. 所有的同一进程的线程要在一个线程组里面了,因此CLONE_THREAD也是必须的 /*
从man手册可以看出,CLONE_THREAD的设置要求CLONE_SIGHAND被设置,而CLONE_SIGHAND的设置要求CLONE_VM被设置,在内核的copy_process函数里面有:
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);
*/
通过判断传入的clone_flags参数,可以判断出当前是创建进程还是创建线程
CLONE_VM | CLONE_THREAD | CLONE_SIGHAND
//只要同时出现这3个FLAG,说明此时正在进行线程创建
Relevant Link:
https://www.ibm.com/developerworks/cn/linux/thread/posix_threadapi/part1/
Copyright (c) 2014 LittleHann All rights reserved