Linux进程编程介绍(二)

来源:互联网 发布:软件开发简历专业技能 编辑:程序博客网 时间:2024/04/30 04:41
Linux进程编程介绍(二)
2006-11-19 16:38
 

转自:计算机基础教程网(ITWEN.com)

  摘要:本节先介绍一些关于进程的基本操作,通过本节,我们将了解如何产生子进程,进程如何改变它的执行映像,父子进程的同步等操作。由此也了解到一些并行程序的基本概念与如何编制简单的并行程序。

2. 进程的一般操作

    上一节介绍了一些有关进程的基本概念,从这一节开始要结合一些例子来阐述一些有关进程的系统调用。本节先介绍一些关于进程的基本操作,通过本节,我们将了 解如何产生子进程,进程如何改变它的执行映像,父子进程的同步等操作。由此也了解到一些并行程序的基本概念与如何编制简单的并行程序。

2.1 fork 系统调用

    系统调用fork是用来创建一个子进程。创建的过程前面一节已经介绍过。现在,再介绍一个系统调用vfork,这个调用的产生是因为认识到创建子进程时对 父进程的所有页不进行拷贝能带来性能上的改善。该调用假定进行vfork调用后,将立即调用exec,这样就不需要拷贝父进程的所有页表。因为它不拷贝页 表,所以比fork调用快。有些系统的fork也采用了其他方法来提高性能,比较典型的一种是增加“写时拷贝”。这种fork调用,产生子进程时,并不拷 贝父进程的所有页面,而是置父进程所有页面的写时拷贝位,子进程共享父进程的所有页面。直到父进程或子进程写某个页面时,就会发生一个保护性错误,并拷贝 该页面。这样不仅提高了内核的性能,而且改善了内存的利用。

    系统调用fork和vfork的声明格式如下:

pid_t fork(void);
pid_t vfork(void);

    在使用该系统调用的程序中要加入以下头文件:

#include <unistd.h>

    当调用执行成功时,该调用对父进程返回子进程的PID,对子进程返回0。调用失败时,给父进程返回-1,没有子进程创建。

    下面是发生错误时,可能设置的错误代码errno:

    EAGAIN:系统调用fork不能得到足够的内存来拷贝父进程页表。或用户是超级用户但进程表满,或者用户不是超级用户但达到单个用户能执行的最大进程数。 

    ENOMEM:对创建新进程来说没有足够的空间,该错误是指没有足够的空间分配给必要的内核结构。 
    下面我们看一个fork调用的简单的例子。该例子产生一个子进程,父进程打印出自己和子进程的PID,子进程打印出自己的PID和父进程的PID。

    注意:父进程打开了一个文件。父子进程都可以对该文件操作,该程序父子进程都向文件中写入了一行。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
extern int errno;
int main()
{
char buf[100];
pid_t cld_pid;
int fd;
int status;

if ((fd=open("temp",O_CREAT|O_TRUNC|O_RDWR,S_IRWXU)) == -1)
{
printf("open error %d",errno);
exit(1);
}
strcpy(buf,"This is parent process write");

if ((cld_pid=fork()) == 0)
{ /* 这里是子进程执行的代码 */
strcpy(buf,"This is child process write");
printf("This is child process");
printf("My PID(child) is %d",getpid()); /*打印出本进程的ID*/
printf("My parent PID is %d",getppid()); /*打印出父进程的ID*/
write(fd,buf,strlen(buf));
close(fd);
exit(0);
}
else
{ /* 这里是父进程执行的代码 */
printf("This is parent process");
printf("My PID(parent) is %d",getpid()); /*打印出本进程的ID */
printf("My child PID is %d",cld_pid); /*打印出子进程的ID*/
write(fd,buf,strlen(buf));
close(fd);
}
wait(&status); /* 如果此处没有这一句会如何?*/
return 0;
}

    下面我们看一下,程序运行的结果,假设源文件命名为fork.c:

[root@wapgw /root]# gcc -o fork fork.c
[root@wapgw /root]# ./fork
This is parent process
This is child process
My PID(child) is 5258
My parent PID is 5257
My PID(parent) is 5257
My child PID is 5258
[root@wapgw /root]#

    从上面的运行结果可以看出进程的调度,父进程打印出第一行后,CPU调度子进程,打印出后续的三行,子进程结束,调度父进程执行(其中可能还有其他的进程被调度),父进程执行完,将控制返还给shell程序,最后一行是shell程序输出的提示符。

    看看temp文件里有什么内容

[root@wapgw /root]# more temp
This is child process write 
This is parent process write
[root@wapgw /root]#

    现在我们将程序稍作修改。将wait调用注释掉,我们看看会有什么样的结果。因为调度的原因,多执行几次,你会看到如下的结果:

[root@wapgw /root]#vi fork.c //将wait调用注释掉
[root@wapgw /root]# gcc -o fork fork.c
[root@wapgw /root]# ./fork
This is parent process
This is child process
My PID(parent) is 5282
My child PID is 5283
[root@wapgw /root]# My PID(child) is 5283
My parent PID is 1
[root@wapgw /root]#

    第一行是父进程的输出,第二行是子进程的输出,第三、四行是父进程的输出,这时父进程由于没有wait调用,不等待子进程而结束。下面一行中的  “[root@wapgw /root]#”是父进程结束,将控制返回给shell时,shell输出的提示符。然后CPU调用子进程,输出子进程的 PID是5283。注意,下面子进程输出其父进程的ID是1,因为它的父进程结束了,内核将它交给了进程1(进程init)来管理,这个过程见前面一节。 这里要提一下的是,输出结果的顺序和进程调度的顺序有关,自己试验的结果与例子中的顺序很可能不同,请自行分析。(从我的系统给出的结果来看,加不加 wait 都一样,都先执行完子进程,后执后父进程,不过用管道从父进程向子进程传消息,子进程也可以正常收到,看来现在内核调度比较智能,具体调度顺序 有待于进一步研究)

2.2 exec 系统调用

    系统调用exec是用来执行一个可执行文件来代替当前进程的执行映像。需要注意的是,该调用并没有生成新的进程,而是在原有进程的基础上,替换原有进程的 正文,调用前后是同一个进程,进程号PID不变。但执行的程序变了(执行的指令序列改变了)。它有六种调用的形式,随着系统的不同并不完全与以下介绍的相 同。它们的声明格式如下:

int execl( const char *path, const char *arg, ...);
int execlp( const char *file, const char *arg, ...);
int execle( const char *path, const char *arg , ..., char* const envp[]);
int execv( const char *path, char *const argv[]);
int execve( const char *filename, char *const argv [], char *const envp[]);
int execvp( const char *file, char *const argv[]);

    在使用这些系统调用的程序中要加入以下头文件和外部变量:

#include 
extern char **environ;

    下面我们先详细讲述其中的一个,然后再给出它们之间的区别。在系统调用execve中,参数path是将要执行的文件,参数argv是要传递给文件的参 数,参数envp是要传递给文件的环境变量。当参数path所指的文件替换原进程的执行映像后,文件path开始执行,参数argv和envp便传递给进 程。下面我们举一个简单的例子。

    在讲述系统调用execve的例子之前,我们先来看看环境变量。为了使用户方便和灵活地使用Shell,LINUX引入了环境的概念。环境是一些数据,用 户可以改变这些数据,增加新的数据或删除一些数据。这些数据称为环境变量。因为它们定义了用户的工作环境,同时又可以被修改。每个用户都可以有自己不同的 环境变量,用户可以用env命令(不带参数)浏览环境变量,输出的格式和变量名随着 Shell的不同和系统配置的不同而不同。下面这个例子打印出传递给 该进程的所有参数和环境变量:

#include 
#include 
extern char **environ;
int main(int argc,char* argv[])
{
int i;
printf("Argument:/n");
for (i=0;i<=argc;i++) printf("Arg%d is: %s/n",i,argv[i]);
         printf("Environment:/n");
for (i=0;environ[i]!=NULL;i++) printf("%s/ ",environ[i]);
}

下面是执行时的屏幕拷贝:

[root@wapgw /root]# gcc -o example example.c
[root@wapgw /root]# ./example test
Argument:
Arg0 is ./example
Arg1 is test

Environment:
PWD=/root
REMOTEHOST=cjm
HOSTNAME=wapgw
HOME=/root
。。。。。。。。。。。。。。。。。。。。。。。
SSH_ASKPASS=/usr/libexec/ssh/gnome-ssh-askpass
PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/sbin:/usr/local/bin:
/sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/root/bin
[root@wapgw /root]#

    其中Environment后的都是环境变量及其取值。下面我们来看看execve的一个简单的例子:

#include 
#include 
extern char **environ;
int main(int argc,char* argv[])
{
printf("Will replace by another image");
execve("example",argv,environ); /* 用上面的例子example替换进程执行映像 */
printf("process never go to here"); /* 进程永远不会执行到这里 */
}

    该程序用自己的参数argv和环境变量传递给新的执行映像。执行结果的屏幕拷贝如下:

[root@wapgw /root]# gcc -o execve execve.c
[root@wapgw /root]# ./execve these args will dend to example
Will replace by another image
Argument:
Arg0 is ./execve
Arg1 is these
Arg2 is args
Arg3 is will
Arg4 is dend
Arg5 is to
Arg6 is example
Environment:
PWD=/root
REMOTEHOST=cjm
HOSTNAME=wapgw
HOME=/root
。。。。。。。。。。。。。。。。。。。。。。。
SSH_ASKPASS=/usr/libexec/ssh/gnome-ssh-askpass
PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/sbin:/usr/local/bin:
/sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/root/bin
[root@wapgw /root]#

    这里要注意的是,如果你用execve some args > screen时,会发现输出重定向到一个文件后,丢失了数据(即少了输出的第一行 Will replace …)。这是因为将输出重定向到一个文件后,进程的第一行是输出到文件,所以被缓冲还没有真正写入文件,进程的第二行替换进程的 执行映像,也覆盖了文件的缓冲。这个问题可以通过fflush(stdout)刷新stdout的缓冲区或者用setbuf(stdout,NULL)设 置stdout的缓冲为空来解决。

    如果对某个文件描述符fd设置了close-on-exec标志,那么在exec调用后,该文件描述符被关闭。下面我们看一个简单的例子:

    这里有一个程序pp.c:

#include 
int main()
{
printf("test");
}

    它是用来替换进程执行图像的。再来看看下面的程序:

#include 
#include 
#include 
extern char **environ;
int main(int argc,char* argv[])
{
printf("close-on-exec is %d",fcntl(1,F_GETFD));
fcntl(1,F_SETFD,16);
printf("close-on-exec is %d",fcntl(1,F_GETFD));
execve("pp",argv,environ);
printf("AH!!!!!");
}

    该程序的执行结果为:

[root@wapgw /root]# ./fcntl
close-on-exec is 0
close-on-exec is 0
test
[root@wapgw /root]#

    这是没有设置close-on-exec标志的结果,将fcntl语句改为

fcntl(1,F_SETFD,25);

    对于最后一个参数,只要保证该参数的最低位(二进制)是1就可以。这时的执行结果为:

[root@wapgw /root]# ./fcntl
close-on-exec is 0
close-on-exec is 1
[root@wapgw /root]#

    这时,系统调用execve用pp替换原进程的执行图像,但由于文件描述符1(stdout)被关闭,所以执行完execve调用后无输出。

    系统调用execve可以执行二进制的可执行文件(如a.out)。也可以执行shell程序,该shell程序必须以下面所示的格式开头,即第一行为: #! interpreter [arg]。其中interpreter可以是shell或其他解释器,例如:#!/bin/bsh或#! /usr/bin/perl。其中的arg是传递给解释器的参数。

  该系统调用成功时,不会返回任何值(因为进程的执行映像已经被替换,没有接收返回值的地方了)。如果有任何返回值(一般是-1),就代表有错误发生,内核将设置相应的错误代码errno,下面是一些可能设置的错误代码:

EACCES:指向的文件或脚本文件不是普通文件;指向的文件或脚本文件没有设置可执行位;文件系统被安装成noexec;指向的文件或脚本文件所处的路径中有目录不能搜索(没有execute权)。 
E2BIG:传递的参数清单太大。 
ENOEXEC:指定的文件确实有执行权限位,但是为即不可识别的执行文件格式。 
ETXTBUSY:指定文件被一个或多个进程以可写入的方式打开。 
EIO:从文件系统读入时发生I/O错误。 
    现在我们来看看这一族系统调用。在系统调用execl、execlp、execle中,参数是以arg0、arg1、arg2、…的方式传递的。按照惯 例,arg0应该是要执行的程序名。在调用execl、execlp中环境变量的值是自动传递的,即不用象调用execve、execle那样在调用中指 定参数envp。在调用execve、execv、execvp中参数是以数组的方式传递的。另一个区别是,调用execlp、execvp可以在环境变 量PATH定义的路径中查找执行程序。例如,PATH定义为“/bin:/usr/bin:/usr/sbin”,如果调用指定执行文件名为test,那 么这两个调用会在PATH定义的三个目录中查找名为test的可执行文件。

    系统调用exec和fork经常结合使用,父进程fork一个子进程,在子进程中调用exec来替换子进程的执行映像,并发的执行一些操作。

2.3 exit 系统调用

   系统调用exit的功能是终止发出调用的进程。它的声明格式如下:

void _exit(int status);

    在使用这个系统调用的程序中要加入以下头文件:

#include 

    系统调用_exit立即终止发出调用的进程。所有属于该进程的文件描述符都关闭。该进程的所有子进程由进程1(进程init)接收,并对该进程的父进程发 出一个SIGCHLD(子进程僵死)的信号。参数status作为退出的状态值返回父进程,该值可以通过系统调用wait来收集。返回状态码status 只有最低一个字节有效。如果进程是一个控制终端进程,则SIGHUP信号将被送往该控制终端的前台进程。系统调用_exit从不返回任何值给发出调用的进 程;也不刷新I/O缓冲,如果要自动完成刷新,可以用函数调用exit。

2.4 wait 系统调用

    系统调用wait的功能是发出调用的进程只要有子进程,就睡眠直到它们中的一个终止为止。该调用声明的格式如下:

pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *status, int options);

    在使用这些系统调用的程序中要加入以下头文件

#include 
#include 

    发出wait调用的进程进入睡眠直到它的一个子进程退出时或收到一个不能被忽略的信号时被唤醒。如果调用发出时,已经有退出的子进程(这时子进程的状态是僵死状态),该调用立即返回。其中调用返回时参数status中包含子进程退出时的状态信息。

    调用waitpid与调用wait的区别是waitpid等待由参数pid指定的子进程退出。其中参数pid的含义与取值方法如下:

    参数pid < -1时,当退出的子进程满足下面条件时结束等待:该子进程的进程组ID(process group)等于绝对值的pid这个条件。 
    参数pid = 0时,等待任何满足下面条件的子进程退出:该子进程的进程组ID等于发出调用进程的进程组ID。 
    参数pid > 0时,等待进程ID等于参数pid的子进程退出。 
    参数pid = -1时,等待任何子进程退出,相当于调用wait。 
    对于调用waitpid中的参数options的取值及其含义如下:

WNOHANG:该选项要求如果没有子进程退出就立即返回。 
WUNTRACED:对已经停止但未报告状态的子进程,该调用也从等待中返回和报告状态。如果status不是空,调用将使status指向该信息。下面的宏可以用来检查子进程的返回状态。前面三个用来判断退出的原因,后面三个是对应不同的原因返回状态值:

    WIFEXITED(status):如果进程通过系统调用_exit或函数调用exit正常退出,该宏的值为真。 
    WIFSIGNALED(status):如果子进程由于得到的信号(signal)没有被捕捉而导致退出时,该宏的值为真。 
    WIFSTOPPED(status):如果子进程没有终止,但停止了并可以重新执行时,该宏返回真。这种情况仅出现在waitpid调用中使用了WUNTRACED选项。 
    WEXITSTATUS(status):如果WIFEXITED(status)返回真,该宏返回由子进程调用_exit(status)或exit(status)时设置的调用参数status值。 
    WTERMSIG(status):如果WIFSIGNALED(status)返回为真,该宏返回导致子进程退出的信号(signal)的值。 
    WSTOPSIG(status):如果WIFSTOPPED(status)返回真,该宏返回导致子进程停止的信号(signal)值。 
    该调用返回退出的子进程的PID;或者发生错误时返回-1;或者设置了WNOHANG选项没有子进程退出就返回0;发生错误时,可能设置的错误代码如下:

    ECHILD:该调用指定的子进程pid不存在,或者不是发出调用进程的子进程。 
    EINVAL:参数options无效。 
    ERESTARTSYS:WNOHANG没有设置并且捕获到SIGCHLD或其它未屏蔽信号。 
关于wait调用的例子,前面在介绍fork调用时,就有了简单的应用。此处不再举例。

    注意:子进程退出(SIGCHLD)信号设置不同的处理方法,会导致该调用不同的行为,详细情况见Linux信号处理机制。

2.5 sleep 函数调用

    函数调用sleep可以用来使进程挂起指定的秒数。该函数调用的声明格式如下:

unsigned int sleep(unsigned int seconds)

    在使用这个函数调用的程序中加上以下的头文件:

#include 

    该函数调用使得进程挂起一个指定的时间,直到指定时间用完或者收到信号。系统的活动对指定的时间有一定的影响。Linux系统是用SIGALRM实现的,在Linux系统里,sleep函数不能和alarm()调用混用。

    如果指定挂起的时间到了,该调用返回0;如果该函数调用被信号所打断,则返回剩余挂起的时间数(指定的时间减去已经挂起的时间)。