UNIX环境高级编程——进程控制

一、进程标识符

     ID为0的进程是调度进程,常常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。init通常读与系统有关的初始化文件,并将系统引导到一个状态(例如多用户)。init进程决不会终止。它是一个普通的用户进程,但是它以超级用户特权运行。

pid_t getpid(void);          返回值:调用进程的进程ID
pid_t getppid(void);        返回值:调用进程的父进程ID
uid_t getuid(void);           返回值:调用进程的实际用户ID
uid_t geteuid(void);         返回值:调用进程的有效用户ID
gid_t getgid(void);           返回值:调用进程的实际组ID
gid_t getegid(void);         返回值:调用进程的有效组ID

二、 fork系统调用

包含头文件 <sys/types.h> 和 <unistd.h>
函数功能:创建一个子进程
函数原型
         pid_t  fork(void);
参数:无参数。
返回值:
如果成功创建一个子进程,对于父进程来说返回子进程ID
如果成功创建一个子进程,对于子进程来说返回值为0
如果为-1表示创建失败


(1)使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。

UNIX环境高级编程——进程控制_第1张图片


(2)子进程与父进程的区别在于:
1、父进程设置的锁,子进程不继承;
2、各自的进程ID和父进程ID不同;
3、子进程的未决告警被清除;
4、子进程的未决信号集设置为空集。


(3)fork系统调用需要注意的地方:

     fork系统调用之后,父子进程将交替执行。
     如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程,子进程是孤儿进程
     如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵进程。子进程退出会发送SIGCHLD信号给父进程,可以选择忽略或使用信号处理函数接收处理就可以避免僵尸进程。

     僵尸进程:一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。
     孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。


(4)写时复制 copy on write

     如果多个进程要读取它们自己的那部分资源的副本,那么复制是不必要的。
     每个进程只要保存一个指向这个资源的指针就可以了。
     如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源。这就是写时复制的含义

     例如fork就是基于写时复制,只读代码段是可以共享的。

     若使用vfork()则在还没调用exec之前,父子进程是共享同一个地址空间,不像fork()一样会进行拷贝 


(5)fork之后父子进程共享文件

UNIX环境高级编程——进程控制_第2张图片

子进程继承了父进程打开的文件描述符,故每个打开文件的引用计数为2


(6)fork与vfork

     在fork还没实现copy on write之前。Unix设计者很关心fork之后未立刻执行exec所造成的地址空间浪费,所以引入了vfork系统调用。
     vfork有个限制,子进程必须立刻执行_exit或者exec函数。
     即使fork实现了copy on write,效率也没有vfork高,但是我们不推荐使用vfork,因为几乎每一个vfork的实现,都或多或少存在一定的问题。


(7)fork和vfork的区别

vfork()用法与fork()相似.但是也有区别,具体区别归结为以下3点:

1.  fork():子进程拷贝父进程的数据段,代码段。vfork():子进程与父进程共享数据段

2.  fork():父子进程的执行次序不确定

     vfork():保证子进程先运行在调用exec或exit(注意:return也不行)之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。

3.  vfork()保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

4.  当需要改变共享数据段中变量的值,则拷贝父进程。

下面通过几个例子加以说明:

第一:子进程拷贝父进程的代码段的例子:

#include<sys/types.h>  
#include<unistd.h>  
#include<stdio.h>  
  
int main()  
{  
    pid_t pid;  
    pid = fork();  
    if(pid<0)  
        printf("error in fork!\n");  
    else if(pid == 0)  
        printf("I am the child process,ID is %d\n",getpid());  
    else   
        printf("I am the parent process,ID is %d\n",getpid());  
    return 0;  
  
} 
运行结果: 

I am the child process,ID is 4711  
I am the parent process,ID is 4710 


再来看一个拷贝数据段的例子: 

#include<sys/types.h>  
#include<unistd.h>  
#include<stdio.h>  
  
int main()  
{  
    pid_t pid;  
    int cnt = 0;  
    pid = fork();  
    if(pid<0)  
        printf("error in fork!\n");  
    else if(pid == 0)  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the child process,ID is %d\n",getpid());  
    }  
    else  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the parent process,ID is %d\n",getpid());  
    }  
    return 0;  
} 
运行结果: 

[root@localhost fork]# ./fork2  
cnt=1  
I am the child process,ID is 5077  
cnt=1  
I am the parent process,ID is 5076 

那么再来看看vfork ()吧。如果将上面程序中的fork ()改成vfork(),运行结果是什么 
样子的呢? 

[root@localhost fork]# gcc -o fork3 fork3.c   
[root@localhost fork]# ./fork3 
cnt=1
I am the child process,ID is 4520
cnt=2
I am the parent process,ID is 4519
cnt=1
I am the child process,ID is 4521
cnt=2
I am the parent process,ID is 4519
cnt=1
I am the child process,ID is 4522
cnt=2
后面无限循环

这样上面程序中的fork ()改成vfork()后,vfork ()创建子进程并没有调用exec 或exit,注意:就算是最后又执行return 0也是不行的。所以最终将导致死锁。 
怎么改呢?看下面程序: 

#include<sys/types.h>  
#include<unistd.h>  
#include<stdio.h>  
  
int main()  
{  
    pid_t pid;  
    int cnt = 0;  
    pid = vfork();  
    if(pid<0)  
        printf("error in fork!\n");  
    else if(pid == 0)  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the child process,ID is %d\n",getpid());  
       _exit(0);  
    }  
    else  
    {  
        cnt++;  
        printf("cnt=%d\n",cnt);  
        printf("I am the parent process,ID is %d\n",getpid());  
    }  
    return 0;  
  
} 
     如果没有_exit(0)的话,子进程没有调用exec 或exit,所以父进程是不可能执行的,在子 进程调用exec 或exit 之后父进程才可

能被调度运行。 所以我们加上_exit(0);使得子进程退出,父进程执行,这样else 后的语句就会被父进程执行, 又因在子进程调用

exec 或exit之前与父进程数据是共享的,所以子进程退出后把父进程的数 据段count改成1 了,子进程退出后,父进程又执行,最终就将count变成了2,看下实际 运行结果: 

[root@localhost fork]# gcc -o fork3 fork3.c   
[root@localhost fork]# ./fork3  
cnt=1  
I am the child process,ID is 4711  
cnt=2  
I am the parent process,ID is 4710 


示例程序:

/* 如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)
 * 如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,
 * 否则这个时候子进程就成为僵进程。
 */
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int main(int argc, char *argv[])
{
    signal(SIGCHLD, SIG_IGN); // 避免产生僵尸进程,忽略SIGCHLD信号
    printf("before fork pid=%d\n", getpid());
    int fd;
    fd = open("test.txt", O_WRONLY);
    if (fd == -1)
        ERR_EXIT("open error");

    pid_t pid;
    pid = fork(); // 写时复制copy on write,只读代码段可以共享
    /* 若使用vfork()则在还没调用exec之前,父子进程是共享同一个地址空间,
     * 不像fork()一样会进行拷贝 */
    if (pid == -1)
        ERR_EXIT("fork error");

    if (pid > 0)
    {
        printf("this is parent\n");
        printf("parent pid=%d child pid=%d\n", getpid(), pid);
        write(fd, "parent", 6); // 父子进程共享一个文件表
        sleep(10);
    }

    else if (pid == 0)
    {
        printf("this is child\n");
        printf("child pid=%d parent pid=%d\n", getpid(), getppid());
        write(fd, "child", 5);
    }

    return 0;
}
测试输出如下:

huangcheng@ubuntu:~$ ./a.out
before fork pid=5400
this is parent
parent pid=5400 child pid=5401
this is child
child pid=5401 parent pid=5400
huangcheng@ubuntu:~$ cat test.txt
parentchild

可以看到因为共享一个文件表,故文件偏移也共享,父子进程打印进test.txt文件的内容是紧随的而不是从头开始的。

测试输出如下:

huangcheng@ubuntu:~$ ./a.out > temp.out
huangcheng@ubuntu:~$ cat temp.out
before fork pid=5492
this is child
child pid=5493 parent pid=5492
before fork pid=5492                  //第二次输出
this is parent
parent pid=5492 child pid=5493

     标准I/O库是带缓冲的。如果标准输出连到终端设备,则它是行缓冲的,否则它是全缓冲的。当以交互方式运行该程序,只得到printf输出一次,其原因是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出两次。其原因是,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程。于是那时父、子进程各自有了带该行内容的标准I/O缓冲区


三、exit函数

     进程的最后一个线程在其启动例程中执行返回语句。但是,该线程的返回值不会用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

     进程的最后一个线程调用pthread_exit函数,这种情况下,进程的终止状态总是0,这与传送给pthread_exit的参数无关。

在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或者waitpid函数取得其终止状态。

 

四、僵尸进程

     当子进程退出的时候,内核会向父进程发送SIGCHLD信号,子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止)。
     一个已经终止,但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程为僵尸进程它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。
     父进程查询子进程的退出状态可以用wait/waitpid函数。

     利用命令ps,可以看到有标记为Z的进程就是僵尸进程。

 

五、如何避免僵尸进程

     当一个子进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行或者父进程调用了wait/waitpid才告终止。
     进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的wait/waitpid调用使用。它将称为一个“僵进程”。

     调用wait或者waitpid函数查询子进程退出状态,此方法父进程会被挂起(waitpid可以设置不挂起)。
     如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。也可以不忽略SIGCHLD信号,而接收在信号处理函数中调用wait/waitpid。

     杀死僵尸进程的办法:杀死进程的父进程,僵尸进程称为孤儿进程,过继给1号进程initinit始终会负责清理僵尸进程。

 

六、wait函数

     当一个进程正常或异常终止时,内核就向父进程发送SIGCHLD信号。

头文件<sys/types.h>和<sys/wait.h>
     函数功能:当我们用fork启动一个进程时,子进程就有了自己的生命,并将独立地运行。有时,我们需要知道某个子进程是否已经结束了,我们可以通过wait安排父进程在子进程结束之后。
函数原型:pid_t wait(int *status)
函数参数:status:该参数可以获得你等待子进程的信息
返回值:成功等待子进程函数返回等待子进程的ID

 

     wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
     返回的是子进程的PID,它通常是结束的子进程
     状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
     如果status不是一个空指针,状态信息将被写入它指向的位置

 

通过以下的宏定义可以获得子进程的退出状态

WIFEXITED(status) 如果子进程正常结束,返回一个非零值
WEXITSTATUS(status) 如果WIFEXITED非零,返回子进程退出码
WIFSIGNALED(status) 子进程因为捕获信号而终止,返回非零值
WTERMSIG(status) 如果WIFSIGNALED非零,返回信号代码
WIFSTOPPED(status) 如果子进程被暂停,返回一个非零值
WSTOPSIG(status) 如果WIFSTOPPED非零,返回一个信号代码

 

七、waitpid函数

函数功能: 用来等待某个特定进程的结束

函数原型: pid_t waitpid(pid_t pid, int *status,int options)
 参数:
         status:如果不是空,会把状态信息写到它指向的位置
         options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起等待
返回值:如果成功返回等待子进程的ID,失败返回-1

 

对于waitpid的p i d参数的解释与其值有关:
pid == -1      等待任一子进程。于是在这一功能方面waitpid与wait等效。
pid > 0          等待其进程ID与p i d相等的子进程。
pid == 0       等待其组ID等于调用进程的组I D的任一子进程。换句话说是与调用者进程同在一个组的进程。
pid < -1        等待其组ID等于p i d的绝对值的任一子进程。

 

八、wait和waitpid函数的区别

      两个函数都用于等待进程的状态变化包括正常退出,被信号异常终止,被信号暂停,被信号唤醒继续执行等。

     在一个子进程终止前, wait 使其调用者阻塞,waitpid 有一选择项,可使调用者不阻塞。
     waitpid并不只能等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
     实际上wait函数是waitpid函数的一个特例。

 

示例程序:

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/wait.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int main(int argc, char *argv[])
{
    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork error");

    if (pid == 0)
    {
        sleep(3);
        printf("this is child\n");
        //      exit(100);
        abort();
    }

    printf("this is parent\n");
    int status;
    int ret;
    ret = wait(&status); // 阻塞等待子进程退出
    //  ret = waitpid(-1, &status, 0);
    //  ret = waitpid(pid, &status, 0);
    /* waitpid可以等待特定的进程,而不仅仅是第一个退出的子进程
     * 且可以设置option为WNOHANG,即不阻塞等待 */
    printf("ret=%d, pid=%d\n", ret, pid);
    if (WIFEXITED(status))
        printf("child exited normal exit status=%d\n", WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        printf("child exited abnormal signal number=%d\n", WTERMSIG(status));
    else if (WIFSTOPPED(status))
        printf("child stopped signal number=%d\n", WSTOPSIG(status));

    return 0;
}
输出为:

huangcheng@ubuntu:~$ ./a.out
this is parent
this is child
ret=2195, pid=2195
child exited abnormal signal number=6
    说明子进程被信号异常终止,因为我们调用了abort(), 即产生SIGABRT信号将子进程终止,可以查到此信号序号为6。如果我们不使用abort 而是exit(100), 则应该输出:

huangcheng@ubuntu:~$ ./a.out
this is parent
this is child
ret=2214, pid=2214
child exited normal exit status=100

九、 exec替换进程映象

     在进程的创建上Unix采用了一个独特的方法,它将进程创建与加载一个新进程映象分离。这样的好处是有更多的余地对两种操作进行管理。当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用exec系列的函数来进行。当然,exec系列的函数也可以将当前进程替换掉。


十、exec关联函数组

包含头文件<unistd.h>
功能用exec函数可以把当前进程替换为一个新进程。exec名下是由多个关联函数组成的一个完整系列,头文件<unistd.h>
原型:

     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 execvp(const char *file, char *const argv[]);
     int execvpe(const char *file, char *const argv[],char *const envp[]);

参数
path参数表示你要启动程序的名称包括路径名
arg参数表示启动程序所带的参数
返回值:成功返回0,失败返回-1

execl,execlp,execle(都带“l”)的参数个数是可变的,参数以一个空指针结束。
execv、execvp和execvpe的第二个参数是一个字符串数组,新程序在启动时会把在argv数组中给定的参数传递到main

名字含字母“p”的函数会搜索PATH环境变量去查找新程序的可执行文件。如果可执行文件不在PATH定义的路径上,就必须把包括子目录在内的绝对文件名做为一个参数传递给这些函数。

名字最后一个字母为"e"的函数可以自设环境变量。

这些函数通常都是用execve实现的,这是一种约定俗成的做法,并不是非这样不可。

int execve(const char *filename, char *const argv[], char *const envp[]);

注意,前面6个函数都是C库函数,而execve是一个系统调用。


UNIX环境高级编程——进程控制_第3张图片

示例程序:

为了演示自设环境变量的功能,先写个小程序,可以输出系统的环境变量

#include<stdio.h>
#include<unistd.h>

extern char **environ;

int main(void)
{
    printf("hello pid=%d\n", getpid());
    int i;
    for (i = 0; environ[i] != NULL; i++)
        printf("%s\n", environ[i]);
    return 0;
}
其中environ是全局变量但没有在头文件中声明,所以使用前需要外部声明一下。输出如下:

huangcheng@ubuntu:~$ ./a.out
hello pid=5597
TERM=vt100
SHELL=/bin/bash
XDG_SESSION_COOKIE=0ba97773224d90f8e6cd57345132dfd0-1368605430.130657-1433620678
SSH_CLIENT=192.168.232.1 8740 22
SSH_TTY=/dev/pts/0
USER=simba
......................

即输出了一些系统环境的变量,变量较多,省略输出。

我们前面在讲到fcntl 函数时未讲到当cmd参数取F_SETFD时的情形,即设置文件描述符的标志,现结合exec系列函数讲解如下:

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
/* 这几个库函数都会调用execve这个系统调用 */
int main(int argc, char *argv[])
{
    char *const args[] = {"ls", "-l", NULL};
    printf("Entering main ... \n");
    //  execlp("ls", "ls", "-l", NULL); // 带p会搜索PATH
    //  execl("/bin/ls", "ls", "-l", NULL); // 带l为可变参数
    //  execvp("ls", args); //args数组参数传递给main
    //  execv("/bin/ls", args);

    int ret;
    //  ret = fcntl(1, F_SETFD, FD_CLOEXEC);
    /* FD_CLOSEXEC被置位为1(在打开文件时标志为O_CLOEXEC也会置位),
     * 即在执行execve时将标准输出的文件描述符关闭,
     * 即下面替换的pid_env程序不会在屏幕上输出信息
     */
    //  if (ret == -1)
    //      perror("fcntl error");

    char *const envp[] = {"AA=11", "BB=22", NULL};
    ret = execle("./pid_env", "pid_enV", NULL, envp); // 带e可以自带环境变量
    //  execvpe("ls", args, envp);
    if (ret == -1)
        perror("exec error");
    printf("Exiting main ... \n");

    return 0;
}
     我们使用了exec系列函数进行举例进程映像的替换,最后未被注释的execle函数需要替换的程序正是我们前面写的输出系统环境变量的小程序,但因为execle可以自设环境变量,故被替换后的进程输出的环境变量不是系统的那些而是自设的,输出如下:

huangcheng@ubuntu:~$ ./a.out
Entering main ... 
hello pid=5643
AA=11
BB=22

     如果我们将上面 fcntl 函数的注释打开了,即设置当执行exec操作时,关闭标准输出(fd=1)的文件描述符,也就是说下面替换的pid_env程序不会在屏幕上输出信息。

     因为如果替换进程映像成功,那么直接到替换进程的main开始执行,不会返回,故不会输出Exiting main ...



你可能感兴趣的:(UNIX环境高级编程——进程控制)