多进程编程学习笔记-1-进程基础

来源:互联网 发布:阴茎粗大的感受 知乎 编辑:程序博客网 时间:2024/06/10 21:23

1.背景

在一个程序启动主进程后,可能需要再启动一个或多个进程来完成独立的多个任务。多进程编程的主要内容包括进程控制和进程间通信,在了解这些之前,需要先简单了解进程的结构。

2.Linux下进程的结构

Linux下一个进程在内存里有三部分的数据,”代码段”、”堆栈段”和”数据段”。一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分也是构成一个完整的执行序列的必要的部分。
“代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。”堆栈段”存放的就是子程序的返回地址、子程序的参数以及程序的局部变量,及new出来的变量。而数据段则存放程序的全局变量或静态变量,常数。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。

2.1进程控制

在传统的Unix环境下,有两个基本的操作用于创建和修改进程:函数fork( )用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝;函数族exec( )用来启动另外的进程以取代当前运行的进程。Linux的进程控制和传统的Unix进程控制基本一致,只在一些细节的地方有些区别,例如在Linux系统中调用vfork和fork完全相同,而在有些版本的Unix系统中,vfork调用有不同的功能。

2.1.1 exec()函数族(进程替换)

在Linux中使用exec函数族可以让一个进程启动另一个程序的执行。例如系统调用execve()对当前进程进行替换,替换者为一个指定的程序,其参数包括文件名(filename)、参数列表(argv)以及环境变量(envp)。在 Linux中exec函数族,分别是:execl,execlp,execle,execv,execve和execvp,下面以execlp为例,其它函数究 竟与execlp有何区别,请通过manexec命令来了解它们的具体情况。

#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main(){    printf("Running ps with execlp\n");    execlp("ps", "ps", "ax", 0);    printf("Done.\n");    exit(0);}

上面程序Done字符是不会被打印出来的,且ps的显示结果中并没有运行程序的进程名,这是因为调用了ps ax命令之后,并没有返回到上面的程序中,所以不会打印出Done字符,且由于进程被替换,所以ps的结果中显示的是ps ax对应的进程号等信息。

一个进程一旦调用exec类函数,它本身就”死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈 段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。)

2.1.2 fork()(进程复制)

代码例子:

int main(){    int i;     if ( fork() == 0 )    {         /* 子进程程序 */         for ( i = 1; i <100; i ++ )             printf("This is child process\n");     }     else    {         /* 父进程程序*/         for ( i = 1; i <50; i ++ )            printf("This is parent process\n");     }     return 0;}

这里写图片描述
从上述结果中能看到屏幕上交替出现子进程与父进程各打印出的信息。如果程序还在运行中,你用ps命令就能看到系统中有两个同名程序在运行,注意,进程号是不同的。

调用这个fork函数时发生了什么呢?fork函数启动一个新的进程,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上 数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。它们再要交互信息时,只有通过进程间通信来实现。系统是通过fork返回值来区分父子进程的。对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。对父进程而言,它的进程号是由比它更低层的系统调用赋予的,而对于子进程而言,它的进程号即是fork函数对父进程的返回值。在程序设计中,父进程和子进程都要调用函数fork()下面的代码,而我们就是利用fork()函数对父子进程的不同返 回值用if…else…语句来实现让父子进程完成不同的功能。我们看到,上面例子执行时两条信息是交互无规则的打印出 来的,这是父子进程独立执行的结果,虽然我们的代码似乎和串行的代码没有什么区别。

如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork的系统开销不是很大吗?其实UNIX自有其解决 的办法,一般CPU都是以”页”为单位来分配内存空间的,每一个页都是实际物理内存的一个映像,而无论是数据段还是堆栈段都是由许多”页”构成的,fork函数复制这两个段,只是”逻辑”上的,并非”物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的” 页”从物理上也分开。系统在空间上的开销就可以达到最小。

例子2:

    pid_t pid;    char *message;    int n;    printf("fork program starting\n");    pid = fork();    switch(pid)     {    case -1:        perror("fork failed");        exit(1);    case 0:        message = "This is the child";        n = 5;        break;    default:        message = "This is the parent";        n = 3;        break;    }    for(; n > 0; n--) {        puts(message);        sleep(1);    }    exit(0);

从下图我们可以看到由于父进程在子进程打印完它的消息之前已经结束了,并输出了shell的提示符,所以,我们看到输出内容中混杂着一个shell提示符。
这里写图片描述

2.1.3 wait()(进程等待)

由于fork操作之后新建了一个子进程,且该进程是有自己的生命周期独立运行的,当我们希望知晓子进程是何时结束的,例如上述的代码可以看出,由于父子进程之间执行结束的时间不同,输出的结果略显乱,那么此时我们可以通过在父进程中调用wait函数让父进程等待子进程的结束。
代码例子:

#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main(){    pid_t pid;    char *message;    int n;    int exit_code;    printf("fork program starting\n");    pid = fork();    switch(pid)     {    case -1:        exit(1);    case 0:        message = "This is the child";        n = 5;        exit_code = 37;        break;    default:        message = "This is the parent";        n = 3;        exit_code = 0;        break;    }    for(; n > 0; n--) {        puts(message);        sleep(1);    }/*  This section of the program waits for the child process to finish.  */    if(pid) {        int stat_val;        pid_t child_pid;        child_pid = wait(&stat_val);//调用返回子进程PID        printf("Child has finished: PID = %d\n", child_pid);        if(WIFEXITED(stat_val))//如果子进程正常结束,返回值非零            printf("Child exited with code %d\n", WEXITSTATUS(stat_val));//如果WIFEXITED非零,它返回子进程的退出码        else            printf("Child terminated abnormally\n");    }    exit (exit_code);}

父进程(从fork调用中获取一个非零的返回值)用wait系统调用将自己的执行挂起,直到子进程的状态信息出现为止。这将发生在子进程调用exit()的时候。我们将子进程的退出码设置为37。父进程然后继续运行,通过测试wait调用的返回值来判断子进程是否正常终止。如果是,则从状态信息中提取出子进程的退出码。
运行结果如下:
这里写图片描述

2.2进程间通信

Linux作为一种新兴的操作系统,几乎支持所有的Unix下常用的进程间通信方法:管道、消息队列、共享内存、信号量、套接口等等。鉴于篇幅问题,我们将在下篇文章再逐一介绍。

0 0
原创粉丝点击