当前位置: 首页 > 科技观察

说说Linux的进程控制

时间:2023-03-18 15:32:54 科技观察

简介在上一篇介绍Linux的文章中,介绍了Linux的相关概念,包括进程资源、进程状态、进程属性。在本教程中,我们将重点介绍Linux进程管理的内容,包括Linux进程的创建、进程的终止以及进程的等待。Linux进程创建函数fork现有进程可以调用fork函数创建新进程:#includepid_tfork(void);/*返回值:子进程返回0,父进程返回子进程ID;ifOnerror,return-1*/fork创建的新进程称为子进程。fork函数被调用一次,但返回两次。两者返回的区别在于子进程的返回值为0,而父进程的返回值为新创建的子进程的进程ID。创建子进程的过程大概是这样的:调用系统调用fork后,有一个子进程,fork创建子进程就是以父进程为模板。以下是创建进程的fork函数示例:intmain(intargc,char**argv){printf("Iamprocess!\r\n");pid_tid=fork();if(id<0){printf("forkererror\r\n");}elseif(id==0){printf("Iamchildprocessandmyidis:%d,myparentidis:%d\r\n",getpid(),getppid());sleep(3);}else{printf("Iamparentprocessandmyidis:%d\r\n",getpid());sleep(3);}printf("Nowyoucanseeme!\r\n");sleep(3);return0;}代码运行结果如下:当使用fork创建子进程时,内核所做的工作是:分配一个新的内存块,并向子进程描述进程的数据结构并将父进程的部分数据结构内容复制到子进程中,将子进程和子进程添加到系统进程列表中。fork返回并启动scheduler调度。需要注意的是:fork前父进程独立运行,fork后父子两个执行流程分开运行。而fork之后,调度器决定子进程运行的顺序,获取父进程的数据空间、堆、栈的副本。请注意,这是子进程拥有的副本。父进程和子进程不共享这些内存部分,但是由于fork之后经常跟在exec之后,所以今天的许多实现不会执行父进程的数据段、堆和堆栈的完整复制。相反,使用写时复制。技术,这些区域由父进程和子进程共享,内核将它们的访问权限更改为只读。copy-on-write的原理在介绍copy-on-write的原理之前,首先要了解虚拟内存和物理内存这两个概念:物理内存:即电脑的记忆棒。如果电脑安装了2GB的内存条,那么系统就有0~2GB的物理内存空间。虚拟内存:虚拟内存是由软件模拟出来的。例如在32位操作系统下,每个进程占用4GB的虚拟内存空间。应用程序使用虚拟内存,虚拟内存必须映射到物理内存。可以使用,如果没有映射到虚拟内存地址,会导致pagefault异常。下面是虚拟内存和物理内存映射的示意图:从上面的示意图可以看出,引入虚拟内存的概念后,两个进程的同一个虚拟内存地址可以映射到不同的物理地址。介绍完虚拟内存和物理内存之后,我们再介绍一下copy-on-write的基本原理。在前面的介绍中,我们知道虚拟内存必须映射到物理内存才能使用。如果不同进程的虚拟内存地址映射到同一个物理内存地址,那么就实现了共享内存机制。即如下图所示:从上面的示意图可以看出,进程A的虚拟内存空间和进程B的虚拟内存空间映射到同一个物理内存地址,所以在修改虚拟内存的时候进程A的数据空间,进程B的虚拟内存中的数据也会随之变化。基于这样的原理,实现了写时复制的机制:写时复制的一个过程大致如下:在创建子进程时,复制父进程的虚拟内存和物理内存的映射关系processtochildprocess,andsetthememoryasRead-only当子进程或父进程修改内存数据时,会触发copy-on-write机制,将原来的内存页复制到新的内存页,并重置其内存映射关系将父子进程的内存读写权限设置为读写。image-20210627103516488但是此时只能读取内存。如果父进程或者子进程对内存进行写操作,会触发缺页异常,在缺页异常处理中会复制物理内存,重新Map其内存映射关系,即copy-机制写时。回过头来看,对于fork,有以下两种用法:父进程想复制自己,让父进程和子进程同时执行不同的代码段,这在网络服务进程中很常见,而父进程进程等待客户的服务请求。当这样的请求到达时,父进程调用fork让子进程处理请求。父进程继续等待下一个服务请求。一个进程想要执行一个不同的程序,在这种情况下,exec在子进程调用fork返回后立即被调用。调用fork失败的主要原因有:系统中已经有太多进程实际用户ID的进程总数超过系统限制进程终止正常终止方式有5种,异常终止方式有3种。先描述以下五种正常的终止方式:在main函数中执行return语句,相当于调用exit。调用退出函数Call_exit或_Exit,对于_Exit,目的是为进程提供一种终止方式,而无需运行终止处理程序或信号处理程序。进程的最后一个线程在启动例程中执行返回语句。但是,线程的返回值不作为进程的返回值。当最后一个线程从它的启动例程返回时,进程以终止状态0返回。进程的最后一个线程调用pthread_exit函数,和以前一样,进程的终止状态始终为0。三种异常终止如下:调用abort,产生SIGABRT信号,是下一次异常终止的特例。当一个进程接收到一些信号时最后一个进程响应一个“取消”请求无论进程如何终止,它最终都会执行内核中的同一段代码。此代码关闭相应进程的所有打开的描述符,释放它使用的内存。函数wait和waitpid调用wait和waitpid将做以下事情:如果所有子进程仍在运行,则阻塞如果子进程已终止并正在等待父进程获取其终止状态,则获取子进程的终止状态status并在没有任何子进程时立即返回错误。如果进程在接收到SIGABRT信号时调用wait,我们希望wait立即返回,但如果在随机时间点调用wait,进程可能会阻塞。下面是这两个函数的原型:#includepid_twait(int*statloc);pid_twaitpid(pid_tpid,int*statloc,intoptions);/*两个函数的返回值:如果成功,返回进程号;失败则返回0或-1*/除了这两个函数外,还有其他类似调用的函数,这里不再赘述。竞争条件当多个进程试图对共享数据执行某些操作时,就会出现竞争条件,最终结果取决于进程运行的顺序。如果分叉后的某些逻辑明确或隐含地取决于父代或子代在分叉后先运行,那么分叉函数是竞争条件的活跃滋生地。如果进程希望等待子进程终止,则它必须调用其中一个等待函数。如果进程希望等待其父进程终止,则循环形式为:while(getppid()!=1)sleep(1);这种循环形式称为轮询,它的问题是浪费CPU时间,因为调用者每1s被唤醒一次,然后进行条件测试,为了避免raceconditions和轮询,多个进程之间需要是某种形式的信号发送和接收方法。下次详细描述。函数exec使用fork函数创建新的子进程后,子进程往往需要调用一个exec函数来执行另一个程序。当一个进程调用其中一个exec函数时,该进程执行的程序将被新程序完全替换。通俗的理解这句话,就是说在Window平台下,我们可以通过双击来运行可执行程序,使可执行程序成为一个进程;但是,在Linux平台下,我们可以运行./来制作一个可执行程序Executingaprogrambecomesaprocess。如果我们已经在运行一个程序(进程),如何在进程内部启动一个外部程序,内核将外部程序读入内存使其作为进程执行呢?这是通过exec函数族实现的。exec函数族,顾名思义,就是一个函数族。在Linux中,没有exec()函数。exec指的是一组函数:#includeintexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);intexecve(constchar*path,char*constargv[],char*constenvp[]);其中,只有execve()是真正的系统调用,其他的都是在此基础上封装的库函数。当进程调用exec函数时,进程完全被新程序替换,新程序从其main函数开始执行。因为调用exec并没有创建新的进程,所以前后的进程ID(当然还有父进程号、进程组号、当前工作目录……)没有变化。exec只是将当前进程的文本、数据、堆和堆栈段替换为另一个新程序(进程替换)。下面给出一个关于execl()的示例代码:#include#includeintmain(intargc,char*argv[]){printf("beforeexec\n\n");/*/bin/ls:外部程序,这里是/bin目录下的ls可执行程序,必须带上路径(相对或绝对)ls:无意义,如果需要给这个外部程序传递参数,必须在这里写一个字符串,至于字符串的内容,任意-a,-l,-h:传递给外部程序的参数lsNULL:这个一定要写,表示传递给外部程序的参数结束ls*/execl("/bin/ls","ls","-a","-l","-h",NULL);//如果execl()执行成功,下面不能执行,因为当前进程已经被执行的替换了lsperror("execl");printf("afterexec\n\n");return0;}下面是代码执行的结果:总结本次内容分享到此结束,主要描述Linux进程管理的相关内容,包括Linux进程创建,进程挂起,进程等待等等,在接下来的内容中,我们会重点分享进程间通信的相关内容,每周一篇,坚持下去~