进程控制

进程控制介绍

  进程控制中涉及到进程创建、睡眠、退出等,在Linux中提供fork、clone的进程创建方法,sleep的进程睡眠,exit的进程终止调用。

 

主要的系统调用

  

 

   下面将具体介绍重要的系统调用的代码实现。

 

 

fork创建进程

  我们可输入man 2 fork查看该函数的声明

  

   由图可知函数声明在头文件<unistd.h>中,且fork的类型为pid_t,这个类型与进程标识符PID的类型是一样的,所以我们这样写代码也是OK的:

pid_t pid;
pid = fork();  

 

  fork的中文名为分叉,十分形象。在每次执行fork后,就会产生一个新的进程,这样就分叉了,当前进程称为子进程,原来的进程称为父进程。

  fork的特点在于一次性返回两个值。先来分析下面的代码:

int main()
{
  int i;
  if (fork() == 0)
  {
    for (i = 1; i < 3; i++)
    printf("This is child process\n");
  }
  else
  {
    for (i = 1; i < 3; i++)
    printf("This is parent process\n");
  }
}

  执行结果如下:

This is child process
This is child process
This is parent process
This is parent process
exe

  if和else里头的代码都执行了,说明fork()的返回值必然一个为0另一个为非0。

  通过查阅资料知道,在执行fork()时,由于fork创建了一个子进程,所以fork()会将子进程的PID(进程标识符为一个正值)返回给父进程,而将0返回给刚创建的子进程。如果想得到父进程的PID,用函数getppid(),如果想得到子进程的PID,则用函数getpid(),这两个函数的返回值类型都是int。

  额外补充一点,fork()创建失败的话,返回值为-1,失败的具体情况有下两点:

    1. 父进程拥有的子进程个数超过了限制。
    2. 提供使用的内存不足

  另外我们把for循环的次数放大,比如每个for循环执行5000次,会发现一个有趣的现象,即开始交错打印:

  

  则明显地看到父进程与子进程的并发执行。一般来说是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。但是由于操作系统一般让所有进程都享有相同的执行权,除非另一个优先权更高。所以这里的父进程与子进程的执行会交替进行。

  如何避免父进程与子进程的并发执行呢?可以使用vfork()函数,它的语法与fork()一致。vfork的特性之一是保证子进程先执行,等到它调用exit();或者exec的时候,父进程才可以调度执行。

  vfork的另一个特性是用其创建的子进程共享父进程的地址空间,也就是说子进程完全运行在父进程的地址空间之上。若子进程与父进程有相同的局部变量、全局变量,那么子进程修改任一变量的值,父进程的对应变量的值也被修改。而fork则不同,子进程与父进程是独立的,这就是说子进程里修改任一变量的值,父进程是不受影响的。而且正因为fork创建的子进程的地址空间的与父进程的地址空间是彼此独立,所以父进程才要将几乎是一切的资源都拷入子进程中,这是非常浪费的行为。

  再说一个fork的特性,就是子进程会继承父进程的数据段、堆栈段,还有缓冲区。这意味着如果你不刷新缓冲区,之前在父进程缓冲区中的数据还会被继承到子进程中。刷新缓冲区的办法有:1、printf中的\n  2.fflush(stdout); 。

  举例:

//例一
printf("hello ");
fork();
printf("world  ");  //最后输出hello world  hello world

//例二
printf("hello ");
fflush(stdout);
fork();
printf("world  ");  //最后输出hello world world

//例三
printf("hello\n");
fork();
printf("world  ");  //最后输出hello world world

 

 

 

exit进程退出

  Linux系统中进程退出的方式分为正常退出和异常退出两种,其中正常退出的方法有三种,异常退出的方法有两种。

  正常退出: 1.main函数中的return 2.调用exit 3.调用_exit

  异常退出:1.调用about 2.进程接收到某个信号而终止

  return与exit的区别是:exit是有参数,return是执行完函数后的返回;exit结束后将控制权交给系统,return结束后将控制权交给调用函数

  exit与_exit的区别是:exit在头文件stdlib.h声明,_exit在头文件unistd.h声明。结束进程后,_exit会立即返回给内核,而exit要清除一段指令后才返回给内核。

 

 

exec进程执行新程序

   一般在子进程中会使用exec函数来执行另一个程序。系统调用exec用于执行一个可执行程序以代替当前的程序,也就是说某进程一旦调用了 exec 类函数,正在执行的程序就被干掉了,系统把代码段替换成新的程序(由 exec 类函数执行)的代码,并且原有的数据段和堆栈段也被废弃,新的数据段与堆栈段被分配,但是进程号却被保留。

  所以exec的执行结果为:系统认为该进程还是原来的进程,但是进程里面的程序被替换了。

  下面是通过man 3 exec得到的exec函数族的相关信息

  

   将上面的库函数声明归纳后,如下图

  

  这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。

 

  这儿出现了一个陌生的外部全局变量 char **environ,这是什么呢? 它是环境变量。

  所谓环境变量,就是为了便于用户灵活地使用shell,Linux引入了环境变量的概念,包括了用户的主目录、当前目录、终端类型等,它们定义了用户的工作环境,所以称为环境变量。通过命令env可查看这些环境变量值,或者通过以下方式也能实现查看环境变量值。

extern char **environ;
for(i=0; environ[i] != NULL; i++)
    printf("%s",environ[i]);

  另外,事实上main函数的完整形式是:int main(int argc, char *argv[], char **envp)。

  关于exec函数族的调用举例:

  

  事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,这些函数之间的关系如下图所示:

  

 

  实例:(实现IO重定向)把标准输入转成大写然后打印到标准输出

  分析:调用exec后,原来打开的文件描述符仍然是打开的。利用这一点可以实现I/O重定向。

/* upper.c */
#include <stdio.h>

int main(void)
{
    int ch;
    while((ch = getchar()) != EOF) {
    putchar(toupper(ch));
    }
    return 0;
}
upper.c

 

/* wrapper.c */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
    int fd;
    if (argc != 2) {
    fputs("usage: wrapper file\n", stderr);
    exit(1);
    }

    fd = open(argv[1], O_RDONLY);
    if(fd<0) {
    perror("open");
    exit(1);
    }

    dup2(fd, STDIN_FILENO);
    close(fd);

    execl("./upper", "upper", NULL);
    perror("exec ./upper");
    exit(1);
}
wrapper.c

    wrapper程序将命令行参数当作文件名打开,将标准输入重定向到这个文件,然后调用exec执行upper程序,这时原来打开的文件描述符仍然是打开的,upper程序只负责从标准输入读入字符转成大写,并不关心标准输入对应的是文件还是终端。

 

  

 wait等待进程结束

  当子进程先于父进程退出时,若无调用wait()或waitpid(),子进程就会进入僵尸进程。若调用,则不会变为僵尸进程。

  系统调用声明如下:

  

  wait():系统让父进程暂停执行直到它的一个子进程停止。返回值是该子进程的PID。

  waitpid():与wait同理,不过它是让特定的的子进程停止且需要提供PID。参数pid指明了要等待的子进程的PID。pid值的意义见下表。options 参数允许用户可改变waitpid()的行为,若将该参数赋值为WNOHANG,则父进程不被挂起而立即返回并执行其后的代码。

  

 

  它们都有int *status这个参数,这个参数指向的变量存放了子进程的(状态信息),比如子进程main()返回的值或者子进程中exit()的参数。

  让父进程周期性检查特定的子进程是否已经退出:

waitpid(child_pid, (int*)0, WNOHANG );

  如果子进程未退出,则返回0,若子进程已退出,则返回child_pid。调用失败则返回-1。失败的原因可能是子进程不存在、参数不合法等。

  注意:waitpid是wait的非阻塞版本,如果希望父进程在查看子进程状态的同时不被阻塞,可使用waitpid与WNOHANG选项。

  

 

  

 

  

未完待续  

posted @ 2017-09-27 23:19  bw98  阅读(269)  评论(0编辑  收藏  举报