Linux下的多进程编程

Linux 下的多进程编程
 
(一) 理解 Linux 下进程的结构
   Linux 下一个进程在内存里有三部份的数据,就是 数据段 堆栈段 代码段 ,其实学过汇编语言的人一定知道,一般的 CPU I386 ,都有上述三种段寄存器,以方便操作系统的运行。 代码段 ,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。 堆栈段存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用 malloc 之类的函数取得的空间)。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。
 
(二) 如何使用 fork
Linux 下产生新的进程的系统调用就是 fork 函数,这个函数名是英文中 分叉 的意思。为什么取这个 名字呢?因为一个进程在运行中,如果使用了 fork ,就产生了另一个进程,于是进程就 分叉 了,所以这个名字取得很形象。下面就看看如何具体使用 fork ,这段程序演示了使用 fork 的基本框架:
 
#include <sys/types.h>
#include <unistd.h>
 
int main()
{
        pid_t pid;
        int   i;
 
        if (fork() == 0)
        {
                for(i = 0; i < 100; i++)
                        printf("%d I am child process./n", i);
        }
        else
        {
                for(i = 0; i < 100; i++)
                        printf("%d I am father process./n", i);
        }
}
程序运行后,你就能看到屏幕上交替出现子进程与父进程各打印出的一百条信息了。如果程序还在运行中,你用 ps 命令就能看到系统中有两个它在运行了。
  那么调用这个 fork 函数时发生了什么呢?一个程序一调用 fork 函数,系统就为一个新的进程准备了前述三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数( shmget shmat shmdt 等)来操作。现在,已经是两个进程了,对于父进程, fork 函数返回了子程序的进程号,而对于子程序, fork 函数则返回零,这样,对于程序,只要判断 fork 函数的返回值,就知道自己是处于父进程还是子进程中。
  如果一个大程序在运行中,它的数据段和堆栈都很大,一次 fork 就要复制一次,那么 fork
的系统开销不是很大吗?其实 UNIX 自有其解决的办法,大家知道,一般 CPU 都是以 为单位分配空间的,象 INTEL CPU ,其一页在通常情况下是 4K 字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的, fork 函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行 fork 时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销就可以达到最小。
  一个小幽默:下面演示一个足以 " 搞死 "Linux 的小程序,其源代码非常简单:
void main()
{
for(;;)
fork();
}
  这个程序什么也不做,就是死循环地 fork ,其结果是程序不断产生进程,而这些进程又不断产生新的进程 ,很快系统的进程就满了,系统就被这么多不断产生的进程 " 撑死了 " 。用不着是 root ,任何人运行上述程序都足以让系统死掉。但这不是 Linux 不安全的理由,因为只要系统管理员足够聪明,他(或她)就可以预先给每个用户设置可运行的最大进程数,这样,只要不是 root ,任何能运行的进程数也许不足系统总的能运行和进程数的十分之一,这样,系统管理员就能对付上述恶意的程序了。
 
(三) 如何启动另一程序的执行  
  下面我们来看看一个进程如何来启动另一个程序的执行。在 Linux 中要使用 exec 类的函数, exec 类的函数不止一个,但大致相同,在 Linux 中,它们分别是: execl execlp execle execv execve execvp ,下面 我只以 execlp 为例,其它函数究竟与 execlp 有何区别,请通过 man exec 命令来了解它们的具体情况。一个进程一旦调用 exec 类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过 exec 类函数中有的还允许继承环境变量之类的信息。)
  那么如果我的程序想启动另一程序的执行但自己仍想继续运行的话,怎么办呢?那就是结合 fork exec 的使用。下面一段代码显示如何启动运行其它程序:
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
 
int main()
{
        char    psz_command[256];
        int     i_return;
 
        while(1)
        {
                printf(">");
                fgets(psz_command, 256, stdin);
                psz_command[strlen(psz_command) - 1] = '/0';
 
                if (fork() == 0)
                {
                        execlp(psz_command, psz_command);
                        perror(psz_command);
                        exit(errno);
                }
                else
                {
                        wait(&i_return);
                        printf("child process return %d /n", i_return);
                }
        }
}
  此程序从终端读入命令并执行之,执行完成后,父进程继续等待从终端读入命令。熟悉 DOS WINDOWS 系统调用的朋友一定知道 DOS/WINDOWS 也有 exec 类函数,其使用方法是类似的,但 DOS/WINDOWS 还有 spawn 类函数, 因为 DOS 是单任务的系统,它只能将“父进程”驻留在机器内再执行“子进程”,这就是 spawn 类的函数。 WIN32 已经是多任务的系统了,但还保留了 spawn 类函数, WIN32 中实现 spawn 函数的方法同前述 UNIX 中的方法差不多,开设子进程后父进程等待子进程结束后才继续运行。 UNIX 在其一开始就是多任务的系统,所以从核心角度上讲不需要 spawn 类函数。
  另外,有一个更简单的执行其它程序的函数 system ,它是一个较高层的函数,实际上相当于在 SHELL 环境下执行一条命令,而 exec 类函数则是低层的系统调用。
 
(四) Linux 的进程与 Win32 的进程 / 线程有何区别
  熟悉 WIN32 编程的人一定知道, WIN32 的进程管理方式与 UNIX 上有着很大区别,在 UNIX 里,只有进程的概念,但在 WIN32 里却还有一个“线程”的概念,那么 UNIX WIN32 在这里究竟有着什么区别呢?
   UNIX 里的 fork 是七十年代 UNIX 早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程方法。 WIN32 里的进程 / 线程是继承自 OS/2 的。在 WIN32 里,“进程”是指一个程序,而“线程”是一个“进程”里的一个执行“线索”。从核心上讲, WIN32 的多进程与 UNIX 并无多大的区别,在 WIN32 里的线程才相当于 UNIX 的进程,是一个实际正在执行的代码。但是, WIN32 里同一个进程里各个线程之间是共享数据段的。这才是与 UNIX 的进程最大的不同。
  下面这段程序显示了 WIN32 下一个进程如何启动一个线程:(请注意,这是个终端方式程序,没有图形界面)
 
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
 
int i_g;
DWORD WINAPI ChildProcess(LPVOID lpParameter)
{
    int i;
    for (i = 1; i < 1000; i++)
    {
        i_g++;
        printf("This is child thread: %d/n", i_g);   
    }
    ExitThread(0);
   
       
}
int main(int argc, char *argv[])
{
 DWORD ThreadID;
 int i;
 i_g = 0;
 
 CreateThread(NULL, 0, ChildProcess, NULL, 0, &ThreadID);
 for (i = 0; i < 1000; i++)
 {
        i_g++;
        printf("This is parent thread: %d/n", i_g);     
 }
 
 return 0;
}
  在 WIN32 下,使用 CreateThread 函数创建线程,与 UNIX 不同,线程不是从创建处开始运行的,而是由 CreateThread 指定一个函数,线程就从那个函数处开始运行。此程序同前面的 UNIX 程序一样,由两个线程各打印 1000 条信息。 threadID 是子线程的线程号,另外,全局变量 g 是子线程与父线程共享的,这就是与 UNIX 最大的不同之处。大家可以看出, WIN32 的进程 / 线程要比 UNIX 复杂,在 UNIX 里要实现类似 WIN32 的线程并不难,只要 fork 以后,让子进程调用 ThreadProc 函数,并且为全局变量开设共享数据区就行了,但在 WIN32 下就无法实现类似 fork 的功能了。所以现在 WIN32 下的 C 语言编译器所提供的库函数虽然已经能兼容大多数 UNIX 的库函数,但却仍无法实现 fork
  对于多任务系统,共享数据区是必要的,但也是一个容易引起混乱的问题,在 WIN32 下,一个程序员很容易忘记线程之间的数据是共享的这一情况,一个线程修改过一个变量后,另一个线程却又修改了它,结果引起程序出问题。但在 UNIX 下,由于变量本来并不共享,而由程序员来显式地指定要共享的数据,使程序变得更清晰与安全。
   Linux 还有自己的一个函数叫 clone ,这个函数是其它 UNIX 所没有的,而且通常的 Linux 也并不提供此函数(要使用此函数需自己重新编译内核,并设置 CLONE_ACTUALLY_WORKS_OK 选项), clone 函数提供了更多的创建新进程的功能,包括象完全共享数据段这样的功能。
  至于 WIN32 的“进程”概念,其含义则是“应用程序”,也就是相当于 UNIX 下的 exec 了。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值