从进程到线程
该文出自:http://www.civilnet.cn/bbs/browse.php?topicno=78429
首先声明,gemfield本文以Linux为基础,所涉及到的线程概念以Linux为准。避免对于windows下的你产生困扰。
在《从程序到进程》一文中,我们知道了进程在内核中是以一个task_struct结构来描述和维护的,那么,我们编程中使用的线程概念,在内核中是怎么维护的呢?和进程有什么区别?
真相是:进程和线程没有什么大的区别;在Linux内核中,内核将用户进程、用户线程和内核线程一视同仁,即内核使用唯一的数据结构task_struct来分别表示他们;内核使用相同的调度算法对这三者进行调度;为什么没有内核进程呢?gemfield说,你可以把内核线程叫作内核进程!这又是为什么呢?
因为在Linux下,进程和线程都是由task_struct结构描述的,要说区别,就是线程是共享一个进程的内存资源的(所以也被称为轻量级进程)。而在内核中,内存本来就是共享的,所以你可以叫它内核线程,也可以叫它内核进程,不过习惯上称之为kernel threads(内核线程)。
gemfield通过ps -ef 命令来给你一个直观的印象:
ps -ef 输出:
****************************************
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 May13 ? 00:00:05 /sbin/init
root 2 0 0 May13 ? 00:00:00 [kthreadd]
root 3 2 0 May13 ? 00:31:52 [ksoftirqd/0]
root 6 2 0 May13 ? 00:00:39 [migration/0]
root 7 2 0 May13 ? 00:00:33 [watchdog/0]
root 8 2 0 May13 ? 00:00:59 [migration/1]
root 10 2 0 May13 ? 00:26:26 [ksoftirqd/1]
root 11 2 0 May13 ? 00:00:32 [watchdog/1]
root 12 2 0 May13 ? 00:00:43 [migration/2]
root 13 2 0 May13 ? 00:31:13 [kworker/2:0]
root 14 2 0 May13 ? 00:23:13 [ksoftirqd/2]
root 15 2 0 May13 ? 00:00:32 [watchdog/2]
root 16 2 0 May13 ? 00:00:44 [migration/3]
root 18 2 0 May13 ? 00:24:34 [ksoftirqd/3]
root 19 2 0 May13 ? 00:00:37 [watchdog/3]
**************************************************
上面输出的就是一系列的用户和内核进程,其中内核线程使用方括号[]括起来。pid是当前进程的id,ppid是父进程的id。 比如:[ksoftirqd/0] 内核线程是用来实施软中断的; 更多内容参考:http://civilnet.cn/bbs/topicno/71181。 要了解线程(下面只介绍用户线程,所以提到线程,指的就是用户线程)的这些内容,我们先来看看线程是怎么产生的?
Linux上使用pthread这个POSIX的线程库来创建线程。如下:
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(* start_routine)(void *), void *arg);
若成功则返回0,否则返回出错编号;返回成功时,由thread指向的内存单元被设置为新创建线程的线程ID;attr参数用于初始化线程属性;新创建的线程从start_routine函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。在编译时注意加上 -lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。
因此,创建线程的pthread_create就相当于创建进程的程序中的main()函数。
而pthread_create的库函数调用最终调用了clone()系统调用。现在情况有点明了了:创建进程用的是fork系统调用,而创建线程用的是clone系统调用。焦点就集中在这里了:fork和clone这两个系统调用的区别是什么?
在内核中这2个调用分别调用sys_fork(),sys_clone(),然后又都调用do_fork()去做具体的创建进程的工作。如下:
*****************************************************
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0);
}
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx; newsp = regs.ecx;
if (!newsp)
newsp = regs.esp;//子进程在用户态时使用的栈低,由clone中的child_stack参数指定
return do_fork(clone_flags, newsp, ®s, 0);
}
****************************************************
这么说来,创建进程和创建线程的工作最终都归于do_fork系统调用了?那不是没有区别了吗?gemfield说,别急,do_fork系统调用还有参数呢,这个参数是从clone传过来的。如下:
****************************************************** 标志 含义
CLONE_PARENT 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
CLONE_PTRACE 若父进程被trace,子进程也被trace
CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
CLONE_VM 子进程与父进程运行于相同的内存空间
CLONE_PID 子进程在创建时PID与父进程一致
CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
************************************************************
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针, child_stack明显是为子进程分配系统堆栈空间,flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。而flags可以取的值就是上面表中所介绍的。
现在我们知道了这两者的本质区别是参数的不同,现在就要去do_fork中去看看是如何处理这些参数的。do_fork的原型:
int do_fork(unsigned int clone_flags, unsigned long stack_start, struct pt_regs * regs, unsigned long stack_size)
1、clone_flags是由2部分组成,最低字节为信号类型,用于规定子进程去世时向父进程发出的信号,fork中这个信号就是SIGCHLD;
2、clone则可以由用户自己定义信号类型。第2部分是表示资源和特性的标志位(标志参考上表);
3、对于fork我们可以看出第2部分全部是0,也就是对有关资源都要复制,而不是通过指针共享;
do_fork的代码如下:
**********************************************************
int do_fork(unsigned int clone_flags, unsigned long stack_start, struct pt_regs * regs, unsigned long stack_size)
{
int retval = -ENOMEM;//将可能返回的值error初始值置为-ENOMEM(error no mem,没有内存)
struct task_struct *p;//创建一个进程描述符的指针 DECLARE_MUTEX_LOCKED(sem); //定义和创建了一个用于进程互斥和同步的信号量
//CLONE_PID是子进程和父进程拥有相同的PID号,这只有一种情况可以使用,就是父进程的PID为0
if(clone_flags & CLONE_PID)
{
if(current->pid)
return -EPERM;
}
current->vfork_sem = sem;
p = alloc_task_struct();//为子进程分配2个页面(用来做系统栈和存放task_struct的)
if(!p)
goto fork_out;
*p = *current; //将父进程的task_struct赋值到2个页面中 retval = -EAGAIN;
//p->user 指向该进程所属用户的数据结构,这个数据结构见http://civilnet.cn/bbs/topicno/71182 //内核进程不属于任何用户,所以它的p->user = 0),p->rlim是对进程资源的限制, //而p->rlim[RLIMIT_NPROC]则规定了该进程所属用户可以拥有的进程数量,如果超过这个数量就不可以再fork了。
if(atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur)
goto bad_fork_free;
atomic_inc(&p->user->__count); atomic_inc(&p->user->processes);
//上面是对用户进程的限制,这里是对内核进程的数量限制
if(nr_threads >= max_threads) goto bad_fork_cleanup_count;
//p->exec_domain指向一个exec_domain结构,定义见http://civilnet.cn/bbs/topicno/71183 get_exec_domain(p->exec_domain);
//每个进程都属于某种可执行的印象格式如a.out或者elf, //对这些格式的支持都是通过动态安装驱动模块来实现的,binfmt就是用来指向这些格式驱动。 if(p->binfmt && p->binfmt->module) __MOD_INC_USE_COUNT(p->binfmt->module);
p->did_exec = 0;//表示进程未被执行过 p->swappable = 0;//由于是新建进程,暂时拒绝被调用出内存 //表示本进程将被置于等待队列中,由于资源未分配好, //因此置为不可中断,使其待资源有效时唤醒,不可由其它进程通过信号唤醒 p->state = TASK_UNINTERRUPTIBLE; copy_flags(clone_flags, p);
//get_pid()函数先判断调用它的do_fork()是否进行clone系统调用, //它还进行了与组标识号及区标识号进行区别的判断; p->pid = get_pid(clone_flags); //设置新建进程的PID
//由于新产生的进程的状态还是为TASK_UNINTERRUPTIBLE, //因此不将其放入就绪队列,将next_run,prev_run项均置为NULL。 //将指向原始父进程、父进程指针项赋值为当前进程Current; p->run_list.next = NULL; p->run_list.prev = NULL;
if((clone_flags & CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) { p->p_opptr = current; if(!(p->trace & PT_PTRACED)) p->p_pptr = current; }
p->p_cptr = NULL; //wait4()与wait3()函数是一个进程等待子进程完成使命后再继续执行,这个队列为此做准备,这里是做初始化 init_waitqueue_head(&p->wait_childexit); p->vfork_sem = NULL; spin_lock_init(&p->alloc_lock); //表示新建进程尚未收到任何信号 p->sigpending = 0; init_sigpending(&p->sigpending); //对子进程待处理信号队列和有关结构成分初始化 p->it_real_value = p->it_virt_value = p->it_prof_value = 0; p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0; //初始化时间数据成员 init_timer(&p->real_timer); p->real_timer.data = (unsigned long)p; p->leader = 0; p->tty_old_pgrp = 0; p->times.tms_utime = p->times.tms_stime = 0; //对进程各种记时器的初始化 p->times.tms_curtime = p->times.tms_cstime = 0;
#ifdef CONFIG_SMP { int i; p->has_cpu = 0; p->processor = current->processor; for(i = 0; i < smp_num_cpus; i++) p->per_cpu_utime[i] = p->per_cpu_stime[i] = 0; spin_lock_init(&p->sigmask_lock); } #endif //多处理器相关
p->lock_death = -1; p->start_time = jiffies; //对进程初始时间的初始化,jeffies是时钟中断记录的记时器,到这里task_struct基本初始化完毕 retval = -ENOMEM; //copy_files是复制已打开文件的控制结构,但只有才clone_flags中CLONE_FILES标志才能进行,否则只是共享 if(copy_files(clone_flags,p)) goto bad_fork_cleanup;
if(copy_fs(clone_flags, p)); goto bad_fork_cleanup_files;
//和上面一样,这里是对信号的处理方式 if(copy_sighand(clone_flags, p)) goto bad_fork_cleanpu_fs;
//内存,copy_mm的代码参考:http://civilnet.cn/bbs/topicno/71184 if(copy_mm(clone_flags, p)) goto bad_fork_cleanup_sighand; //到这里所有需要有条件复制的资源全部结束
//4个资源中,还剩系统堆栈资源没有复制, //copy_thread源代码参考:http://civilnet.cn/bbs/topicno/71185 retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if(retval) goto bad_fork_cleanup_sighand;
p->semundo = NULL; p->parent_exec_id = p->self_exec_id; //parent_exec_id父进程的执行域 p->swappable = 1;//表示本进程的页面可以被换出 //将父进程传入的信号SIGCHLD放入exit_signal,用来被强行终止时发送 p->exit_signal = clone_flags & CSIGNAL; p->pdeath_signal = 0; p->counter = (current->counter + 1) >> 1; current->counter >>= 1;//父进程的分配的时间额被分成2半
if (!current->counter) current->need_resched = 1; //让父子进程各拥有时间的一半
retval = p->pid; p->tgid = retval; INIT_LIST_HEAD(&p->thread_group); write_lock_irq(&tasklist_lock);
if (clone_flags & CLONE_THREAD) { p->tgid = current->tgid; list_add(&p->thread_group, ¤t->thread_group); }
SET_LINKS(p); //将子进程的PCB放入进程队列,让它可以接受调度 hash_pid(p); //将子进程放入hash表中 nr_threads++; write_unlock_irq(&tasklist_lock);
if (p->ptrace & PT_PTRACED) send_sig(SIGSTOP, p, 1); //唤醒新进程放入就绪队列,等待调度,返回 wake_up_process(p); ++total_forks;
fork_out:
if ((clone_flags & CLONE_VFORK) && (retval > 0)) down(&sem); //这里就是达到扣留一个进程的目的
return retval; } ************************************************************************ 从上面的代码可以看出,clone的工作相比于fork少了地址空间、文件句柄、信号量等的拷贝,也就是线程的地址空间、文件句柄、信号量等是共享父进程的,这也是gemfield本文最开始处的背景。
下面用2个简单的程序来演示下:
第一个:fork.c,创建进程
***********************************************
#include <stdio.h> #include <unistd.h> int main(int argc,char **argv) { int gemfield=3; int ret = fork(); printf(“gemfield do fork…\n”); scanf(“%d”,gemfield); } ************************************************ gcc fork.c -o fork ./fork & ps -ef xH 输出:
UID PID PPID C STIME TTY STAT TIME CMD …… gemfield 21776 21775 0 14:20 pts/2 Ss 0:00 -bash gemfield 22015 21533 0 14:42 pts/1 S+ 0:00 ./fork gemfield 22016 22015 0 14:42 pts/1 S+ 0:00 ./fork gemfield 22018 21776 0 14:42 pts/2 R+ 0:00 ps -ef xH ……
第二个:clone.c ,创建线程 **************************************************** #include <stdio.h> #include <pthread.h> int gemfield =0; void civilnet_cn(){ printf(“gemfield do clone***\n”); scanf(“%d”,gemfield); }
int main(int argc,char **argv) { pthread_t tid; int ret = pthread_create(&tid,NULL,civilnet_cn,NULL); printf(“gemfield do clone…\n”); scanf(“%d”,gemfield); } **************************************************** gcc clone.c -lpthread -o clone ./clone& ps -ef xH 输出:
UID PID PPID C STIME TTY STAT TIME CMD …… gemfield 21533 21530 0 12:10 pts/1 Ss 0:00 -bash gemfield 21952 21533 0 14:30 pts/1 Sl+ 0:00 ./clone gemfield 21952 21533 0 14:30 pts/1 Sl+ 0:00 ./clone gemfield 21984 21776 0 14:41 pts/2 R+ 0:00 ps -ef xH ……
看出区别了吗?两个线程的父进程都是bash,但自己的pid是一样的。
最后用一个比喻来总结下: 1、一个进程就好比一个房子里有一个人; 2、clone创建线程就相当于在这个房子里添加一个人; 3、fork创建进程就相当于再造一个房子,然后在新房子里添加一个人;
有了上面的比喻后,我们就清楚很多了: 1、线程之间有很多资源可以共享:比如厨房资源、洗手间资源、热水器资源等; 2、而对于进程来说,一个概念就是进程间通信(你要和另外一个房子里的人通信要比一个房子里的两个人之间通信复杂); 3、线程之间因为共享内存,所以通过一个全局的变量就可以交换数据了; 4、但与此同时,对于线程来说,又有新的概念产生了:一个人使用洗手间的时候,得锁上以防止另一个人对洗手间的访问;一个人(或几个人)睡觉的时候,另外一个人可以按照之前约定的方式来叫醒他;热水器的电源要一直开着,直到想洗澡的人数减为0;
上面的概念,在gemfield的后文中术语化的时候,你就不会再觉得很深奥或者枯燥了。