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

1、进程标识符

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

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

2、 fork系统调用

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

小程序


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



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


(3)fork系统调用须要注意的地方:数据结构

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

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


(4)写时复制 copy on write测试

     若是多个进程要读取它们本身的那部分资源的副本,那么复制是没必要要的。
     每一个进程只要保存一个指向这个资源的指针就能够了。
     若是一个进程要修改本身的那份资源的“副本”,那么就会复制那份资源。这就是写时复制的含义ui

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

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


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


子进程继承了父进程打开的文件描述符,故每一个打开文件的引用计数为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缓冲区


3、exit函数

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

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

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

 

4、僵尸进程

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

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

 

5、如何避免僵尸进程

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

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

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

 

6、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非零,返回一个信号代码

 

7、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的绝对值的任一子进程。

 

8、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

9、 exec替换进程映象

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


10、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是一个系统调用。



示例程序:

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

#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 ...

相关文章
相关标签/搜索