LInux进程控制与编程

.用fork创建一个新的进程,新进程几乎是当前进程的一个完全拷贝

 

.调用函数execve可以在进程中用另外的程序来替换当前运行的进程

 

.轻量级进程, 也称为线程, 提供了独立的执行线索和堆栈段,但却共享数据段, Linux特有的_clone调用用于支持线程,它通过指定共享的属性带来了更好的灵活性

 

.一个进程是一个正在执行的程序的实例,也是Linux基本的调度单位

 

.正在运行的程序的一个进程由如下元素组成


  1) 程序的当前上下文(context), 它是程序当前执行的状态
  2) 程序的当前执行目录
  3) 程序访问的文件和目录
  4) 程序的信任状态(credentials)或说访问权限, 比如它的文件模式和所有权
  5) 内存和其他分配给进程的系统资源

 

.内核使用进程来控制对CPU和其他系统资源的访问,并且使用进程来决定在CPU上运行哪个程序、运行多久以及采用什么特性运行它

 

.内核的调度器负责在所有的进程间分配CPU执行时间, 称为时间片(time slice), 它轮流在每个进程分得的时间片用完后从进程那里抢回控制权。 时间片非常小,小到让单处理器系统上的几个进程仿佛是在同时运行一样。 每个进程还包含了有关它们自身的充分信息,必要时内核能在执行与不执行它之间进行切换。

 

.进程也具有许多惟一定义它们的属性和特性。 进程的属性或特性能够把它们标识出来并且规定它们的行为。  内核内部还维护了关于每个进程的大量信息, 并且对外提供一个访问这些信息的接口。

 

.进程标识号


 1) 进程的最知名的属性就是它的进程号(prosess ID, PID) 和它的父进程号(parent process ID, PPID).
 2) 进程号是非零正整数
 3) 所有进程追溯其祖先最终都会落到进程号为1的进程身上, 这个进程叫做init进程。 init进程是内核自举后第一个启动的进程。  init引导系统、启动守护进程并且运行必要的程序。  init是所有进程的父进程
 
.PID的常见用法


 1) 创建惟一的文件名或目录。 例如, 当调用getpid后, 进程接着要用PID创建一个临时文件。  
 2) 把PID写入日志文件作为日志消息的一部分, 以清楚说明是哪个进程记录下的日志消息
 3) 可以用它的PPID向它的父进程发送信号或其他信息
    
示例: 打印PID和PPID

[cpp]  view plain copy
  1. /* 
  2.  * process_print_pid_ppid - Print PID and PPID 
  3.  */  
  4.   
  5. #include <stdio.h>  
  6. #include <unistd.h>  
  7. #include <stdlib.h>  
  8.   
  9. int main(void)  
  10. {  
  11.     printf("PID = %d /n", getpid());  
  12.     printf("PPID = %d /n", getppid());  
  13.     exit(EXIT_SUCCESS);  
  14. }  

 

[root@localhost c]# ./process_print_pid_ppid
PID = 5406
PPID = 5344

[root@localhost c]# ps aux | grep 5344
root      5344  0.0  0.1   6516  1616 pts/4    /bin/bash

 

.Real 和 Effective 标识号


每个进程的其他属性

 

属性名                  类型                 函数
---------------      ----------        ---------------
进程ID                 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)


1) 真实用户ID和真实用户组ID代表用户真实的身份, 当用户登录时从/etc/passwd文件中读取它们, 它们是你的登录名和主用户组成员身份的数字化表示

 

.SetUID 和 SetGID 程序


  1) 用来设置程序的有效用户ID和组ID


     就是设置程序/文件的执行者用户ID和组ID为新的用户ID和组ID, 而不是(除所有者)执行程序的用户的ID和组ID

 

    如用来更改口令文件/etc/passwd的程序passwd,  /etc/passwd这个文件所有用户都可以读, 但仅有root用户可以修改。


[root@106 bargainmng]$ ls -l /etc/passwd
-rw-r--r--  1 root root 1841  6月  5 13:36 /etc/passwd

 

    这样, 普通户要用/usr/bin/passwd命令修改这个文件时,必须要有超级用户的权限才行。


    解决的办法就是, /usr/bin/passwd程序是setUID为超级用户程序。 也就是,当执行/usr/bin/passwd程序时, 它的有效UID被设置为超级用户的UID, 从而让它能够修改/etc/passwd文件。


   [root@106 bargainmng]$ ls -l /usr/bin/passwd 
   -r-s--x--x  1 root root 27728 2005-10-08  /usr/bin/passwd


   这里可以看到在所有者的执行位上是一个s, 它代表了程序在执行时的有效用户ID为 文件所有者root的 用户ID, 也就是这时命令是代表所有者root来执行, 从而可以用来修改/etc/passwd文件。

 

 注:  setUID和setGID为超级用户的程序具有严重的安全风险, 因为即便只由普通用户来执行, 也会拥有超级用户的权利, 所以能够访问整个系统, 这样的程序能够破坏一切。 所以在执行或创建setUID或setGID为超级用户的程序时要极为小心。


.进程的信息


1)用户和用户组


  取得执行程序的用户登陆名


  #include <unistd.h>
  char *getlogin(void)

 

  根据登陆名取得/etc/passwd中的用户信息


  #include <pwd.h>
  struct passwd *getpwnam(const char *name);

 

示例:从/etc/passwd中取得当前操作程序的用户信息
[root@localhost c]# cat getname.c

[cpp]  view plain copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <sys/types.h>  
  4. #include <unistd.h>  
  5. #include <pwd.h>  
  6.   
  7. int main(void)  
  8. {  
  9.     char *login;  
  10.     struct passwd *pentry;  
  11.   
  12.     if((login = getlogin()) == NULL )  
  13.     {  
  14.         perror("getlogin");  
  15.         exit(EXIT_FAILURE);  
  16.     }  
  17.   
  18.     if((pentry = getpwnam(login)) == NULL)  
  19.     {  
  20.         perror("getpwnam");  
  21.         exit(EXIT_FAILURE);  
  22.     }  
  23.   
  24.     printf("user name: %s/n", pentry->pw_name);  
  25.     printf("passwd   : %s/n", pentry->pw_passwd);  
  26.     printf("UID      : %d/n", pentry->pw_uid);  
  27.     printf("GID      : %d/n", pentry->pw_gid);  
  28.     printf("gecos    : %s/n", pentry->pw_gecos);  
  29.     printf("home dir : %s/n", pentry->pw_dir);  
  30.     printf("Shell    : %s/n", pentry->pw_shell);  
  31.   
  32.   
  33.     exit(EXIT_SUCCESS);  
  34. }  

 

[root@localhost c]# ./getname
user name: root
passwd   : 143052813
UID      : 0
GID      : 0
gecos    : root
home dir : /root
Shell    : /bin/bash

 

2)资源利用情况和执行次数


  Wall clock time   (墙上时钟时间)      流逝的时间
  User CPU time     (用户CPU时间)      进程花在执行用户模式(非内核模式)代码上的时间总量
  System CPU time   (系统CPU时间)      花在执行内核代码上的时间总量

 

  通过调用times 或 getrusage 可以获得这信息, 前者能给出细致时间,后者可以给出更多信息,进程的利用情况, 比如它的内存占用量只能从getrusage调用获得。

 

  1> 进程计时


  #include <sys/times.h>
  clock_t time(struct tms *buf);

 

  times返回系统自举后经过的时间滴答数, 也称为墙上时钟时间。

  
[root@localhost c]# cat  get_times.c

 

[cpp]  view plain copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <sys/times.h>  
  4. #include <time.h>  
  5. #include <unistd.h>  
  6.   
  7. void doit(char *, clock_t);  
  8.   
  9. int main(void)  
  10. {  
  11.     clock_t  start, end;  
  12.     struct tms t_start, t_end;  
  13.     start = times(&t_start);  
  14.     system("grep the /usr/share/doc/* > /dev/null ");  
  15.     end=times(&t_end);  
  16.   
  17.     doit("elpapsed", end - start);  
  18.   
  19.     puts("parent times");  
  20.     doit("/tuser CPU", t_end.tms_utime);  
  21.     doit("/tsys CPU",  t_end.tms_stime);  
  22.   
  23.     puts("child times");  
  24.     doit("/tuser CPU", t_end.tms_cutime);  
  25.     doit("/tsys CPU", t_end.tms_cstime);  
  26.   
  27.     exit(EXIT_SUCCESS);  
  28.   
  29. }  
  30.   
  31. void doit(char *str, clock_t time)  
  32. {  
  33.     /* Get clock ticks/second */  
  34.     long tps = sysconf(_SC_CLK_TCK);  
  35.     printf("%s: %6.2f secs /n", str, (float)time/tps);  
  36. }  

 

[root@localhost c]# ./get_times
elpapsed:   0.05 secs
parent times
        user CPU:   0.00 secs
        sys CPU:   0.00 secs
child times
        user CPU:   0.02 secs
        sys CPU:   0.00 secs

 

    当程序调用system函数时, 它先产生一个子进程, 然后是子进程而不是父进程完成所有工作并消耗了CPU时间
  
    进程的执行时间0.05并不等于用户CPU时间和系统CPU时间之和 0.02秒。  原因是子进程执行的grep操作是I/O密集型而非CPU密集型的操作。 它扫描了这里讨论所使用的系统上的多个文件, 缺少的0.03秒全部用从硬盘读取数据。

 

    times返回的时间是相对而非绝对时间(系统自举后经过的时钟滴答数), 所以要让它有实用价值, 就必须做两次测量并使用它们的差值。 这就引入了 流逝时间, 或者称为墙上时钟时间。 resusg1通过把起止的时钟滴答数分别保存在start和end中来做到这一点。 另一种进程计时值可从<sys/times.h>中定义的tms结构中获得。 tms结构保存着一个进程及其子进程的当前CPU时间。


   struct tms{
         clock_t  tms_utime;   /* user cpu time */
         clock_t  tms_stime;   /* system cpu time */
         clock_t  tms_cutime;  /* user cpu time of children */
         clock_t  tms_cstime;  /* system cpu time of children */
   };

 

   注: 这些时间都是时钟滴答数,而不是秒数。 使用sysconf函数能把时钟滴答数转为秒数, 这个函数把它的参数转换成在运行时定义的系统限制值或选项值, 返回类型为long


   _SC_CLK_TCK 是定义每秒钟有多少滴答的宏。


   这个程序的关键处是doit函数, 它接受一个字符串指针和一个clock_t类型的值,然后计算并输出进程每部分实际的计时信息。


.资源利用

 进程的资源利用包括的内容不只是CPU时间, 你还要考虑进程的内存使用量、内存如何进行组织、进程访问内存方式的类型、进程执行I/O操作的种类和数量以及进程产生的网络活动的数量和种类。 内核会为每个进程跟踪所有这些信息, 甚至还有更多的东西。 至少内核有能力做到这一点


 #include  <sys/resource.h>
 struct rusage{
     struct timeval  ru_utime;   /* user time useed */
     struct timeval  ru_sutime;  /* system time used */
     long ru_maxrss;             /* maxium resident set size */
     long ru_maxixrss;           /* shared memory size */
     long ru_maxidrss;           /* unshared data size */
     long ru_maxisrss;           /* unshared stack size */
     long ru_minflt;             /* page reclaims */
     long ru_majflt;             /* page faults */
     long ru_nswap;              /* swaps */
     long ru_inblock;            /* block input operations */
     long ru_outblock;           /* block output operations */
     long ru_msgsnd;             /* messages sent */
     long ru_msgrcv;             /* messages recieved */
     long ru_nsignals;           /* signals received */
     long ru_nvcsw;              /* voluntary context switches */
     long ru_nivcsw;             /* involuntary context swicthes */
 };

 

 不过现在LINUX现在只能列出如下内容:


  ru_utime          执行用户模式(非内核)代码时间
  ru_stime          执行内核代码(用户代码对系统服务的请求)的时间
  ru_minflt         次要失效(minor fault)数(内存访问没有引起磁盘访问)
  ru_majflt         主要失效(major fault)数(内存访问引起磁盘访问)
  ru_nswap          国主要错误而从磁盘读取的内存页数

 

从上面可以看出, 有两种类型的内存失效: 次要失效和主要失效。


  1> 当CPU必须访问主存(RAM)而不是从L2或L1高速缓存(X86体系结构上分别是level 1或 level 2级高速缓存)、CPU的片上或高速缓冲存储器中读取数据时, 就发生了次要失效() 。 出现这种失效的原因是因为CPU需要的代码或数据不在寄存器或高速缓存中。

  2> 当进程因所需要代码或数据不在RAM中而必須从磁盘读取数据时就发主要失效(major fault).  ru_nswap 保存了因出现主要失效而必須从磁盘读取的内存页面数量

#include <sys/times.h>
#include <sys/resource.h>
#include <unistd.h>
int getrusage(int who,   /* 返回进程RUSAGE_SELF还是子进程USAGE_CHILDREN的信息 */
              struct rusage *usage  /* 函数要填充的结构指针 */ 
              );

 
[root@localhost c]# cat get_rusage.c

 

[cpp]  view plain copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <sys/times.h>  
  4. #include <sys/resource.h>  
  5. #include <time.h>  
  6. #include <unistd.h>  
  7.   
  8. void err_quit(char *);  
  9. void doit(char *, long);  
  10.   
  11. int main(void)  
  12. {  
  13.     struct rusage  usage;  
  14.     system("grep -R  the  /usr/share > /dev/null 2> /dev/null");  
  15.   
  16.     if((getrusage(RUSAGE_SELF, &usage)) == -1)  
  17.          err_quit("getrusage");  
  18.   
  19.     puts("parent times");  
  20.     doit("/tuser CPU", usage.ru_utime.tv_sec);  
  21.     doit("/tsys  CPU", usage.ru_stime.tv_sec);  
  22.   
  23.   
  24.     puts("parent memory stats");  
  25.     doit("/tminor faults", usage.ru_minflt);  
  26.     doit("/tmajor faults", usage.ru_majflt);  
  27.     doit("/tpage   swaps", usage.ru_nswap);  
  28.   
  29.   
  30.     if((getrusage(RUSAGE_CHILDREN, &usage)) == -1)  
  31.          err_quit("getrusage");  
  32.   
  33.     puts("parent times");  
  34.     doit("/tuser CPU", usage.ru_utime.tv_sec);  
  35.     doit("/tsys  CPU", usage.ru_stime.tv_sec);  
  36.   
  37.   
  38.     puts("parent memory stats");  
  39.     doit("/tminor faults", usage.ru_minflt);  
  40.     doit("/tmajor faults", usage.ru_majflt);  
  41.     doit("/tpage   swaps", usage.ru_nswap);  
  42.   
  43.     exit(EXIT_SUCCESS);  
  44. }  
  45.   
  46. void doit(char *str, long resval)  
  47. {  
  48.     printf("%s: %ld/n", str, resval);  
  49. }  
  50.   
  51. void err_quit(char *str)  
  52. {  
  53.     perror(str);  
  54.     exit(EXIT_FAILURE);  
  55. }  

 

[root@localhost c]# ./get_rusage
parent times
        user CPU: 0
        sys  CPU: 0
parent memory stats
        minor faults: 12
        major faults: 92
        page   swaps: 0
parent times
        user CPU: 4
        sys  CPU: 2
parent memory stats
        minor faults: 180
        major faults: 12030
        page   swaps: 0

 

使用getrusage函数能比较清楚的了解进程的内存使用情况, 运行中出现的主要失效数目证实了前面有关本程序需要大量磁盘I/O操作的讨论。 进程因所需数据不在内存中而从磁盘读取数据多达12 122次, 但是没有发生页交换(swaps)

 

.会话和进程组


 1> 进程组


   如, 管道 ls -l | sort | more


   这时的进程间的关系不是父子进程,而是同一进程组的成员关系。


   进程组(process group)是相关进程的一个集合, 这些相关进程通常是在一个管道中的命令序列。


   在进程组中的所有进程都具有相同的进程组号, 即PGID. 使用进程组的目的是为了方便作业控制。 例如, 假定你运行 了上面的命令, 如果要在运行中杀死它(CTRL+C) , 则能终止所的进程, 它是通过杀死进程组而不是每一个进程来做到这一点的。

 

2> 会话(session)


   由一个或多个进程组构成。


   会话领导(session leader)进程是创建会话的进程


   每个会话都有惟一的标识号, 称为会话ID(session ID), 它只是会话领导进程的PID。


   会话对进程组起的作用和进程组对单个进程起的作用一样。
   
   假如在后台执行了前面提到的管道命令(ls -l | sort |more) , 并且还在前台执行其他命令。 现在, 如果在一个X Window终端里运行这些命令, 并且在所有进程正在运行的时候关闭终端窗口,则内核会向控制进程(会话领导进程)发送一个信号, 接着会话领导进程逐个杀死它的各个进程组。

 

.创建进程

1) 使用system函数


#include <stdlib.h>
int system(const char *string)

 

   把system传递给/bin/sh -c来执行string所指定的命令, string中可以包含选项和参数, 接着整个命令行(/bin/sh -c string)又传递给系统调用execve。 

   如果没有找到/bin/sh, system返回127, 如果出现其他错误则返回-1, 如果执行成功则返回string的代码。 但如果string为NULL, system返回一个非0值, 否则返回0

 

[root@localhost c]# cat c_system.c

 

[cpp]  view plain copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main(void)  
  5. {  
  6.     int retval;  
  7.     retval = system("ls -l");  
  8.   
  9.     if(retval == 127)  
  10.     {  
  11.         fprintf(stderr, "/bin/sh not available/n");  
  12.         exit(127);  
  13.     }  
  14.     else if(retval == -1)  
  15.     {  
  16.        perror("system");  
  17.        exit(EXIT_FAILURE);  
  18.     }  
  19.     else if(retval !=0)  
  20.     {  
  21.         fprintf(stderr, "command returned %d/n", retval);  
  22.         perror("ls");  
  23.     }  
  24.     else  
  25.     {  
  26.         puts("command successfully executed");  
  27.     }  
  28.     exit(EXIT_SUCCESS);  
  29. }  

 

[root@localhost c]# ./c_system
总计 64
-rwxr-xr-x 1 root root 5449 06-07 15:18 c_system
-rw-r--r-- 1 root root  509 06-07 15:18 c_system.c
-rwxr-xr-x 1 root root 5397 06-07 12:30 getname
-rw-r--r-- 1 root root  750 06-07 12:30 getname.c
-rw-r--r-- 1 root root 1472 06-07 12:39 getname.o
-rwxr-xr-x 1 root root 5713 06-07 14:28 get_rusage
-rw-r--r-- 1 root root 1208 06-07 14:28 get_rusage.c
-rwxr-xr-x 1 root root 5472 06-07 13:05 get_times
-rw-r--r-- 1 root root  756 06-07 13:05 get_times.c
-rwxr-xr-x 1 root root 5031 06-07 10:11 process_print_pid_ppid
-rw-r--r-- 1 root root  234 06-07 10:11 process_print_pid_ppid.c
command successfully executed

 

2)fork 系统调用


  fork调用创建一个新进程, 新进程或子进程是调用进程或者说父进程的副本。


  #include <unistd.h>
  pid_t fork(void)

 

  如果fork执行成功, 就向父进程返回子进程的PID, 并向子进程返回0.  这意味着既使只调用fork一次, 它也会返回两次。

 

  fork创建的新进程是和父进程(除了PID和PPID)一样的副本, 包括真实和有效UID和GID、进程组和会话ID、环境、资源限制、打开的文件以及共享内存段。
   
  父进程和子进程之间有一点区别。 子进程没有继承父进程的超时设置(使用alarm调用)、父进程创建的文件锁, 或者未决信号。 要理解的关键概念是fork创建的新进程是父进程的一个标准副本。

 

[root@localhost c]# cat c_fork.c

 

[cpp]  view plain copy
  1. #include <unistd.h>  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4.   
  5. int main(void)  
  6. {  
  7.     pid_t child;  
  8.     puts("before fork:/n");  
  9.     printf("/tchild pid = %d/n", getpid());  
  10.     printf("/tparent ppid= %d/n", getppid());  
  11.   
  12.     puts("/nafter fork:/n");  
  13.     if((child = fork()) == -1)  
  14.     {  
  15.         perror("fork");  
  16.         exit(EXIT_FAILURE);  
  17.     }else if(child == 0)  
  18.     {  
  19.       puts("in child");  
  20.       printf("/tchild pid = %d/n", getpid());  
  21.       printf("/tparent ppid= %d/n", getppid());  
  22.   
  23.     }else {  
  24.       puts("in parent");  
  25.       printf("/tchild pid = %d/n", getpid());  
  26.       printf("/tparent ppid= %d/n", getppid());  
  27.     }  
  28.     exit(EXIT_SUCCESS);  
  29. }  

 

[root@localhost c]# ./c_fork
before fork:

        child pid = 7660
        child ppid= 5344

after fork:

in parent
        child pid = 7660
        child ppid= 5344
in child
        child pid = 7661
        child ppid= 1

 

[root@localhost c]# ./c_fork
before fork:

        child pid = 7662
        child ppid= 5344

after fork:

in child
        child pid = 7663
        child ppid= 7662
in parent
        child pid = 7662
        child ppid= 5344

 

注:


    子进程的PPID(父进程ID)和父进程的PID 一样


    不能预计父进程是在子进程产生前还是后才执行,  也就是说两者是异步的


    因为异步性, 所以不应在子进程中执行依赖于父进程的代码, 反之亦然


    fork执行可能失败,原因可以是系统上已经运行了太多的进程, 也可以是试图fork的UID已经超过了允许它执行的进程数。失败返回给父进程-1, 并且不创建子进程。 
   

.exec函数族 
 

exec函数实际上是包含了6个函数的函数族, 这6个函数中的每一个都在调用规则和用法上略有区别。 无论其功能的多样化如何, 还是习惯上称它们为exec函数。 
 
 和fork类似, exec也在<unistd.h>中声明。


 #include <unistd.h>
 int execl(const char *path, const char *arg, ...);
 int execlp(const char *file, const char *arg, ...);
 int execle(const char *path, const *arg, char *const envp[]);
 int execv(const char *path, char *const argv[]);
 int execve(const char *path, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);

 

 exec用被执行的程序完全替换了调用进程的映像。


 fork创建一个新进程就产生了一个新的PID


 exec启动一个新程序, 替换原有的进程, 因此,被执行的进程PID不会改变。

 

 execve接受3个参数: path、argv和envp


    path 是要执行的二进制文件或脚本的完整路径, 
    argv 是要传递给程序的完整参数列表,包括argv[0] ,它一般是执行程序的名字。
    envp 是指向用于执行execed程序的专门环境的指针(在示例程序中为NULL)
    
[root@localhost c]# cat c_execve.c

 

[cpp]  view plain copy
  1. #include <unistd.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4.   
  5. int main(void)  
  6. {  
  7.     char *args[] = {"/bin/ls", NULL};  
  8.   
  9.     puts("carry on execve: /n");  
  10.     if(execve("/bin/ls", args, NULL) == -1)  
  11.     {  
  12.         perror("execve");  
  13.         exit(EXIT_FAILURE);  
  14.     }  
  15.   
  16.     puts("shouldn't get here");  
  17.     exit(EXIT_SUCCESS);  
  18. }  

 

[root@localhost c]# ./c_execve
carry on execve:

c_execve    c_fork    c_system    get_rusage    get_times    getname    getname.o               process_print_pid_ppid.c
c_execve.c  c_fork.c  c_system.c  get_rusage.c  get_times.c  getname.c  process_print_pid_ppid

 

注:


   1> 从结果可以看出 execve执行成功后, 没有继续进行父进程的puts("shouldn't get here"); 操作,而直接返回了shell。 也就是execve 用被执行的程序(新程序)完全替换了调用进程的映像 , 不可能再次返回父进程

 

   2> 但如果执行失败后,也不继续进行父进程的puts("shouldn't get here"); ,直接返回自己的状态符$?, 而不执行父进程的 exit(EXIT_SUCCESS);


[root@localhost c]# cat c_execve.c

 

[cpp]  view plain copy
  1. #include <unistd.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4.   
  5. int main(void)  
  6. {  
  7.     char *args[] = {"/bin/ls", NULL};  
  8.   
  9.     puts("carry on execve: /n");  
  10.     if(execve("/bin/ls", args, NULL) == -1)  
  11.     {  
  12.         perror("execve x");  
  13.         exit(EXIT_FAILURE);  
  14.     }  
  15.   
  16.     puts("shouldn't get here");  
  17.     exit(EXIT_SUCCESS);  
  18.   
  19. }  

 

[root@localhost c]# ./c_execve
carry on execve:

execve x: No such file or directory  (看下 perror部分)
[root@localhost c]# echo $?
1


 说明:
      1) execl, execv, execle 和 execve   第一个参数都是 路径名
         execlp, execvp                   第一个参数都是 文件名
         
         如果文件名没有包含"/", 它们会模仿shell的行为搜索$PATH 找到要执行的二进制文件
         含有l的函数希望接收以逗号分隔的参数列表, 列表以NULL指针作为结束标志, 这些参数将传递给被执行的程序。 
         名字中包含v的函数则接收一个向量, 假设希望执行命令/bin/cat /etc/passwd  /etc/group , 则它们的执行方式如下
    execl("/bin/cat", "/bin/cat", "/etc/passwd", "/etc/group", NULL);

    char *argv[]={"/bin/cat", "/etc/passwd", "/etc/group", NULL};
    execv("/bin/cat", argv);
         
          以e结尾的函数, 可以让你为被执行的程序创建专门的环境。 这个环境保存在envp中, 它也是一个指向以空结尾的字符串数组的指针, 数组中每个字符串也是以空结尾。 每个字符串的形式为 "name=value" 对, name是环境变量的名字而value是它的值。
    char *envp[]={"PATH=/bin:usr/bin", "USR=cnscn", NULL};


          其他4个函数隐式地通过全局变量environ接受它们的环境, environ是一个指向字符串数组的指针, 数组中包含了调用进程的环境。

          使用putenv和getenv函数可以操控这些函数继承的环境。
          #include <stdlib.h>
          int putenv(const char *string);
          char *getenv(const char *name);

          char envval[]={"MYPATH=/usr/local/someapp/bin"};
          if(putenv(envval) == 0)
             puts("OK");

          if(getenv("MYPATH"))
             printf("%s/n", getenv("MYPATH"));
          else
             puts("MYPATH unassigned!");


.使用popen函数
    popen函数的行为和system类似, 它是一种无需使用fork和exec就能在执行外部程序的简易方法
    但popen使用管道来工作
     
    #include <stdio.h>
    FILE *popen(const char *command, const char *type);
    int pclose(FILE *stream);

    popen调用管道, 并创建通向标准输入或从command指定的程序或脚本的标准输出来的管道, 但不是同时两者都有。 第二个参数type在读取管道的stdout时为r, 在写入stdin时为w。 
     
    注: popen的I/O用法有点违反直觉。 读和写都是相对于command而言的, 所以command的输出是从stdout读入的。 要向command输入, 则需向它的stdin写入。


.控制进程

1)等待进程


  一旦用fork或exec创建了一个新进程, 为了收集新进程的退出状态并防止出现僵死进程(zombie process), 父进程应等待新进程终止。
   
  僵进程: 在父进程有机会用wait或waitpid收集它的退出状态之前就终止的子进程。 它虽然终止了,但它依然存在于进程表中。
  
  父进程通过wait或waitpid检索内核的进程表取得退出状态来收集(collect)子进程的退出状态

 

  子进程退出后, 分配给它的内存和其他资源都被释放,但是它还在内核的进程表中保留了一条,内核将在父进程收回子进程的退出状态之前一直保留着它。

 

  孤儿进程(orphan process) 是一个父进程在调用wait或waitpid之前就已经退出的子进程。 此时, init进程成为子进程的父进程并且收集它的退出状态, 从而避免出现僵进程。

 

  使用wait, waitpid调用可以收集子进程的退出状态。

 

  #include <sys/wait.h>
  #include <sys/type.h>
  pid_t wait(int *status);
  pid_t waitpid(pid_t pid, int *status, int options)

 

  status保存子进程的退出状态, pid是等待进程的pid. 它能接受下面的值:


    -1    等待任何PGID等于PID的绝对值的子进程
     1    等待任何子进程 
     0    等待任何PGID等于调用进程的子进程
    >0    等待PID等于pid的子进程

 

  Options规定wait调用的行为, 它可以是


     WNOHANG         导致waitpid在没有子进程退出时立即返回
     WUNTRACED       意味着它应因为存在没有报告状态的进程而返回


     也可以用 逻辑”或“ 操作, 取得两种行为 (WNOHANG || WUNTRACED)

[root@localhost c]# cat c_waitpid.c

 

[cpp]  view plain copy
  1. #include <unistd.h>  
  2. #include <sys/types.h>  
  3. #include <sys/wait.h>  
  4. #include <stdio.h>  
  5. #include <stdlib.h>  
  6.   
  7. int main(void)  
  8. {  
  9.     pid_t  child;  
  10.     int    status;  
  11.     child=fork();  
  12.     if(child == -1)  
  13.     {  
  14.        perror("fork");  
  15.        exit(EXIT_FAILURE);  
  16.     }  
  17.     else if(child == 0)  
  18.     {  
  19.         puts("in child");  
  20.     }  
  21.     else  
  22.     {  
  23.         puts("return to  parent");  
  24.         waitpid(child, &status, 0);  
  25.         printf("child exited with %d/n", status);  
  26.     }  
  27.     exit(EXIT_SUCCESS);  
  28. }  

 

[root@localhost c]# ./c_waitpid
in child
return to  parent
child exited with 0

 

waitpid函数专门等待child指定的子进程返回, 并且显示子进程的退出状态。 现在父进程和子进程的输出没有像上面的c_fork.c那样混合起来, 因为父进程直到子进程退出才停止执行。  waitpid和wait返回退出的子进程PID, 如果在options中指定WNOHANG则返回0, 如果出错则返回-1


.杀死进程

 一个进程由于以下5个原因中的一个而终止:


  它的main函数调用了return    (正常终止)
  它调用了exit               (正常终止)  
  它调用了_exit              (正常终止)

  它调用了abort              (异常终止)
  它被一个信号终止            (异常终止)

 

  无论正常还是异常终止, 最后都执行相同的内核代码、关闭打开的文件、释放内存资源, 并且执行其他要求的清理工作

 

1)exit 、_exit 函数


  #include <stdlib.h>
  int exit(int status)

 

  exit导致程序正常终止并且返回父进程的状态(status). 用atexit登记的函数也被执行。

 

  _exit函数在<unistd.h> 中声明, 它立即终止调用它的进程, 用atexit登记的函数将不再被执行

 

2)abort 函数


  如果需要异常地终止一个程序, 可以使用abort函数。


  Linux下abort还可以让程序产生内存转储(core dump), 这是大多数调试器用于分析程序崩溃时的文件。 虽然任何打开的文件都被关闭了, 但abort函数仍然是个粗暴的调用, 应做为最后的手段来使用, 比如你碰到类似严重内存不足这样的错误, 无法用程序的方法处理时再用。
 
 #include <stdlib.h>
 void abort(void);


[root@localhost c]# cat c_abort.c

 

[cpp]  view plain copy
  1. #include <stdlib.h>  
  2. #include <stdio.h>  
  3.   
  4. int main(void)  
  5. {  
  6.     abort();  
  7.     exit(EXIT_SUCCESS);  
  8. }  

 

[root@localhost c]# ./c_abort
已放弃         (内存未转储)

[root@localhost c]# ulimit  -c unlimited  (设置以让内存进程转储)
[root@localhost c]# ./c_abort
已放弃 (core dumped)

 

注: 你的系统可能不能生成一个core文件。 如果没有生成core文件,则按上面的操作来使用shell的命令ulimit

 

2)使用kill函数


  前面介绍了进程如何杀死自己。


  一个进程可以用kill 函数杀死另一个进程


  #include <signal.h>
  #include <sys/types.h>
  int kill(pid_t pid, int sig);

 

  pid指定了要杀死的进程,而sig是要发送的信号。

  
[root@localhost c]# cat c_kill.c

 

[cpp]  view plain copy
  1. #include <stdlib.h>  
  2. #include <stdio.h>  
  3. #include <sys/types.h>  
  4. #include <sys/wait.h>  
  5.   
  6. int main(void)  
  7. {  
  8.    pid_t  child;  
  9.    int status, retval;  
  10.   
  11.    child = fork();  
  12.   
  13.    if(child <0)  
  14.    {  
  15.        perror("fork");  
  16.        exit(EXIT_FAILURE);  
  17.    }  
  18.    else if(child == 0)  
  19.    {  
  20.        sleep(1000);  
  21.        exit(EXIT_SUCCESS);  
  22.    }  
  23.    else  
  24.    {  
  25.        /* WNOHANG 在子进程未退出时就返回到主进程 */  
  26.        if((waitpid(child, &status, WNOHANG)) == 0)  
  27.        {  
  28.            retval = kill(child, SIGKILL);  
  29.            if(retval)  
  30.            {  
  31.                puts("kill failed/n");  
  32.                perror("kill");  
  33.                waitpid(child, &status, 0);  
  34.            }  
  35.            else  
  36.            {  
  37.                printf("%d killed/n", child);  
  38.            }  
  39.        }  
  40.    }  
  41.    exit(EXIT_SUCCESS);  
  42. }  

 

fork成功后,子进程睡眠1000秒, 然后退出。


父进程在子进程上调用waitpid, 但它使用WNOHANG选项, 所以对子进程的调用会在子进程结束前立即返回。 接着父进程就杀死子进程。

 

kill常常用来终止一个进程或进程组, 但它也能用来向进程或进程组发送任何信号


.信号

 1)信号: 是硬件中断的软件模拟


         是由相同或不同的进程向一个进程传递的事件。  
         它通常用来向一个进程通知异常事件。
        
         进程执行时,几乎在任何时候都会发生事件, 这种不可预测意味着信号是异步的。 不但信号可在任何时候发生, 当信号发出时接收信号的进程也可以没有控制权
       
         每个信号名都以SIG开头, 比如SIGTERM或SIGHUP , 这些名字对应正整数常量, 称为信号量(signal number), 它们在系统的头文件<signal.h>中定义

 

         许多情况下都会出现信号: 硬件异常, 如非法的内存引用, 就能产生一个信号; 软件异常, 如试图向一个没有读取的管道执行写操作(SIGPIPE), 也会产生一个信号; kill函数向进程发送信号; 由终端产生的操作,如键入Ctrl+Z以挂起前台进程, 也产生信号

 

2)信号处理


  当一个进程收到一个信号后,它可以对信号采取如下三种措施之一:


    忽略这个信号


    捕获(trap/catch)这个信号, 这将导致执行一段称为信号处理器的特殊代码,叫做信号处理


    允许执行信号的默认操作

 

3)术语


  产生信号  当导致信号发生的事件出现时,比如硬件异常,就产生信号(generate)
  传递信号  进程对发送给它的信号采取措施时, 称信号被传递(deliver)
  信号未决  在产生信号和递送信号之间的时间间隔称为信号未决(pending), 被阻塞的信号也称为未决的信号
  信号布署  指进程如何响应信号: 可以忽略、允许默认操作、处理信号(对信号执行自定义的代码)
  信号集合  是一个C数据类型sigset_t, 它在<signal.h>中定义, 能够表示多种信号
  信号掩码  进程目前正阻塞不能递送的信号集合

 

3)发送信号


  从编程的角度看,向正在运行 的程序发送信号的方法有两种:


     使用kill命令(kill(1))   (它是kill函数的用户接口)
     使用kill(2)函数   
         
  使用kill命令


     此时必须使用system、fork或exec
     
  使用kill函数


     使用kill函数比kill命令简单, 因为此时不用exec字符串的额外步骤, 所要的只是PID和要用的信号
     
[root@localhost c]# cat c_signal.c

 

[cpp]  view plain copy
  1. #include <sys/types.h>  
  2. #include <wait.h>  
  3. #include <unistd.h>  
  4. #include <signal.h>  
  5. #include <stdio.h>  
  6. #include <stdlib.h>  
  7.   
  8. int main(void)  
  9. {  
  10.     pid_t child;  
  11.     int errret;  
  12.   
  13.     child = fork();  
  14.   
  15.     if(child < 0)  
  16.     {  
  17.         perror("fork");  
  18.         exit(EXIT_FAILURE);  
  19.     }  
  20.     else if(child == 0)  
  21.     {  
  22.         sleep(30);  
  23.     }  
  24.     else  
  25.     {  
  26.         /*send a signal that gets ignored*/  
  27.         errret = kill(child, SIGCHLD);  
  28.         if(errret < 0)  
  29.         {  
  30.             perror("kill: SIGCHLD");  
  31.         }  
  32.         else  
  33.         {  
  34.             printf("%d still alive/n", child);  
  35.   
  36.             /*now kill the child process*/  
  37.             printf("killing  %d/n", child);  
  38.             if(kill(child, SIGTERM) < 0)  
  39.                 perror("kill: SIGTERM");  
  40.             /*have to wait to  reap the status*/  
  41.             waitpid(child, NULL, 0);  
  42.         }  
  43.         exit(EXIT_SUCCESS);  
  44.     }  
  45. }  

 

[root@localhost c]# ./c_signal
4383 still alive
killing  4383
[root@localhost c]# ./c_signal
4385 still alive
killing  4385

 

4)捕获信号


  每个进程都能决定怎么响应除了SIGSTOP和SIGKILL之外的其他所有信号, 而这个两个信号不能被捕获或者忽略

 

1>捕获信号  简的方法不是真去捕获信号而是等待它们被发送过来


  alarm    函数设置了一个定时,当定时器时间到时就发送SIGALRM信号
  pause    函数也有类似功能, 但它是把进程挂起直至进程收到任何信号

 

2>设置超时


           alarm函数在调用进程中设置一个定时器, 当定时器时间到时,它就向调用进程发出SIGALRM信号, 除非调用进程捕获这个信号, 否则SIGALRM的默认动作是中止进程


           #include <unistd.h>
           unsigned int alarm(unsigned int seconds)

 

           seconds 是计时器时间到后时钟的秒数。 如果没有设置其他超时,则返回值为0, 否则返回值为前面安排的超时中保留的秒数。 一个进程只能设置一次超时。 把seconds设为0就会取消前面的超时设置。

 

3>使用pause函数


           pause函数挂起调用它的进程直到有任何信号达到。 调用进程必须有能力处理递送到的信号, 否则信号的默认部署就会发生


           #include <unistd.h>
           int pause(void);
            
           只有进程捕获到一个信号时pause才返回调用进程。 如果递送到的信号引发了对信号的处理, 那么处理工作将在pause返回前执行。 pause总是返回-1并且把变量errno设置为EINTR

 

4>  定义一个信号处理器


5)检测信号


   sigpending允许进程检测未决信号(当阻塞被挂起的信号), 然后决定是忽略它们还是递送他们。


    为什么会有未决的信号呢? 假如你想要向一个文件写入数据, 为了保持文件的完整性,写操作必须不能中断。 在写入期间,你想要阻塞SIGTERM和SIGQUIT, 但一般情况下你不是处理它们就是许可它们的默认动作。 在开始写入执行操作之前, 你阻塞了SIGTERM和SIGQUIT信号,一旦写入成功完成, 就就要检查未决信号,而如果有未决的SIGTERM和SIGQUIT信号, 就要解除对它们的阻塞。 或者, 也可以简单地解除对它们的阻塞而不必费力地检查它们是否未决。 是否要检查未决信号。 否则, 只需解除阻塞即可。

 

   #include <signal.h>
   int sigpending(sigset_t *set);

 

   未决信号的集合在set中返回,调用本身在成功时反回0, 出错时返回-1. 使用sigismember判断你感兴趣的信号是否未决信号, 也就是说, 判断他们是否在set中。

 

[root@localhost c]# cat c_pending.c

 

[cpp]  view plain copy
  1. #include <sys/types.h>  
  2. #include <unistd.h>  
  3. #include <signal.h>  
  4. #include <stdio.h>  
  5. #include <stdlib.h>  
  6.   
  7. int main(void)  
  8. {  
  9.     sigset_t set, pendset;  
  10.     struct sigaction action;  
  11.   
  12.     sigemptyset(&set);  
  13.   
  14.     /*Add the interesting signal*/  
  15.     sigaddset(&set, SIGTERM);  
  16.   
  17.     /*Block the signal*/  
  18.     sigprocmask(SIG_BLOCK, &set, NULL);  
  19.   
  20.     /*send SIGTERM to myself*/  
  21.     kill(getpid(), SIGTERM);  
  22.   
  23.     /*get pending signals*/  
  24.     sigpending(&pendset);  
  25.   
  26.     /*if SIGTERM pending, ignore it */  
  27.     if(sigismember(&pendset, SIGTERM))  
  28.     {  
  29.        sigemptyset(&action.sa_mask);  
  30.        action.sa_handler = SIG_IGN;  /*Ignore SIGTERM*/  
  31.        sigaction(SIGTERM, &action, NULL);  
  32.     }  
  33.   
  34.     /*unblock SIGTERM*/  
  35.     sigprocmask(SIG_UNBLOCK, &set, NULL);  
  36.     exit(EXIT_SUCCESS);  
  37. }  

 

为使程序简洁,所以省略了对出错的检查。 那创建了阻塞SIGTERM的信号掩码, 然后使用kill向自己发出SIGTERM信号。 因为信号被阻塞, 所以信号不被递送。 你可以使用sigpending和sigismember来判断SIGTERM是否未决, 如果是这样, 则把它的部署设置为SIG_IGN. 当解除阻塞后, SIGTERM不被递送, 程序正常终止。


6)进程调度


  进行进程调度算法和优先级


  #include <sched.h>
  #include <unistd.h>
  #include <sys/time.h>
  #include <sys/resource.h>

  int sched_setscheduler(pid_t pid, int policy, const struct sched_param *p);
  int sched_getscheduler(pid_t  pid);
  int sched_get_priority_max(int policy);
  int sched_get_priority_min(int policy);
  int getpriority(int which, int who);
  int setpriority(int which, int who, int prio);
  int nice(int inc)

 

  具有更高静态优先级的进程总是会抢先于较低静态优先级的程序而执行。


  对于传统的调度算法来说, 具有静态优先级0的进程是按照它们的动态优先级来分配CPU时间的, 动态优先组也称为它们的“谦让度(nice)”值


  系统调用nice通过向调用进程的动态优先级值加inc, 从而降低其优先级。


  超级用户可以指定一个负值, 这能提高进程的优先级


  系统调用setpriority设置进程(which = PRIO_PROCESS)、进程组(which = PRIO_PGRP) 或用户(which = PRIO_USER)的动态优先级。


  优先级的值设置为prio, 它可以取介于-20到20之间的某个值, 值越小高度优先级越高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值