学习笔记04-学习《精通UNIX下C语言编程及项目实践》

 

  第三篇 并发程序设计

  业精于勤 , 而荒于嬉 .

  进程控制

  进程是程序的一次执行 , 是运行在自己的虚拟地址空间的一个具有独立功能的程序 . 进程是分配和释放资源的基本单位 , 当程序执行时 , 系统创建进程 , 分配内存和 CPU 等资源 ; 进程结束时 , 系统回收这些资源 .

  线程与进程

  线程又名轻负荷进程 , 它是在进程基础上程序的一次执行 , 一个进程可以拥有多个线程 .

  线程没有独立的资源 , 它共享进程的 ID, 共享进程的资源 .

  线程是 UNIX 中最小的调度单位 , 目前有系统级调度和进程级调度两种线程调度实行方式 : 系统级调度的操作系统以线程为单位进行调度 ; 进程级调度的操作系统仍以进程为单位进行调度 , 进程再为其上运行的线程提供调度控制

  环境变量

  UNIX , 存储了一系列的变量 , shell 下执行 'env' 命令 , 就可以得到环境变量列表 .

  环境变量分为系统环境变量和用户环境变量两种 . 系统环境变量在注册时自动设置 , 大部分具有特定的含义 ; 用户环境变量在 Shell 中使用赋值命令和 export 命令设置 . 如下例先设置了变量 XYZ, 再将其转化为用户环境变量 :

[bill@billstone Unix_study]$ XYZ=/home/bill

[bill@billstone Unix_study]$ env | grep XYZ

[bill@billstone Unix_study]$ export XYZ

[bill@billstone Unix_study]$ env | grep XYZ

XYZ=/home/bill

[bill@billstone Unix_study]$

  UNIX C 程序中有两种获取环境变量值的方法 : 全局变量法和函数调用法

  (a) 全局变量法

  UNIX 系统中采用一个指针数组来存储全部环境值 :

Extern char **environ;

  该法常用于将 environ 作为参数传递的语句中 , 比如后面提到的 execve 函数等 .

[bill@billstone Unix_study]$ cat env1.c

#include <stdio.h>

extern char **environ;

 

int main()

{

        char **p = environ;

 

        while(*p){

                fprintf(stderr, "%s/n", *p);

                p++;

        }

 

        return 0;

}

[bill@billstone Unix_study]$ make env1

cc     env1.c   -o env1

[bill@billstone Unix_study]$ ./env1

SSH_AGENT_PID=1392

HOSTNAME=billstone

DESKTOP_STARTUP_ID=

SHELL=/bin/bash

TERM=xterm

... ... ... ...

[bill@billstone Unix_study]

  (b) 函数调用法

  UNIX 环境下操作环境变量的函数如下 :

#include <stdlib.h>

char *getenv(char *name);

int putenv(const char *string);

  函数 getenv 以字符串形式返回环境变量 name 的取值 , 因此每次只能获取一个环境变量的值 ; 而且要使用该函数 , 必须知道要获取环境变量的名字 .

[bill@billstone Unix_study]$ cat env2.c

#include <stdio.h>

#include <stdlib.h>

 

int main(int argc, char **argv)

{

        int i;

 

        for(i=1;i<argc;i++)

                fprintf(stderr, "%s=%s/n", argv[i], getenv(argv[i]));

 

        return 0;

}

[bill@billstone Unix_study]$ make env2

cc     env2.c   -o env2

[bill@billstone Unix_study]$ ./env2 HOME LOGNAME rp

HOME=/home/bill

LOGNAME=bill

rp=(null)

[bill@billstone Unix_study]$

  在进程中执行新程序的三种方法

  进程和人类一样 , 都有创建 发展 休眠和死亡等各种生命形态 . 其中 , 函数 fork 创建新进程 , 函数 exec 执行新程序 , 函数 sleep 休眠进程 , 函数 wait 同步进程和函数 exit 结束进程 .

  (1) fork-exec

  调用 fork 创建的子进程 , 将共享父进程的代码空间 , 复制父进程数据空间 , 如堆栈等 . 调用 exec 族函数将使用新程序的代码覆盖进程中原来的程序代码 , 并使进程使用函数提供的命令行参数和环境变量去执行新的程序 .

  exec 函数族有六个函数如下 :

#include <unistd.h>

int execl(const char *path, const char *arg0, ..., (char *)0);

int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);

int execlp(const char *file, const char *arg0, ..., (char *)0);

int execv(const char *path, const char *argv[]);

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

int execvp(const char *file, const char *argv[]);

extern char **environ;

  如何用 fork-exec 方式执行程序 'uname -a?

[bill@billstone Unix_study]$ cat exec1.c

#include <sys/types.h>

#include <unistd.h>

#include <stdio.h>

 

int main()

{

        pid_t pid;

 

        if((pid = fork()) == 0){

                fprintf(stderr, "---- begin ----/n");

                // sleep(3);               // 睡眠 3 秒会导致子进程成为僵死进程

                execl("/bin/uname", "uname", "-a", 0);

                fprintf(stderr, "----  end  ----/n");

        }

        else if(pid > 0)

                fprintf(stderr, "fork child pid = [%d]/n", pid);

        else

                fprintf(stderr, "Fork failed./n");

 

        return 0;

}

[bill@billstone Unix_study]$ make exec1

cc     exec1.c   -o exec1

[bill@billstone Unix_study]$ ./exec1

---- begin ----

Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux

fork child pid = [13276]               

[bill@billstone Unix_study]$ ./exec1

---- begin ----

fork child pid = [13278]                      

[bill@billstone Unix_study]$ Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux

  (2) vfork-exec

  vfork 比起 fork 函数更快 , 二者的区别如下 :

  a) vfork 创建的子进程并不复制父进程的数据 , 在随后的 exec 调用中系统会复制新程序的数据到内存 , 继而避免了一次数据复制过程

  b) 父进程以 vfork 方式创建子进程后将被阻塞 , 知道子进程退出或执行 exec 调用后才能继续运行 .

  当子进程只用来执行新程序时 , vfork-exec 模型比 fork-exec 模型具有更高的效率 , 这种方法也是 Shell 创建新进程的方式 .

[bill@billstone Unix_study]$ cat exec2.c

#include <sys/types.h>

#include <unistd.h>

#include <stdio.h>

 

int main()

{

        pid_t pid;

 

        if((pid = vfork ()) == 0){

                fprintf(stderr, "---- begin ----/n");

                sleep(3);

                execl("/bin/uname", "uname", "-a", 0);

                fprintf(stderr, "----  end  ----/n");

        }

        else if(pid > 0)

                 fprintf(stderr, "fork child pid = [%d]/n", pid);

        else

                fprintf(stderr, "Fork failed./n");

 

        return 0;

}

[bill@billstone Unix_study]$ make exec2

make: `exec2' is up to date.

[bill@billstone Unix_study]$ ./exec2

---- begin ----

fork child pid = [13293]               

[bill@billstone Unix_study]$ Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux

 

  (3) system

  UNIX , 我们也可以使用 system 函数完成新程序的执行 .

#include <stdlib.h>

char *getenv(char *name);

int putenv(const char *string);

  函数 system 会阻塞调用它的进程 , 并执行字符串 string 中的 shell 命令 .

[bill@billstone Unix_study]$ cat exec3.c

#include <unistd.h>

#include <stdio.h>

 

int main()

{

        char cmd[] = {"/bin/uname -a"};

 

        system(cmd);

 

        return 0;

}

[bill@billstone Unix_study]$ make exec3

cc     exec3.c   -o exec3

[bill@billstone Unix_study]$ ./exec3

Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux

[bill@billstone Unix_study]$

   僵死进程

  僵死进程是已经终止 , 但没有从进程表中清除的进程 , 下面是一个僵死进程的实例

[bill@billstone Unix_study]$ cat szomb1.c

#include <unistd.h>

#include <stdio.h>

#include <sys/types.h>

 

int main()

{

        pid_t pid;

 

        if((pid = fork()) == 0){

                printf("child[%d]/n", getpid());

                exit(0);

        }

        // wait();

        printf("parent[%d]/n", getpid());

        sleep(10);

 

        return 0;

}

[bill@billstone Unix_study]$

  后台运行程序 szobm1, 并在子进程结束后 , 父进程没有结束前 , 运行命令查询进程情况 :

[bill@billstone Unix_study]$ make szomb1

cc     szomb1.c   -o szomb1

[bill@billstone Unix_study]$ ./szomb1 &

[2] 13707

child[13708]

[bill@billstone Unix_study]$ parent[13707]

ps -ef | grep 13707

bill     13707  1441  0 04:17 pts/0    00:00:00 ./szomb1

bill     13708 13707  0 04:17 pts/0    00:00:00 [szomb1 <defunct>]     // 僵死进程

bill     13710  1441  0 04:17 pts/0    00:00:00 grep 13707

[bill@billstone Unix_study]$

  其中 , 'defunct' 代表僵死进程 . 对于僵死进程 , 不能奢望通过 kill 命令杀死之 , 因为它已经 ' ' , 不再接收任何系统信号 .

  当子进程终止时 , 它释放资源 , 并且发送 SIGCHLD 信号通知父进程 . 父进程接收 SIGCHLD 信号 , 调用 wait 返回子进程的状态 , 并且释放系统进程表资源 . 故如果子进程先于父进程终止 , 而父进程没有调用 wait 接收子进程信息 , 则子进程将转化为僵死进程 , 直到其父进程结束 .

  一旦知道了僵死进程的成因 , 我们可以采用如下方法预防僵死进程 :

  (1) wait

  父进程主动调用 wait 接收子进程的死亡报告 , 释放子进程占用的系统进程表资源 .

  (2) 托管法

  如果父进程先于子进程而死亡 , 则它的所有子进程转由进程 init 领养 , 即它所有子进程的父进程 ID 号变为 1. 当子进程结束时 init 为其释放进程表资源 .

  (3) 忽略 SIGC(H)LD 信号

  当父进程忽略 SIGC(H)LD 信号后 , 即使不执行 wait, 子进程结束时也不会产生僵死进程 .

  (4) 捕获 SIGC(H)LD 信号

  当父进程捕获 SIGC(H)LD 信号 , 并在捕获函数代码中等待 (wait) 子进程

  守护进程

  所谓守护进程是一个在后台长期运行的进程 , 它们独立于控制终端 , 周期性地执行某项任务 , 或者阻塞直到事件发生 , 默默地守护着计算机系统的正常运行 . UNIX 应用中 , 大部分 socket 通信服务程序都是以守护进程方式执行 .

  完成一个守护进程的编写至少包括以下几项 :

  (1) 后台执行

  后台运行的最大特点是不再接收终端输入 , 托管法可以实现这一点

pid_t pid;

pid = fork();

if(pid > 0) exit(0);       // 父进程退出

/* 子进程继续运行   */

父进程结束 , shell 重新接管终端控制权 , 子进程移交 init 托管

  (2) 独立于控制终端

  在后台进程的基础上 , 脱离原来 shell 的进程组和 session , 自立门户为新进程组的会话组长进程 , 与原终端脱离关系

#include <unistd.h>

pid_t setsid();

  函数 setsid 创建一个新的 session 和进程组 .

  (3) 清除文件创建掩码

  进程清除文件创建掩码 , 代码如下 :

umask(0);

  (4) 处理信号

  为了预防父进程不等待子进程结束而导致子进程僵死 , 必须忽略或者处理 SIGCHLD 信号 , 其中忽略该信号的方法为 :

signal(SIGCHLD, SIG_IGN);

  守护进程独立于控制终端 , 它们一般以文件日志的方式进行信息输出 .

  下面是一个简单的守护进程实例 InitServer

[bill@billstone Unix_study]$ cat initServer.c

#include <assert.h>

#include <signal.h>

#include <sys/wait.h>

#include <sys/types.h>

 

void ClearChild(int nSignal){

        pid_t pid;

        int nState;

                    //  WNOHANG 非阻塞调用 waitpid, 防止子进程成为僵死进程

         while((pid = waitpid(-1, &nState, WNOHANG)) > 0);

        signal(SIGCLD, ClearChild);    // 重新绑定 SIGCLD 信号

}

 

int InitServer(){

        pid_t pid;

 

        assert((pid = fork()) >= 0);        // 创建子进程

        if(pid != 0){               // 父进程退出 , 子进程被 init 托管

                sleep(1);

                exit(0);

        }

        assert(setsid() >= 0);           // 子进程脱离终端

        umask(0);                    // 清除文件创建掩码

        signal(SIGINT, SIG_IGN);      // 忽略 SIGINT 信号

        signal(SIGCLD, ClearChild);      // 处理 SIGCLD 信号 , 预防子进程僵死

 

        return 0;

}

 

int main()

{

        InitServer();

        sleep(100);

 

        return 0;

}

[bill@billstone Unix_study]$ make initServer

cc     initServer.c   -o initServer

[bill@billstone Unix_study]$ ./initServer

[bill@billstone Unix_study]$ ps -ef | grep initServer

bill     13721     1  0 04:40 ?        00:00:00 ./initServer      // '?' 代表 initServer 独立于终端

bill     13725  1441  0 04:41 pts/0    00:00:00 grep initServer

[bill@billstone Unix_study]$

  程序在接收到 SIGCLD 信号后立即执行函数 ClearChild, 并调用非阻塞的 waitpid 函数结束子进程结束信息 , 如果结束到子进程结束信息则释放该子进程占用的进程表资源 , 否则函数立刻返回 . 这样既保证了不增加守护进程负担 , 又成功地预防了僵死进程的产生 .

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值