1 进程与线程
进程是处于执行期的程序,线程是进程的执行单元。进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
进程 | 线程 | |
---|---|---|
资源开销 | 每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销 | 同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小 |
包含关系 | 可包含多个线程 | 进程的一部分 |
内存分配 | 进程之间的地址空间和资源是相互独立的 | 同一进程的线程共享本进程的地址空间和资源 |
2 描述符及结构
进程描述符:struct task_struct
位置:位于进程内核栈的栈底
获取:通过current
宏
3 进程创建
写时拷贝:调用fork时,不直接复制资源给新进程,以只读方式共享,在需要写入时,才被复制。
fork与vfork
fork | vfork | |
---|---|---|
资源 | 复制父进程资源 | 与父进程共享 |
执行 | 不确定 | 子进程先运行,直到exit或exec |
3.1 fork流程
当应用层调用fork、vfork、__clone时,最终都会调用到内核的do_fork
,根据传入的参数不同,进行不同的处理。
应用层:fork、vfork、__clone
---------------------------
内核: do_fork
asmlinkage int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
}
asmlinkage int sys_vfork(struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
}
asmlinkage int sys_clone(unsigned long clone_flags, unsigned long newsp,
int __user *parent_tidptr, int tls_val,
int __user *child_tidptr, struct pt_regs *regs)
{
if (!newsp)
newsp = regs->ARM_sp;
return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}
do_fork中调用copy_process()进行创建工作
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)
{
...
p = dup_task_struct(current);//为新进程分配内核栈
...
//每个用户有且只有一个user_struct
//rlim,对该进程占用的各种资源做限制
//RLIMIT_NPROC,规定了该进程所属用户可以拥有的进程数
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;
}
...
copy_flags(clone_flags, p);//复制进程标志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);//分配pid
if (!pid)
goto bad_fork_cleanup_io;
if (clone_flags & CLONE_NEWPID) {
retval = pid_ns_prepare_proc(p->nsproxy->pid_ns);
if (retval < 0)
goto bad_fork_free_pid;
}
}
...
}
copy_process()函数完成的工作:
1)调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。
2)检查并确保当前用户的进程数目没有超出资源限制。
3)子进程着手使自己和父进程区分开来。进程描述符内的不继承父进程的成员被清0或者设为初始值。
4)子进程状态被置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。
5)copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
6)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
7)调用alloc_pid()为新进程分配一个有效的PID。
8)copy_process()做扫尾工作并返回一个指向子进程的指针。
4 线程
(1)创建线程
通过clone传入不同的参数(linux/sched.h中定义),指明要共享的资源,例如
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
(2)内核线程
与普通进程区别:没有独立的地址空间,共享内核地址空间。
5 终结
5.1 do_exit()
进程终结,大部分任务靠do_exit完成。
NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
if (unlikely(in_interrupt()))//检查不应该在中断中调用
panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))//0号进程,init不应该退出
panic("Attempted to kill the idle task!");
...
exit_irq_thread();
exit_signals(tsk); /* sets PF_EXITING */
...
exit_mm(tsk);//释放进程占用的mm_struct
...
exit_sem(tsk);
exit_files(tsk);
exit_fs(tsk);
check_stack_usage();
exit_thread();
cgroup_exit(tsk, 1);
...
exit_notify(tsk, group_dead);
...
tsk->state = TASK_DEAD;
schedule();
...
}
流程:
1)将task_struct中的标识成员设置为PF_EXITING;
2)调用exit_mm()释放进程占用的mm_struct;
3)调用exit_sem(),使进程离开等待IPC信号的队列;
4)调用exit_files()和exit_fs(),递减文件描述符、文件系统数据的引用计数。
5)把task_struct的exit_code设置为进程的返回值,供父进程随时检索;
6)调用exit_notify()向父进程发送信号;
7)调用schedule()切换到新进程继续执行。
5.2 删除进程描述符
do_exit后,进程资源并没有完全释放,当最终需要释放进程描述符时,release_task()会被调用。
void release_task(struct task_struct * p)
{
struct task_struct *leader;
int zap_leader;
repeat:
tracehook_prepare_release_task(p);
rcu_read_lock();
atomic_dec(&__task_cred(p)->user->processes);
rcu_read_unlock();
proc_flush_task(p);
write_lock_irq(&tasklist_lock);
tracehook_finish_release_task(p);
__exit_signal(p);
zap_leader = 0;
leader = p->group_leader;
if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {
BUG_ON(task_detached(leader));
do_notify_parent(leader, leader->exit_signal);
zap_leader = task_detached(leader);
if (zap_leader)
leader->exit_state = EXIT_DEAD;
}
write_unlock_irq(&tasklist_lock);
release_thread(p);
call_rcu(&p->rcu, delayed_put_task_struct);
p = leader;
if (unlikely(zap_leader))
goto repeat;
}
完成以下工作:
1)调用__exit_signal(),该函数调用_unhash_process(),后者又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程。
2)_exit_signal()释放目前僵死进程所使用的剩余资源,并进行最终统计和记录。
3)如果这个进程是线程组的最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程。
4)release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存。
至此,进程描述符和所有进程独享的资源就全部释放掉了。