linux学习---进程控制(fork,vfork,popen,exec,system)

来源:互联网 发布:netflix ribbon源码 编辑:程序博客网 时间:2024/06/04 18:49

一.进程的概念

进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。可以认为进程是一个程序的一次执行过程。

二.进程和程序的区别

程序时静态的,它是一些保存在磁盘上得指令的有序集合,没有任何执行的概念。

进程是一个动态的概念,它是程序执行的过程,包括创建、调度和     消亡。

三.进程控制

1.  fork函数

函数原形

#include <unistd.h>

pid_t fork(void);

fork()函数用于从已存在的进程中创建一个新的进 程,新的进程称为子进程,而原进程称为父进程,fork ()的返回值有两个,子进程返回0,父进程返回子进程的进程号,进程号都是非零的正整数,所以父进程返回的值一定大于零,在pid=fork();语句之前只有父进程在运行,而在pid=fork();之后,父进程和新创建的子进程 
都在运行,所以如果pid==0,那么肯定是子进程,若pid =0 (事实上肯定大于0),那么是 父进程在运行。而我们知道fork()函数子进程是拷贝父进程的代码段的,并且拷贝了代码执行位置

#include <stdio.h>#include<unistd.h>  int main(){int pid;printf("创建进程前!\n");pid=fork();if(pid==0){           while(1)           {                    printf("子进程\n");                    sleep(1);           }        }else{           while(1)           {                    printf("父进程\n");                    sleep(1);           }}return 0;}

执行结果

一般来说,fork之后,父子进程谁先执行是不确定的,这主要取决于内核调度算法

fork内存部分验证

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/mman.h>int b=2;int main(){printf("before create process\n");int pid;pid = fork();int a = 1;int *c=malloc(4);int *d=sbrk(4); int *e=mmap(0,4,PROT_READ|PROT_WRITE,                    MAP_ANONYMOUS|MAP_PRIVATE,0,0);int *f=mmap(0,4,PROT_READ|PROT_WRITE,                    MAP_ANONYMOUS|MAP_SHARED,0,0);*c=3;        *d=4;*e=5;*f=6;if(pid == 0){           printf("child process\n");           a=10;           b=20;           *c=30;           *d=40;           *e=50;           *f=60;           printf("a %d\n",a);           printf("b %d\n",b);           printf("c %d\n",*c);           printf("d %d\n",*d);           printf("e %d\n",*e);           printf("f %d\n",*f); }else{           printf("parent process\n");           sleep(5);           printf("a %d\n",a);           printf("b %d\n",b);           printf("c %d\n",*c);           printf("d %d\n",*d);           printf("e %d\n",*e);           printf("f %d\n",*f); }return 0;}

执行结果

先自己体会下结果,一会讲完文件共享后再总结子进程都继承了父进程的哪些东西

文件共享

有一个文件,内容为

写一个程序

#include <sys/wait.h>#include<signal.h>#include<sys/mman.h>#include<fcntl.h>#include<stdio.h> int main(){int fd1;char buff[20]={0};pid_t pid;fd1 = open("test.txt",O_RDWR); pid = fork();if(pid == 0){              sleep(1);           read(fd1,buff,5);                      printf("child %s\n",buff);           close(fd1);     } else{           read(fd1,buff,5);            printf("parent %s\n",buff);           close(fd1);}     }

执行结果为:


子进程和父进程共享一个文件的指针偏移量

借一张APUE的图说明了一切

由此总结一下,以下参照参照:http://blog.csdn.net/xiaojun111111/article/details/51764389

由子进程自父进程继承到:

·       进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs))

·       环境(environment)

·       堆栈

·       内存

·       打开文件的描述符(注意对应的文件的位置由父子进程共享,这会引起含糊情况)

·       执行时关闭(close-on-exec) 标志 (译者注:close-on-exec标志可通过fnctl()对文件描述符设置,POSIX.1要求所有目录流都必须在exec函数调用时关闭。更详细说明,参见《UNIX环境高级编程》 W. R. Stevens, 1993, 尤晋元等译(以下简称《高级编程》), 3.13节和8.9)

·       信号(signal)控制设定

·       nice (译者注:nice值由nice函数设定,该值表示进程的优先级,数值越小,优先级越高)

·       进程调度类别(scheduler class) (译者注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级(Global process prority),优先级高的进程优先执行)

·       进程组号

·       对话期ID(Session ID) (译者注:译文取自《高级编程》,指:进程所属的对话期 (session)ID,一个对话期包括一个或多个进程组,更详细说明参见《高级编程》 9.5)

·       当前工作目录

·       根目录 (译者注:根目录不一定是“/”,它可由chroot函数改变)

·       文件方式创建屏蔽字(file mode creation mask (umask)) (译者注:译文取自《高级编程》,指:创建新文件的缺省屏蔽字)

·       资源限制

·       控制终端

子进程所独有:

·       进程号

·       不同的父进程号(译者注:即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)

·       自己的文件描述符和目录流的拷贝(译者注:目录流由opendir函数创建,因其为顺序读取,顾称目录流”)

·       子进程不继承父进程的进程,正文(text),数据和其它锁定内存(memory locks) (译者注:锁定内存指被锁定的虚拟内存页,锁定后,不允许内核将其在必要时换出(page out),详细说明参见《The GNU C Library Reference Manual 2.2版, 1999,3.4.2)

·       tms结构中的系统时间(译者注:tms结构可由times函数获得,它保存四个数据用于记录进程使用中央处理器 (CPUCentral Processing Unit)的时间,包括:用户时间,系统时间,用户各子进程合计时间,系统各子进程合计时间)

·       资源使用(resource utilizations)设定为0

·       阻塞信号集初始化为空集(译者注:原文此处不明确,译文根据fork函数手册页稍做修改)

·       不继承由timer_create函数创建的计时器

·       不继承异步输入和输出

进程结束

说到进程终止,会延伸出来一个名词:僵尸进程

来看一段程序

#include<sys/wait.h>#include<signal.h>#include<sys/mman.h>#include<fcntl.h>#include<stdio.h> int main(){int fd1;char buff[20]={0};pid_t pid; pid = fork();if(pid == 0){              printf("child process\n");           while(1);} else{           printf("parent process\n");           while(1);}   }

发现pstree是这种状态,并且ps aue一下看下

runing状态

但是以上程序我修改下代码,把父进程的while1)去掉,然后再执行

发现子进程直接挂到init进程下面了,这个孤儿进程被linux老大进程init收养了

再做一个验证,如果我把子进程先退出,父进程一直是while(1)循环呢

发现pstree中还有

并且ps aue变为僵尸进程

这时候wait一系列的函数就派上用场了

先来科普下僵尸进程的弊端吧,否则就没必要回收了

僵尸需要他的父进程来为他收尸,如果他的父进程没有安装SIGCHLD信号处理函数调用wait 或 waitpid() 等待子进程结束,有没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时候父进程结束了,那么init进程会自动接手这个子进程,为他收尸,他还是能被清除掉的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态。虽然一个僵尸进程响应的资源被释放掉,也不会被调度执行,但是OS中还保存着他的一些信息,所以会造成内存泄漏

以下参考:http://blog.csdn.net/u011068702/article/details/54409273

1)  wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接收到了一个指定的信号为止。如果该父进程没有子进程或者它的子进程已经结束,则wait()函数就会立即返回。

2) waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程(它可以指定需要等待终止的子进程),它还有若干选项,如可提供一个非阻塞版本的 wait()功能,也能支持作业控制。实际上,wait()函数只是 waitpid()函数的一个特例,在Linux 内部实现 wait()函数时直接调用的就是waitpid()函数。

来一段程序

#include<sys/wait.h>#include<signal.h>#include<sys/mman.h>#include<fcntl.h>#include<stdio.h> int main(){int fd1;char buff[20]={0};pid_t pid; pid = fork();if(pid == 0){              printf("child process pid%d\n",getpid());           sleep(10);           //while(1);} else{           printf("parent process pid%d\n",getpid());           wait(NULL);           printf("catch a child processend\n");           while(1);}   }

执行结果

这时在用ps aue命令发现不存在僵尸进程

另外,延时10s才出来catch a child process edn这句说明wait是阻塞的,但是这样就有一个弊端了··指望着父进程阻塞来等子进程结束回收资源,那么父进程什么都不用干了,所以就引出一个信号的概念,子进程结束会给父进程发送一个编号为17的信号SIGCHLD,后面的文章再介绍信号,但是此部分先直接用到,程序源码

#include <stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/wait.h>#include <signal.h>void deal(int s){wait(NULL);printf("回收中.....\n");sleep(5);  printf("回收完毕!\n");                    }intmain(){if(fork()==0){           printf("child process\n");           sleep(10);           printf("end\n");}else{                            signal(17,deal);                             while(1)           {                    printf("parentruning\n");                    sleep(1);             }}}

执行结果

这样处理起来就比较合理了,但是有个需要注意的点是:

在信号处理函数的时候,此函数也是阻塞的,父进程并不继续执行,在信号处理函数完毕才继续执行

2.  vfork

fork()与vfock()都是创建一个进程,那他们有什么区别呢?总结有以下三点区别: 
1).  fork 
():子进程拷贝父进程的数据段,代码段 
    vfork 
):子进程与父进程共享数据段 
2).  fork ()父子进程的执行次序不确定 
    vfork
保证子进程先运行

,在调用exec exit 之前与父进程数据是共享的,在它调用exec
    
exit 之后父进程才可能被调度运行。 
3).  vfork ()保证子进程先运行,在她调用exec exit 之后父进程才可能被调度运行。如果在
  
调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。 

来一段代码体会下

#include<sys/wait.h>#include<signal.h>#include<sys/mman.h>#include<fcntl.h>#include<stdio.h> intglobvar = 6;           /* externalvariable in initialized data */ int main(void){int var;               /*automatic variable on the stack */pid_t pid;var = 88;printf("before vfork\n");  /* we don't flush stdio */if ((pid = vfork()) < 0) {           printf("vfork error");} else if (pid == 0) {             /* child */           printf("child process\n");           globvar++;                                    /* modify parent's variables */           var++;           _exit(0);                               /* child terminates */}else{           /* parent continues here */           printf("pid = %ld, glob = %d,var = %d\n", (long)getpid(), globvar,var);}}

程序的执行结果

这样就验证了我们第一和第二点区别

Vfork保证子进程先运行,并且和父进程共享一个数据段,下面我们来验证下第三点区别,还是沿用上面的程序,只是把子进程的_exit(0)去掉,看看结果会怎样

发现出现段错误

3. popen

#include <stdio.h>

FILE *popen(const char *command, const char *type);

int pclose(FILE *stream);

popen() 函数用于创建一个管道:其内部实现为调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程这个进程必须由 pclose() 函数关闭。

程序:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/wait.h>int main(){char buf[1024];FILE *f=popen("ls -l","r");int fd=fileno(f);int r;          printf("=============\n");while((r=read(fd,buf,1024))>0){           buf[r]=0;           printf("::%s\n",buf);}printf("=============\n");close(fd);pclose(f); }

执行结果:

4. exec

Exec函数是一个系列

intexecl(const char *path, const char *arg, ...)intexecv(const char *path, char *const argv[])intexecle(const char *path, const char *arg, ..., char *const envp[])intexecve(const char *path, char *const argv[], char *const envp[])intexeclp(const char *file, const char *arg, ...)intexecvp(const char *file, char *const argv[])

函数返回值:成功 -> 函数不会返回,出错 -> 返回-1,失败原因记录在error中。
6 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较说明。
1)  查找方式:上表其中前4个函数的查找方式都是完整的文件目录路径,而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。
2)参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举的方式,而另一种则是将所有参数整体构造成指针数组进行传递。
在这里参数传递方式是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举的方式,字母为“v”(vertor)的表示将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。读者可以观察execlexecleexeclp的语法与execvexecveexecvp的区别。
3)环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execleexecve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。

4)PATH环境变量说明
PATH环境变量包含了一张目录表,系统通过PATH环境变量定义的路径搜索执行码,PATH环境变量定义时目录之间需用用“:”分隔,以“.”号表示结束。PATH环境变量定义在用户的.profile.bash_profile中,下面是PATH环境变量定义的样例,此PATH变量指定在“/bin”、“/usr/bin”和当前目录三个目录进行搜索执行码。
PATH=/bin:/usr/bin:.
export $PATH
5)进程中的环境变量说明
    Linux中,Shell进程是所有执行码的父进程。当一个执行码执行时,Shell进程会fork子进程然后调用exec函数去执行执行码。Shell进程堆栈中存放着该用户下的所有环境变量,使用execlexecvexeclpexecvp函数使执行码重生时,Shell进程会将所有环境变量复制给生成的新进程;而使用execleexecve时新进程不继承任何Shell进程的环境变量,而由envp[]数组自行设置环境变量。
6)exec函数族关系
4位统一为:exec
5
l:参数传递为逐个列举方式
execlexecleexeclp
v
:参数传递为构造指针数组方式
execvexecveexecvp
6
e:可传递新进程环境变量
execleexecve
p
:可执行文件查找方式为文件名
execlpexecvp

事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用

7)exec调用举例如下:

char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);execv("/bin/ps", ps_argv);execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);execve("/bin/ps", ps_argv, ps_envp);execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);execvp("ps", ps_argv);

请注意exec函数族形参展开时的前两个参数,第一个参数是带路径的执行码(execlpexecvp函数第一个参数是无路径的,系统会根据PATH自动查找然后合成带路径的执行码),第二个是不带路径的执行码,执行码可以是二进制执行码和Shell脚本。
8)exec函数族使用注意点
在使用exec函数族时,一定要加上错误判断语句。因为exec很容易执行失败,其中最常见的原因有:
   找不到文件或路径,此时errno被设置为ENOENT
   数组argvenvp忘记用NULL结束,此时errno被设置为EFAULT
   没有对应可执行文件的运行权限,此时errno被设置为EACCES
9exec后新进程保持原进程以下特征
Ÿ      环境变量(使用了execleexecve函数则不继承环境变量);
Ÿ      进程ID和父进程ID
Ÿ      实际用户ID和实际组ID
Ÿ      附加组ID
Ÿ      进程组ID
Ÿ      会话ID
Ÿ      控制终端;
Ÿ      当前工作目录;
Ÿ      根目录;
Ÿ      文件权限屏蔽字;
Ÿ      文件锁;
Ÿ      进程信号屏蔽;
Ÿ      未决信号;
Ÿ      资源限制;
Ÿ      tms_utimetms_stimetms_cutime以及tms_ustime值。
对打开文件的处理与每个描述符的exec关闭标志值有关,进程中每个文件描述符有一个exec关闭标志(FD_CLOEXEC),若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开,利用这一点可以实现I/O重定向。

Exec参考文章:http://blog.csdn.net/zhengqijun_/article/details/52852074

5.system

说白了点就是system会执行一个程序或者一个shell命令,但是是开启另外一个进程去执行,函数原形

#include <stdlib.h>

int system(const char *command);

为了更好的理解system()函数返回值,需要了解其执行过程,实际上system()函数执行了三步操作:

  1. )fork一个子进程;
  2. )在子进程中调用exec函数去执行command;
  3. )在父进程中调用wait去等待子进程结束。 对于fork失败,system()函数返回-1。 如果exec执行成功,也即command顺利执行完毕,则返回command通过exit或return返回的值。 (注意,command顺利执行不代表执行成功,比如command:"rm debuglog.txt",不管文件存不存在该command都顺利执行了) 如果exec执行失败,也即command没有顺利执行,比如被信号中断,或者command命令根本不存在,system()函数返回127. 如果command为NULL,则system()函数返回非0值,一般为1.

另外,函数是阻塞的,直接上代码体会下

Test.c编译为test可执行文件

System.c文件内容为

#include<stdio.h>#include<unistd.h>#include<stdlib.h> int main(){int r;printf("%d\n",getpid());r = system("./test");printf("system end\n");}

执行结果

此部分发现system新创建一个进程,并且是阻塞

另外,网上有说system不稳定,容易出问题,暂时没有研究

好了,进程控制就到这里为止吧,整理的好累···

 

原创粉丝点击