当前位置: 首页 > Linux

Linux进程和线程浅析

时间:2023-04-06 04:58:01 Linux

简介进程和线程是所有程序员都熟悉的概念。简单的说,进程就是一个正在执行的程序,线程就是进程中的一条执行路径。进程是操作系统中的一个基本抽象概念。本文介绍Linux中进程和线程的使用和原理,包括创建和死亡。进程的创建和执行Linux中进程的创建和执行分为fork和exec两个函数,如下代码所示:intmain(){pid_tpid;if((pid=fork()<0){printf("forkerror\n");}elseif(pid==0){//childif(execle("/home/work/bin/test1","test1",NULL)<0){printf("execerror\n");}}//parentif(waitpid(pid,NULL)<0){printf("waiterror\n");}}fork从当前进程创建一个子进程,该函数返回两次,对于父进程,返回子进程的进程号,对于子进程返回0。子进程是父进程的副本,有和父进程一样的数据空间,堆和栈的一份副本,共享代码段。由于子进程通常会调用exec加载其他程序执行,Linux采用了copy-on-write技术,即数据段、堆和栈的副本在fork之后不会被复制,但是对这些内存区域的访问权限变为只读,如果父子进程中的任何一个想要修改这些区域,则对应的内存页会被修改,生成一个新的副本。这是为了提高性能。fork后,父进程先执行或者子进程先执行。肯定的,所以如果需要父子进程同步,往往需要使用进程间通信。fork之后,子进程会继承父进程的很多东西,比如:打开文件的实际用户ID,组用户ID等进程组的当前工作目录信号屏蔽和排列...父子进程的区别在于进程ID不同,子进程不继承父进程的文件锁。子进程的unhandledsignalset为空...fork之后,子进程可以执行不同的代码段,或者使用exec函数执行其他程序。进程描述符进程在运行时,除了加载程序外,还会打开文件,占用一些资源,进入睡眠等其他状态。为了支持进程的运行,操作系统必须有一个数据结构来存储这些东西。在Linux中,一个名为task_struct的结构保存了进程运行时的所有信息,称为进程描述符:structtask_struct{unsignedlongstate;内部优先级;pid_tpid;...}进程描述符完整地描述了一个进程:打开文件、进程地址空间、挂起信号、进程信号等。系统将所有进程描述符放在一个双端循环链表中:进程描述符存放在内存的什么位置?在内核堆栈的末尾。众所周知,进程占用的内存中有一部分是栈,栈主要用于函数调用,但这里所说的栈一般指的是用户空间的栈。事实上,进程也有一个内核堆栈。当一个进程调用系统调用时,进程被困在内核中。这时,内核代表进程执行一个操作。这时候就用到了内核空间中的栈。进程状态进程描述符中的状态描述了进程的当前状态。有以下五种类型:TASK_RUNNING:进程可执行。此时,进程要么正在执行,要么在运行队列中等待调度。TASK_INTERRUPTIBLE:进程正在休眠(阻塞),等待条件满足。如果满足条件或者接收到信号,进程将被唤醒,进入可运行状态被其他进程跟踪,通常用于调试_TASK_STOPPED:进程停止运行,通常是在收到SIGINT、SIGTSTP信号时。fork和vfork使用copy-on-write后,fork的实际开销是复制父进程的页表,为子进程创建唯一的进程描述符。fork到底做了什么来创建一个进程?fork实际上调用了clone,这是一个系统调用。通过给clone传递参数,表示父子进程需要共享资源。clone内部会调用do_fork,do_fork的主要逻辑在copy_process中。大致有以下步骤:创建内核栈和task_struct。这时候他们的值和父进程是一样的。将task_struct中的一些变量,比如统计信息,设置为0。将子进程的状态设置为TASK_UNINTERRUPTIBLE,保证不会投入运行。根据传递给pid的参数分配clone、复制或共享打开的文件、文件系统信息、信号处理函数和进程的地址空间等。除了fork,Linux还有一个类似的函数vfork。它的作用与vfork相同,子进程运行在父进程的地址空间中。但是,父进程会阻塞,直到子进程退出或执行exec。需要注意的是,子进程不能向地址空间写入数据。如果子进程修改数据、进行函数调用或不调用exec,则结果未知。vfork在fork没有copy-on-write技术时有性能优势,现在意义不大。退出进程的运行当有退出时,有8种方式终止进程,其中5种是正常终止:returnfrommaincallexitcall_exitor_Exit最后一个线程从它的启动例程返回从最后一个线程调用pthread_exit异常终止的三种方式:调用abort、接收信号、最后一个线程响应取消请求。exit函数会执行标准的I/O库清理和关闭操作:对所有打开的流调用fclose函数,缓冲区中的所有数据都会被刷新,而_exit会直接陷入内核。看下面的代码:#include#include#includeintmain(){printf("line1\n");printf("第2行");//没有换行符//exit(0)_exit(0);}第二行的输出没有\n。如果最后调用_exit,则只会输出第1行。如果换成exit,第二行会是第2行也输出。进程出口最终会执行系统的do_exit函数。主要步骤如下:删除进程定时器,释放进程占用的页表,递减文件描述符的引用计数,如果某个引用计数为0,则关闭文件并向父进程发送信号,重新为子进程寻找养父,并将进程状态设置为EXIT_ZOMBIE,以调度其他进程。这时候进程的大部分资源都被释放了,不会进入运行状态。但是,一些资源仍然存在,主要是task_struct结构。之所以保留它,是为了给父进程提供信息,让父进程知道子进程的一些信息,比如退出码。需要注意的是,如果父进程不进行任何操作,这些信息会一直留在内存中,成为僵尸进程,占用系统资源,比如下面的代码:intmain(){pid_tpid=fork();如果(pid==0){退出(0);}else{睡眠(10);}}父进程fork出子进程后,子进程立即退出,而父进程进入休眠状态。运行程序,观察进程状态:可以看到第一个进程是父进程,状态为S,表示正在休眠,第二个是子进程,状态为Z,表示僵尸状态(zombie),因为子进程已经退出了,但是task_struct还保存着,等待父进程处理。父进程如何处理?调用等待函数,如本文第一个代码片段所示。当父进程调用wait时,释放子进程的task_struct。如果父进程先结束怎么办?当父进程结束时,会为其子进程寻找新的父进程,不断向上查找,最终成为init进程的子进程。init子进程会负责调用wait来释放子进程的剩余信息。线程上面介绍了Linux中的进程,那么线程呢?网上有一些说法,Linux中没有真正的内核线程,线程是以进程的形式实现的,只是它们之间共享内存。这种说法有一定道理,但并不完全准确。一开始,Linux不支持线程。后来出现了线程库LinuxThreads,但是它有很多问题,主要是不兼容POXIS标准。从Linux2.6开始,Linux中使用了一个新的线程库NPTL(NativePOSIXThreadLibrary)。NPTL中线程的创建也是通过clone实现的,线程的特性由以下参数表示:CLONE_VM|克隆文件|克隆文件系统|克隆信号|克隆线程|克隆设置|CLONE_PARENT_SETTID|共享同一个进程地址空间CLONE_FILES:所有线程共享进程的文件描述符列表CLONE_THREAD:所有线程共享同一个进程ID,父进程IDNPTL实现的线程库是用户线程到内核线程的1:1映射,以及内核为了实现POSIX线程标准也做了一些改变,比如信号的处理。所以说Linux内核根本不区分进程和线程,甚至不知道线程的存在是不准确的。线程共享代码段、堆、打开的文件等,线程的私有部分有以下内容:线程ID寄存器错误码(errno)堆栈信号屏蔽...总结Linux中进程和线程的使用是必须的-有程序员的技能,如果你能理解一些实现原理,你就可以更轻松地使用它。本文介绍了Linux中进程的创建、执行和消亡,也简要说明了线程的实现及其与进程的关系。进程和线程要研究的内容比较多,比如进程调度,进程和线程之间的通信等等。参考《UNIX 环境高级编程》《Linux 内核设计与实现》