分类: LINUX
2012-06-01 17:14:27
1 > 线程和进程的差别 线程机制支持并发程序设计技术,在多处理器上能真正保证并行处理。而在linux实现线程很特别,linux把所有的线程都当作线程实现。 linux下线程看起来就像普通进程(只是该进程和其他进程共享资源,如地址空间)。上述机制与Microsoft windows或是Sun Solaris实现 差异很大。这些系统提供专门支持线程机制(轻量级进程)。 在现代操作系统中,进程支持多线程。进程是资源管理及分配的最小单元;而线程是程序执行的最小单元。一个进程的组成实体可以分为两大部分:线程集和资源集。进程中的线程是动态的对象,代表了进程指令的执行过程。资源,包括地址空间、打开的文件、用户信息等等,由进程内的线程共享。线程有自己的私有数据:程序计数器,栈空间以及寄存器。 现实中有很多需要并发处理的任务,如数据库的服务器端、网络服务器、大容量计算等。 如果采用多进程的方法,则有如下问题: 1> fork一个子进程的消耗是很大的,fork是一个昂贵的系统调用。 2> 各个进程拥有自己独立的地址空间,进程间的协作需要复杂的IPC技术,如消息传递和共享内存等。 线程推广了进程的概念,使一个进程可以包含多个活动(或者说执行序列等等)。多线程的优点和缺点实际上是对立统一的。使用线程的优点在于: 1> 改进程序的实时响应能力; 2> 更有效的使用多处理器,真正的并行(parallelism); 3> 改进程序结构,具备多个控制流; 4> 通讯方便,由于共享进程的代码和全局数据; 5> 减少对系统资源的使用。对属于同一个进程的线程之间进行调度切换时不需要调用系统调用,因此将减少额外的消耗,往往一个进程可以启动上千个线程也没有什么问题。 缺点在于: 由于各线程共享进程的地址空间,因此可能会导致竞争,因此对某一块有多个线程要访问的数据需要一些同步技术。 2 > 线程的分类 2.1 内核线程 Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。内核需要多个执行流并行,为了防止可能的阻塞,多线程化是必要的。内核线程就是内核的分身,一个分身可以处理一件特定事情。Linux内核使用内核线程来将内核分成几个功能模块,像kswapd、kflushd等,这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。这与用户线程是不一样的。 2.2用户线程 用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,还是进程本身,内核并不知道用户线程的存在。 由于Linux内核没有轻量级进程(线程)的概念,因此不能独立的对用户线程进行调度,而是由一个线程运行库来组织线程的调度,其主要工作在于在各个线程的栈之间调度。如果一个进程中的某一个线程调用了一个阻塞的系统调用,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会。因此linux使用了异步I/O机制。 3 > 内核线程 内核线程(thread)或叫守护进程(daemon),在操作系统中占据相当大的比例,当Linux操作系统启动以后,尤其是X window也启动以后,你可以用”ps -ef”命令查看系统中的进程,这时会发现很多以”d”结尾的进程名,确切说名称显示里面加 "[]"的,这些进程就是内核线程。系统的启动是从硬件->内核->用户态进程的,pid的分配是一个往前的循环的过程,所以随系统启动的内核线程的pid往往很小。 UID PID PPID C STIME TTY TIME CMD root 1 0 0 09:42 ? 00:00:01 /sbin/init root 2 0 0 09:42 ? 00:00:00 [kthreadd] root 3 2 0 09:42 ? 00:00:00 [migration/0] root 4 2 0 09:42 ? 00:00:00 [ksoftirqd/0] root 5 2 0 09:42 ? 00:00:00 [watchdog/0] root 6 2 0 09:42 ? 00:00:00 [events/0] root 7 2 0 09:42 ? 00:00:00 [khelper] root 40 2 0 09:42 ? 00:00:00 [kblockd/0] root 64 2 0 09:42 ? 00:00:00 [kseriod] root 110 2 0 09:42 ? 00:00:00 [pdflush] root 111 2 0 09:42 ? 00:00:00 [pdflush] root 112 2 0 09:42 ? 00:00:00 [kswapd0] root 151 2 0 09:42 ? 00:00:00 [aio/0] root 1327 2 0 09:42 ? 00:00:00 [ksuspend_usbd] root 1330 2 0 09:42 ? 00:00:00 [khubd] root 1362 2 0 09:42 ? 00:00:00 [ata/0] root 1370 2 0 09:42 ? 00:00:00 [ata_aux] root 1428 2 0 09:42 ? 00:00:00 [scsi_eh_0] root 1430 2 0 09:42 ? 00:00:01 [scsi_eh_1] root 2304 2 0 09:42 ? 00:00:00 [kjournald] root 2507 1 0 09:42 ? 00:00:00 /sbin/udevd --daemon root 2787 2 0 09:42 ? 00:00:00 [kgameportd] root 3698 2 0 09:42 ? 00:00:00 [kapmd] root 3872 2 0 09:42 ? 00:00:00 [kjournald] 3.1 内核线程 events 处理内核事件 很多软硬件事件(比如断电,文件变更)被转换为events,并分发给对相应事件感兴趣的线程进行响应 ksoftirqd 处理软中断 硬件中断处理往往需要关中断,而这个时间不能太长,否则会丢失新的中断。所以中断处理的很大一部分工作移出,转给任劳任怨的ksoftirqd在中断之外进行处理。比如一个网络包,从网卡里面取出这个过程可能需要关中断,但是TCP/IP协议处理就不必关中断了 kblockd 管理磁盘块读写 kjournald Ext3文件系统的日志管理 通常每个 _已mount_ 的 Ext3分区会有一个 kjournald看管,各分区的日志是独立 pdflush dirty内存页面的回写 太多dirty的页面意味着风险,比如故障时候的内容丢失,以及对突发的大量物理内存请求的响应(大量回写会导致糟糕的响应时间) kswapd 内存回收 确保系统空闲物理内存的数量在一个合适的范围 aio 代替用户进程管理io 用以支持用户态的AIO 3.2 用户进程 crond 执行定时任务 init 为内核创建的第一个线程。引导用户空间服务,管理孤儿线程,以及运行级别的转换 mingetty 等待用户从tty登录 bash shell进程,一个命令行形式的系统接口;接受用户的命令,并进行解释、执 sshd ssh登录、文件传输、命令执行 等操作的服务进程 klogd 从内核信息缓冲区获取打印信息。内核在发现异常的时候,往往会输出一些消息给用户,这个对于故障处理很有用 syslogd 系统日志进程 udevd 支持用户态设备操作 (userspace device) 4> 创建线程 clone_flags 最初的进程定义都包含程序、资源及其执行三部分,其中程序通常指代码,资源在操作系统层面上通常包括内存资源、IO资源、信号处理等部分,而程序的执行通常理解为执行上下文,包括对cpu的占用,后来发展为线程。在线程概念出现以前,为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,数据内存,甚至代码。应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻量进程还是普通进程。在内核中,clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork(): 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) 其中的clone_flags取自以下宏的"或"值: 在 do_fork()中,不同的clone_flags将导致不同的行为,对于LinuxThreads,它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)参数来调用clone()创建"线程",表示共享内存、共享文件系统访问计数、共享文件描述符表,以及共享信号处理方式。本节就针对这几个参数,看看Linux内核是如何实现这些资源的共享的。 1.CLONE_VM do_fork()需要调用copy_mm()来设置task_struct中的mm和active_mm项,这两个mm_struct数据与进程所关联的内存空间相对应。如果do_fork()时指定了CLONE_VM开关,copy_mm()将把新的task_struct中的mm和active_mm设置成与 current的相同,同时提高该mm_struct的使用者数目(mm_struct::mm_users)。也就是说,轻量级进程与父进程共享内存地址空间,由下图示意可以看出mm_struct在进程中的地位: 2.CLONE_FS task_struct 中利用fs(struct fs_struct *)记录了进程所在文件系统的根目录和当前目录信息,do_fork()时调用copy_fs()复制了这个结构;而对于轻量级进程则仅增加 fs->count计数,与父进程共享相同的fs_struct。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前目录、根目录等信息都将直接影响到其他线程。 3.CLONE_FILES 一个进程可能打开了一些文件,在进程结构 task_struct中利用files(struct files_struct *)来保存进程打开的文件结构(struct file)信息,do_fork()中调用了copy_files()来处理这个进程属性;轻量级进程与父进程是共享该结构的,copy_files() 时仅增加files->count计数。这一共享使得任何线程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程。 4.CLONE_SIGHAND 每一个Linux进程都可以自行定义对信号的处理方式,在task_struct中的sig(struct signal_struct)中使用一个struct k_sigaction结构的数组来保存这个配置信息,do_fork()中的copy_sighand()负责复制该信息;轻量级进程不进行复制,而仅仅增加signal_struct::count计数,与父进程共享该结构。也就是说,子进程与父进程的信号处理方式完全相同,而且可以相互更改。 尽管Linux支持轻量级进程,但并不能说它就支持核心级线程,因为Linux的"线程"和"进程"实际上处于一个调度层次,共享一个进程标识符空间,这种限制使得不可能在Linux上实现完全意义上的POSIX线程机制,因此众多的Linux线程库实现尝试都只能尽可能实现POSIX的绝大部分语义,并在功能上尽可能逼近。 |