漫无目的的漂泊着,流浪着

进程的一般操作

上一篇 / 下一篇  2008-02-18 20:30:58

ComputerTech 版 (精华区)

发信人: honger (国庆节长假无聊中...................), 信区: linux
标  题: Linux进程编程介绍(二)
发信站: 同舟共济站 (2001年10月05日14:03:34 星期五), 站内信件

二、 进程的一般操作
    上一节介绍了一些有关进程的基本概念,从这一节开始要结合一些例子来阐述一些
有关进程的系统调用。本节先介绍一些关于进程的基本操作,通过本节,我们将了解如
何产生子进程,进程如何改变它的执行映像,父子进程的同步等操作。由此也了解到一
些并行程序的基本概念与如何编制简单的并行程序。
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 <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.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 /roo
t]#”是父进程结束,将控制返回给shell时,shell输出的提示符。然后CPU调用子进程
,输出子进程的PID是5283。注意,下面子进程输出其父进程的ID是1,因为它的父进程
结束了,内核将它交给了进程1(进程init)来管理,这个过程见前面一节。这里要提一
下的是,输出结果的顺序和进程调度的顺序有关,自己试验的结果与例子中的顺序很可
能不同,请自行分析。
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 en
vp[]);
       int execvp( const char *file, char *const argv[]);
在使用这些系统调用的程序中要加入以下头文件和外部变量:
#include <unistd.h>
extern char **environ;
    下面我们先详细讲述其中的一个,然后再给出它们之间的区别。在系统调用execve
中,参数path是将要执行的文件,参数argv是要传递给文件的参数,参数envp是要传递
给文件的环境变量。当参数path所指的文件替换原进程的执行映像后,文件path开始执
行,参数argv和envp便传递给进程。下面我们举一个简单的例子。
    在讲述系统调用execve的例子之前,我们先来看看环境变量。为了使用户方便和灵
活地使用Shell,LINUX引入了环境的概念。环境是一些数据,用户可以改变这些数据,
增加新的数据或删除一些数据。这些数据称为环境变量。因为它们定义了用户的工作
境,同时又可以被修改。每个用户都可以有自己不同的环境变量,用户可以用env命令(
不带参数)浏览环境变量,输出的格式和变量名随着Shell的不同和系统配置的不同而不
同。下面这个例子打印出传递给该进程的所有参数和环境变量:
#include <stdio.h>
#include <unistd.h>
extern char **environ;
int main(int argc,char* argv[])
{
    int i;
    printf("Argument:
");
    for (i=0;i<argc;i++) printf("Arg%d is %s
",i,argv[i]);
    printf("
Environment:
");
    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:/b
in:/usr/sbin:/usr/bin:/usr/X11R6/bin:/root/bin
[root@wapgw /root]#
    其中Environment后的都是环境变量及其取值。下面我们来看看execve的一个简单的
例子:
#include <unistd.h>
#include <stdio.h>
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:/b
in:/usr/sbin:/usr/bin:/usr/X11R6/bin:/root/bin
[root@wapgw /root]#
    这里要注意的是,如果你用execve some args > screen时,会发现输出重定向到一
个文件后,丢失了数据(即少了输出的第一行Will replace …)。这是因为将输出重定
向到一个文件后,进程的第一行是输出到文件,所以被缓冲还没有真正写入文件,进程
的第二行替换进程的执行映像,也覆盖了文件的缓冲。这个问题可以通过fflush(stdou
t)刷新stdout的缓冲区或者用setbuf(stdout,NULL)设置stdout的缓冲为空来解决。
    如果对某个文件描述符fd设置了close-on-exec标志,那么在exec调用后,该文件描
述符被关闭。下面我们看一个简单的例子:
这里有一个程序pp.c:
#include <stdio.h>
int main()
{
    printf("test
");
}
它是用来替换进程执行图像的。再来看看下面的程序:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
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
来替换子进程的执行映像,并发的执行一些操作。
3、exit 系统调用
    系统调用exit的功能是终止发出调用的进程。它的声明格式如下:
      void _exit(int status);
    在使用这个系统调用的程序中要加入以下头文件:
      #include <unistd.h>
    系统调用_exit立即终止发出调用的进程。所有属于该进程的文件描述符都关闭。该
进程的所有子进程由进程1(进程init)接收,并对该进程的父进程发出一个SIGCHLD(
子进程僵死)的信号。参数status作为退出的状态值返回父进程,该值可以通过系统调
用wait来收集。返回状态码status只有最低一个字节有效。如果进程是一个控制终端进
程,则SIGHUP信号将被送往该控制终端的前台进程。系统调用_exit从不返回任何值给发
出调用的进程;也不刷新I/O缓冲,如果要自动完成刷新,可以用函数调用exit。
4、wait 系统调用
    系统调用wait的功能是发出调用的进程只要有子进程,就睡眠直到它们中的一个终
止为止。该调用声明的格式如下:
      pid_t wait(int *status)
      pid_t waitpid(pid_t pid, int *status, int options);
    在使用这些系统调用的程序中要加入以下头文件
      #include <sys/types.h>
      #include <sys/wait.h>
    发出wait调用的进程进入睡眠直到它的一个子进程退出时或收到一个不能被忽略的
信号时被唤醒。如果调用发出时,已经有退出的子进程(这时子进程的状态是僵死状态
),该调用立即返回。其中调用返回时参数status中包含子进程退出时的状态信息。
    调用waitpid与调用wait的区别是waitpid等待由参数pid指定的子进程退出。其中参
数pid的含义与取值方法如下:
(1) 参数pid < -1时,当退出的子进程满足下面条件时结束等待:该子进程的进程组ID
(process group)等于绝对值的pid这个条件。
(2) 参数pid = 0时,等待任何满足下面条件的子进程退出:该子进程的进程组ID等于发
出调用进程的进程组ID。
(3) 参数pid > 0时,等待进程ID等于参数pid的子进程退出。
(4) 参数pid = -1时,等待任何子进程退出,相当于调用wait。
    对于调用waitpid中的参数options的取值及其含义如下:
    WNOHANG:该选项要求如果没有子进程退出就立即返回。
    WUNTRACED:对已经停止但未报告状态的子进程,该调用也从等待中返回和报告状态
。如果status不是空,调用将使status指向该信息。下面的宏可以用来检查子进程的返
回状态。前面三个用来判断退出的原因,后面三个是对应不同的原因返回状态值:
(1) WIFEXITED(status):如果进程通过系统调用_exit或函数调用exit正常退出,该宏
的值为真。
(2) WIFSIGNALED(status):如果子进程由于得到的信号(signal)没有被捕捉而导致退
出时,该宏的值为真。
(3) WIFSTOPPED(status):如果子进程没有终止,但停止了并可以重新执行时,该宏返
回真。这种情况仅出现在waitpid调用中使用了WUNTRACED选项。
(4) WEXITSTATUS(status):如果WIFEXITED(status)返回真,该宏返回由子进程调用_e
xit(status)或exit(status)时设置的调用参数status值。
(5) WTERMSIG(status):如果WIFSIGNALED(status)返回为真,该宏返回导致子进程退出
的信号(signal)的值。
(6) WSTOPSIG(status):如果WIFSTOPPED(status)返回真,该宏返回导致子进程停止的
信号(signal)值。
    该调用返回退出的子进程的PID;或者发生错误时返回-1;或者设置了WNOHANG选项
没有子进程退出就返回0;发生错误时,可能设置的错误代码如下:
    ECHILD:该调用指定的子进程pid不存在,或者不是发出调用进程的子进程。
    EINVAL:参数options无效。
    ERESTARTSYS:WNOHANG没有设置并且捕获到SIGCHLD或其它未屏蔽信号。
关于wait调用的例子,前面在介绍fork调用时,就有了简单的应用。此处不再举例。
注意 子进程退出(SIGCHLD)信号设置不同的处理方法,会导致该调用不同的行为,详
细情况见Linux信号处理机制。
5、sleep 函数调用
    函数调用sleep可以用来使进程挂起指定的秒数。该函数调用的声明格式如下:
      unsigned int sleep(unsigned int seconds)
    在使用这个函数调用的程序中加上以下的头文件:
      #include <unistd.h>
    该函数调用使得进程挂起一个指定的时间,直到指定时间用完或者收到信号。系统
的活动对指定的时间有一定的影响。Linux系统是用SIGALRM实现的,在Linux系统里,s
leep函数不能和alarm()调用混用。
   如果指定挂起的时间到了,该调用返回0;如果该函数调用被信号所打断,则返回剩
余挂起的时间数(指定的时间减去已经挂起的时间)。

--
※ 来源:·同舟共济站 bbs.tongji.edu.cn·[FROM: 202.120.165.139] 

相关阅读:

TAG: 进程 国庆节 精华 ComputerTech hongernbsp

 

评分:0

我来说两句

显示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

数据统计

  • 访问量: 3470
  • 日志数: 8
  • 建立时间: 2008-01-19
  • 更新时间: 2008-04-13

RSS订阅

Open Toolbar