Linux进程管理和进程函数

进程管理 
和文件一样,进程是Linux系统最基本的抽象之一。 
1、进程ID: 
每一个进程都有一个唯一的标识:进程ID。虽然进程ID是唯一的,但进程终止后,id会被其他进程重用。 
许多Linux都提供了延迟重用的功能,以防止新进程被误认为是旧进程。 


有一些特殊的进程: 
id为0的进程--idle进程或者叫做swapper,通常是一个调度进程。 
id为1的进程--内核booting之后执行的第一个进程。init进程一般执行的是init程序。 
Linux通常尝试执行以下init程序: 
1、/sbin/init: 偏向、最有可能是init程序的地方。 
2、/etc/init: 另一个很有可能是init程序的地方。 
3、/bin/init: 有可能是init进程的地方。 
4、/bin/sh:如果内核找不到init进程,就执行该bourne shell。

 
init进程是一个用户级进程,但是需要执行者有超级用户权限。

 (整个linux系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的0号进程,它是所有进程的祖先。由0号进程创建1号进程 (内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve() 运行可执行程序init,并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2 号...的若干终端注册进程getty。

        每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。

2、获得进程ID和父进程的ID: 

 

  1. #include <sys/types.h>  
  2. #include <unistd.h>  
  3. pid_t getpid(void);  
  4. pit_t getppid(void);  


pid_t是个抽象类型,在linux中pid_t一般是一个int类型,在<sys/types.h>定义。 
但把pid_t当做int类型,不具有可移植性。 
例子: 

 

  1. printf("My pid=%d\n",  getpid());  
  2. printf("Parent's pid=%d\n",  getppid());  


我们可以把pid_t比较安全的当做int类型,虽然这违反了抽象类型的意图和可移植性。 

2、创建一个进程fork: 
一个已经存在的进程可以通过fork创建其他进程: 

 

  1. #include <unistd.h>    
  2. pid_t fork(void);  


新创建的进程被称为子进程,这个函数被调用一次但是被返回两次。在子进程返回0,父进程返回子进程的pid_t。
之所以在父进程返回子进程的id,是由于父进程可以有多个子进程,并且没有提供获得所有子进程的方法。在子进程中返回0,是因为子进程只有一个父进程,并且可以通过getppid获得。 
fork被调用之后,父进程和子进程都开始执行fork之后的程序语句。子进程是父进程的一个拷贝,拷贝了父进程的数据空间,堆,栈,它们共享text段。 
当前fork的实现并不是拷贝父进程的数据、堆、栈,而是使用了copy-on-write技术,这是因为fork之后通常会调用exec。 
如果它们修改了这些区域,那么内核就会把相应的那部分内存进行拷贝。

 
子进程和父进程有以下不同: 
1)进程id不同 
2)  fork的返回值不同 
2)子进程的父进程id(getppid)设置为父进程的id,它们的父进程不同 
3)子进程的资源统计归为0 
4)任何pending的signals被清空,不会被子进程继承 
5)任何获得的文件锁都不会被子进程继承。 
相同的: 
1)打开文件 
2)real user ID,real group ID,effective user ID,effective group ID (用户或用户组)
3)进程的group ID (进程组)
4)Session ID (会话:用户登录就是一个会话的开始)
5)控制终端 
6)set-user-ID和set-group-ID
7)当前工作目录 
8)Root目录 
9)文件mode创建的掩码 
10)信号掩码和dipositions
11)打开文件的close-on-exec flag
12)环境变量 
13)附加进去的共享内存段 
14)内存映像 
15)资源限制 
如果失败返回-1。 
例子: 

 

  1. pid_t pid;  
  2. pid = fork();  
  3. if(pid > 0){  
  4.     printf("I am the parent of pid=%d\n",pid);  
  5. }else if(!pid){  
  6.     printf("I am the baby!\n");  
  7. }else if(pid == -1){  
  8.     perror("fork");  
  9. }  

perror (s)用来将上一个函数发生错误的原因输出到标准设备 (stderr) 。参数s字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno 的值来决定要输出的字符串。在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和现在的errno所对应的错误一起输出。


fork经常和exec在一起使用: 

  1. pid_t pid;   
  2. pid = fork();  
  3. if(pid == -1)  
  4.     perror("fork");    
  5. if( ! pid ){  
  6.     const char *args[] = {"windlass",NULL};  
  7.     int ret;  
  8.     ret = execv("/bin/windlass",  args);  
  9.     if(ret == -1){  
  10.         perror("execv");  
  11.         exit(EXIT_FAILURE);  
  12.     }  
  13. }  


使用fork的场景: 
1)当一个进程想复制自己以便父子进程可以同时执行不同部分的代码。比如一个网络的服务器,父进程等待从客户端发来的请求,当请求到来时,父进程调用fork,让新创建的子进程处理请求,父进程继续等待客户端发来的请求。 
2)当一个进程想执行不同的程序。比如shell,子进程在fork返回之后执行了exec。 

3、vfork: 
在copy-on-write技术使用之前,Unix的设计者认为fork之后执行exec浪费了地址空间的拷贝,BSD开发者实现了vfork系统调用: 

 

  1. #include <sys/types.h>  
  2. #include <unistd.h>  
  3. pid_t vfork(void);  


vfork和fork行为一样,除了子进程要立即调用exec函数或者执行exit退出。vfork系统调用避免了地址空间和页表的拷贝,通过挂起父进程直到子进程终止或者执行一个二进制的进程映像。 
vfork的例子: 

 

  1. #include <sys/types.h>  
  2. #include <unistd.h>  
  3. int glob = 6;  
  4. int main(){  
  5.     int var;  
  6.     pid_t pid;  
  7.     var = 88;  
  8.     printf("before vfork\n");  
  9.     if((pid = vfork()) < 0){  
  10.         perror("vfork");  
  11.         return 1;  
  12. }else if(pid == 0){  
  13.         glob++;  
  14.         var++;  
  15.         _exit(0);  
  16. }  
  17.   
  18.     printf("pid=%d",glob=%d,var=%d\n",getpid(), glob, var);  
  19.     exit(0);  
  20. }  


输出:pid=2903,glob=7,var=89。在子进程里面增加变量会反映到父进程中,因为他们共享同一进程空间。 


4、终止进程: 
POSIX和C89都定义了终止当前进程的标准函数: 

 

  1. #include <stdlib.h>    
  2. void exit(int status);  


调用exit会执行一些关闭操作步骤,然后指示内核终止进程。 
status表示进程终止的状态。EXIT_SUCESS和EXIT_FAILURE,被定义为一种可移植的方式来表示成功和失败。 
在终止之前要做一些关闭的步骤: 
1)调用任何注册在atexit()和on_exit()的方法,和注册的顺序相反。 
2)flush所有打开的I/O流 
3)删除进程由tmpfile()函数创建的临时文件。 
4)执行完这些步骤之后,调用_exit(),让内核来处理剩余的终止操作: 

 

  1. #include <unistd.h>  
  2. void _exit(int status);  


当进程终止后,内核清空了进程申请的所有资源。 
程序可以直接调用_exit,但是很多程序需要执行一些清理操作,比如flush标准输出流。但是vfork用户必须使用_exit终止,因为父子进程共享一个地址空间,exit执行一些I/O清理工作可能把父进程的文件描述流关闭,导致父进程I/O失败。 

exit和_exit函数都是用来终止进程的。当程序执行到exit或_exit时,系统无条件的停止剩下所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。但是,这两个函数是有区别的。

_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;exit()函数则在这一基础上做了一些包装。在执行退出之前加了若干道工序。

exit()函数与_exit()函数最大区别就在于:exit()函数在调用_exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux的标准函数库中,有一种被称作“缓冲I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也给编程代来了一点儿麻烦。比如有一些数据,认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区的数据就会丢失。因此,要想保证数据的完整性,就一定要使用exit()函数。

exit的函数声明在stdlib.h头文件中。_exit的函数声明在unistd.h头文件当中。下面的实例比较了这两个函数的区别。printf函数就是使用缓冲I/O的方式,该函数在遇到''\n''换行符时自动的从缓冲区中将记录读出。实例就是利用这个性质进行比较的。

 

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. int main(void)
  4. {   printf("Using exit...\n");
  5.       printf("This is the content in buffer");
  6.       exit(0);
  7. }

 

输出信息:Using exit...This is the content in buffer

 

  1. #include <unistd.h>
  2. #include <stdio.h>
  3. int main(void)
  4. {    printf("Using exit...\n");
  5.         printf("This is the content in buffer");
  6.         _exit(0);
  7. }

 

则只输出:Using exit…

 

说明:

在一个进程调用了exit之后,该进程并不会马上完全消失,而是留下一个称为僵尸进程(Zombie)的数据结构。僵尸进程是一种非常特殊的进程,它几乎已经放弃了所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其它进程收集,除此之外,僵尸进程不再占有任何内存空间。

 

5、atexit和on_exit: 
1、atexit: 
注册在进程终止之前回到的函数: 

 

  1. #include <stdlib.h>  
  2. int atexit(void (*function)(void));  


如果进程通过exit或者从main返回终止,则会调用注册到atexit的方法。如果进程调用exec函数,注册函数则被清空(因为这些函数不在新的进程空间存在)。如果信号终止了进程,则注册的函数不会被调用。 
被注册的函数按照逆序执行,如果被注册的函数执行了exit,则会导致无穷递归,如果想提前终止需要使用_exit。atexit支持至少ATEXIT_MAX个注册函数,这个值可以通过sysconf得到。 

  1. long atexit_max;  
  2. atexit_max = sysconf(_SC_ATEXIT_MAX);  
  3. printf("atexit_max=%ld\n", atexit_max);  


atexit例子: 

 

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. void out(void){  
  4.     println("atexit() succeed!\n");  
  5. }  
  6.   
  7. int main(){  
  8.     if(atexit(out))  
  9.         fprintf(stderr,"atexit() failed!\n");  
  10.     return 0;  
  11. }  


2、on_exit: 
on_exit和atexit等价,Linux glibc实现了它: 

(glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。)

  1. #include <stdlib.h>  
  2. int on_exit (void (*function)(int , void *), void *arg);  


但是注册的签名函数不同,原型是: 

 void my_func(int status, void *args);  


status是传到exit的或者从main返回的值。args是传到on_exit的第二个参数。Solaris已经不再支持on_exit,所以最好使用atexit().


6、等待子进程终止: 
当一个进程终止之后,内核向父进程发送一个SIGCHLD。默认这个信号被忽略,进程可以通过singnal()或者sigaction()系统调用来处理这个信号。父进程希望得到子进程终止的更多信息,比如返回值,甚至显式的等待这个事件的到来,这就是wait或者waitpid,

 

它们可以做: 
1)阻塞,如果子进程仍然在执行。 
2)立即返回,包含子进程的终止状态,如果一个子进程终止,等待它的终止状态被获取。 
3)返回错误,如果它没有子进程。 

 

  1. #include <sys/wait.h>  
  2. pid_t wait(int *statloc);  
  3. pid_t waitpid(pid_t pid, int *statloc, int options);  


这两个函数的不同之处: 
1)wait会阻塞调用者,直到一个子进程终止,而waitpid有一个设置阻塞的选项。 
2)waitpid不是等待第一个终止的子进程,他有一些选项来控制进程的等待。 


如果一个进程终止,其父进程没有等待他终止(调用wait函数可以回收僵尸),或者先于子进程结束,该子进程就成为僵尸进程,僵尸进程的父进程会置为init进程(PID=1),init周期性的调用wait来回收僵尸。 

wait()要与fork()配套出现,如果在使用fork()之前调用wait(), wait()的返回值则为-1, 正常情况下wait()的返回值为子进程的PID。
子进程的结束状态会保存在statloc指针中,如果不关心结束状态,可以直接传一个NULL。 


POSIX指定了通过一系列宏来获得终止的状态。 

 

  1. #include <sys/wait.h>  
  2.  
  3. int WIFEXITED(status);  //子进程正常结束
  4. int WIFSIGNALED(status);  //被异常终止
  5. int WIFSTOPPED(status);  //被暂停
  6. int WIFCONTINUED(status);  //继续运行时
  7. int WEXITSTATUS(status);  //获取正常结束时的返回值
  8. int WTERMSIG(status);    //获取异常终止信号
  9. int WSTOPSIG(status);  //获取引起子进程暂停的信号
  10. int WCOREDUMP(status);  //获取子进程异常终止所产生的内核文件


WIFEXITED:如果子进程正常终止,返回true。可以通过WEXITSTATUS得到参数的低8位。 
WIFSIGNALED:如果是信号导致子进程不正常终止,返回true。可以通过WTERMSIG返回信号的号。 
一些UNIX实现定义了WCOREDUMP宏,如果进程dump core来响应信号。 
WIFSTOPPED:如果进程被停止,返回true,通过WSTOPSIG来获得导致子进程停止的信号。 
WIFCONTINUED:如果状态是由已经被continued子进程返回,返回true。 
例子: 

 

  1. #include <unistd.h>  
  2. #include <stdio.h>  
  3. #include <sys/types.h>  
  4. #include <sys/wait.h>   

  

  1. int main(void){  
  2.     int status;  
  3.     pid_t pid;  
  4.       
  5.     if(! fork()){  
  6.         return 1;  
  7.     }  
  8.   
  9.     pid = wait(&status);  
  10.     if(pid == -1)  
  11.         perror("wait");  
  12.     printf("pid=%d\n",pid);  
  13.   
  14.     if(WIFEXITED(status))  
  15.         printf("Normal termination with exit status=%d\n",WEXITSTATUS(status));  
  16.     if(WIFSIGNALED(status))  
  17.         printf("Killed by signal=%d%s\n", WTERMSIG(status), WCOREDUMP(status) );  
  18.     if(WIFSTOPPED(status))  
  19.         printf("Stopped by signal=%d\n",WSTOPSIG(status));  
  20.     if(WIFCONTINUED(status))  
  21.         printf("Continued\n");  
  22.   
  23.     return 0;  
  24. }

  

原型:pid_t waitpid(pid_t pid, int *statloc, int options);
waitpid比wait功能更加强大。 
pid参数指定了要等待的进程id: 
pid>0时, 只等待进程ID等于pid的子进程, 不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束, waitpid就会一直等下去。
pid=-1时, 等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样.
pid=0时,等待同一个进程组中的任何子进程, 如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬.
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

参数options的值有以下几种类型:
如果使用了WNOHANG参数,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
如果使用了WUNTRACED参数,则子进程进入暂停则马上返回,但结束状态不予以理会.
Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果我们不想使用它们,也可以把options设为0,如:ret=waitpid(-1,NULL,0);
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD.

例子: 

 

  1. int status;  
  2. pid_t pid;  
  3. pid = waitpid (1742, &status, WNOHANG);  
  4. if (pid == -1)  
  5.     perror ("waitpid");  
  6. else {  
  7.     printf ("pid=%d\n", pid);  
  8.     if (WIFEXITED (status))  
  9.         printf ("Normal termination with exit status=%d\n",WEXITSTATUS (status));  
  10.     if (WIFSIGNALED (status))  
  11.         printf ("Killed by signal=%d%s\n",WTERMSIG (status), WCOREDUMP (status) ? " (dumped core)" : "");  
  12. }  


7、exec函数: 

一个进程一旦调用exec类函数,它本身就”死亡”了。系统把代码段替换成新的程序代码,废弃原有的数据段和代码段,并为新程序分配新的数据段和代码段,唯一留下的就是进程号。不过,有些exec类函数还允许继承环境变量等信息。
当进程调用exec时,当前进程的镜像被由path标定的程序加载到内存中代替。下面是exec一族函数: 

 

  1. #include <unistd.h>  
  2.   
  3. int execl(const char *path,const char *arg,...);  
  4. int execv(const char *path,char * const  argv[]);      

//argv列表最后一个必须是 NULL  

  1. int execle(const char *path,const char *arg,..., char * const envp[]);  
  2. int execve(const char *path,char *const argv[], char * const envp[]);  
  3. int execlp(const char *filename, const char *arg,...);
  4. int execvp(const char *filename, char *const argv);  


区别:

1)前四个参数path代表路径名,后两个代表文件名(不包含路径信息)。 
如果文件名中包含反斜线,作为一个路径名,否则从PATH环境变量中找可执行的文件。 
如果execlp或者execvp找到了可以执行的文件,但是不是机器可执行的,那么就假设这是一个shell脚本,调用/bin/sh执行该shell脚本。 
2)execl、execle、execlp使用的是参数列表,需要以NULL终止,而其他的几个带v的是一个数组参数,参数数组也要以NULL终止。 
3)execle、execve传递环境列表到新的程序中。 
这里面只有execve是系统调用,其他都是函数。 
例子: 

 

  1. int ret;  
  2.   
  3. ret = execl("/bin/vi", "vi", NULL);  
  4. if(ret == -1)  
  5.     perror("execl");  


下面一个例子,要使用vi打开以及文件编辑: 

 

  1. int ret;  
  2.   
  3. ret = execl("/bin/vi", "vi", "/home/fuliang/books.txt",  NULL);  
  4. if(ret == -1)  
  5.     perror("execl");  


成功调用execl之后,改变的不仅是地址空间和进程映像,而且还改变进程一下属性: 
1)任何pending signals都被丢弃。 
2)任何要捕获的信号都还原成默认的行为。因为信号处理函数不在该进程的地址空间了。 
3)任何的内存锁都被释放。 
4)很多线程属性被设置为默认值。 
5)任何和进程内存相关,包括内存映射文件,都被丢弃。 
6)任何在用户空间存在的包括C语言库,比如atexit的行为被丢弃。 


没有改变的进程属性: 
进程pid,父进程id,优先级,进程所属的用户和组。 
execvp例子: 

 

  1. const char *args[] =

             {"vi", "/home/fuliang/books.txt",  NULL};  

  1. int ret;  
  2.   
  3. ret = execvp("vi", args);  
  4. if( ret == -1 )  
  5.     perror("execvp");  


失败返回-1,并设置errno: 
1)E2BIG:参数列表或者环境变量envp太长。 
2)EACCESS:进程没有搜索path的权限,path不是一个普通文件,目标文件不可执行,文件系统被mounted为不可读。 
3)EFAULT:所给的指针不合法。 
4)EIO:底层的I/O发生错误。 
5)EISDIR:path或者解释器是一个目录。 
6)ELOOP:解析path的时候遇到太多的软链。 
7)EMFILE:调用进程超过了打开文件的限制。 
8)ENFILE:系统打开文件数目超过限制。 
9)ENOENT:path不存在,或者以来的共享库不存在。 
10)ENOEXEC:目标path是一个不合法的二进制文件或者是不同的机器架构。 
11)ENOMEM:没有足够的内核内存来执行新的程序 
12)ENOTDIR:path不是一个目录.
13)ETXTBSY:目标文件正被其他的进程写 
8、Launching and Waiting for a New Process: 
ANSI C和POSIX都定义了一个接口,结合了创建一个进程并且等待它的结束: 

 

  1. #include <stdlib.h>    
  2. int system(const char *command);  


system一般用于执行一个简单的工具或者shell脚本。 
成功返回命令的执行状态,如果command是NULL,则返回非0整数。 
在执行command命令时,SIGCHILD被阻塞,SIGINT和SIGQUIT被忽略。忽略SIGINT和SIGQUIT有几个含义, 
尤其system在一个循环里被执行,这时你需要保证程序检查子进程的状态。 

 

  1. do{  
  2.     int ret;  
  3.       
  4.     ret = system("ls -l");  
  5.     if(WIFSIGNALED(ret) && WTERMSIG(ret) == SIGINT || WTERMSIG(ret) == SIGQUIT))  
  6.         break;  
  7. }while(1);  


使用fork、waitpid简单实现system: 

 

  1. int my_system(const char *cmd){  
  2.     int status;  
  3.     pid_pid;  
  4.   
  5.     pid = fork();  
  6.     if(pid == -1);  
  7.         return -1;  
  8.     else if(pid == 0){  
  9.         const char *argv[4];  
  10.         argv[0] = "sh";  
  11.         argv[1] = "-c";  
  12.         argv[2] = cmd;  
  13.         argv[3] = NULL;  
  14.         execv("/bin/sh", argv);  
  15.         exit(-1);  
  16.     }  
  17.   
  18.     if( waitpid(pid, &status,0) == -1)  
  19.         return -1;  
  20.     else if(WIFEXITED(status))  
  21.         return WEXITSTATUS(status);  
  22.   
  23.   
  24.     return -1;  
  25. }  

 

猜你喜欢

转载自blog.csdn.net/qq_38971487/article/details/90415404