Linux C 系统编程(07)进程管理 进程控制

1 进程标识符

1.1 实际用户/用户组;有效用户/用户组

在linux/Unix的进程中,涉及到多个用户ID和用户组ID,包括:

  1. 实际用户ID和实际用户组ID:标识我是谁(据说这是一个变态的哲学问题,难死一片哲学家)。也就是登录用户的uid 和gid,比如我的Linux以taskiller登录,在Linux运行的所有的命令的实际用户ID都是taskiller的uid,实际用户组ID都是taskiller的 gid(可以用id命令查看)。
  2. 有效用户ID和有效用户组ID:进程用来决定我们对资源的访问权限。一般情况下,有效用户ID等于实际用户ID,有效用户组ID等于实际用户组ID。当设置-用户-ID(SUID)位设置,则有效用户ID等于文件的所有者的uid,而不是实际用户ID;同样,如果设置了设置-用户组-ID(SGID)位,则有效用户组ID等于文件所有者的gid,而不是实际用户组ID。

1.2 进程ID

一个进程的基本属性,类似于每个人的身份证号,根据进程ID,可以精确地确定一个进程,多个进程标识符可以对应一个程序。

1.3 进程中重要的ID值

每个进程有6个重要的ID值,分别是进程ID、父进程ID、有效用户ID、有效组ID、实际用户ID、实际用户组ID。这6个ID保存在内核的数据结构中。只是有时候用户需要这些ID。linux下用getpid和getppid函数得到进程的进程ID和父进程ID,函数原型如下:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

详细见linux函数参考手册。linux下使用getuid和geteuid函数得到进程的实际用户和有效用户,函数原型如下:

#include <unistd.h>
#include <sys/types.h>
uid_t getuid(void);
uid_t geteuid(void);

详细见linux函数参考手册。linux下使用getgid和getegid函数得到进程的实际用户组ID和有效用户组ID,函数的原型如下:

#include <unistd.h>
#include <sys/types.h>
gid_t getgid(void);
gid_t getegid(void);

详细见linux函数参考手册。注意:

  1. 进程ID和父进程ID这2个标识符不能更改,其他4个ID在适当的情况下可以被更改。
  2. 对于一般的进程,实际用户ID和有效用户ID是一样的,仅在一些特殊场合不一样。    

2 进程操作

2.1 fork函数创建一个进程

进程是系统中的基本执行单位。linux系统允许任何一个用户进程创建一个子进程。创建之后,子进程存在于系统之中,并且独立于父进程。该子进程可以接受系统调度,可以分配得到系统资源。系统也可以检测到它的存在,并且赋予它与父进程同样的权力。(注意:在linux下,除了0号进程以外,所有的进程都是由其他的进程创建的)
linux下使用fork函数创建一个新的进程。fork函数的原型:

#include <unistd.h>
pid_t fork(void);
函数执行成功有两个返回值;为0,表示子进程;为正数,表示父进程。失败则返回-1。

详细见linux函数参考手册。注意:

  1. 正常情况下,程序运行的时候不能保证父进程或子进程先运行,要想保证的化必须要有额外的操作。
  2. 对于fork函数,父进程与子进程共享代码段,但是其他资源比如数据段和堆栈段是完全复制父进程的。
  3. 子进程继承父进程的时候,文件锁、未处理的闹钟信号和未决信号都不会被继承。
  4. 现在的linux内核实现fork函数时往往实现为子进程先于父进程复制资源,当子进程修改这些内容的时候复制才会发生,内核才会给子进程分配进程空间将父进程中的内容复制过来,继续后面的操作。这实际上也是写时操作的一个重要体现。

fork函数出错的情况:

  1. 系统中的进程数量超过系统规定的限制。    
  2. 调用fork函数的用户进程太多了。

2.2 vfork创建一个进程

linux下提供了一个和fork函数功能差不多的函数vfork函数,它们的区别是:

  1. fork 是子进程拷贝父进程的数据段,代码段 ;vfork 是子进程与父进程共享数据段 
  2. fork是父子进程的执行次序不确定;vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。 
  3. vfork是保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

vfork函数的原型:

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
函数执行成功有两个返回值;为0,表示子进程;为正数,表示父进程。失败则返回-1。

详细见linux函数参考手册。注意:对于vfork函数,一般不要在除main以外的函数中调用。原因是子进程先于父进程运行,会对栈帧进行覆盖操作,最后父进程操作的时候会出现段错误。即子进程对父进程的影响是巨大的。

2.3 退出一个进程

linux下退出一个进程一般用exit函数。exit函数的原型:

#include <unistd.h>
void exit(int status);
参数status:表示的是进程退出的状态,这个状态值是一个整型。在shell中可以检查到退出的状态。正常退出,exit中的参数为0,异常退出为非0。

详细见linux函数参考手册。可以利用errno变量作为参数传递给exit函数,这样可以在程序退出后检查程序退出的原因。即在shell中可以确定出错的原因。    

exit函数实际上是封装了linux系统调用的_exit函数,两者的主要区别在于exit函数会在用户空间做一些善后工作,比如将内容同步到磁盘,清理用户缓冲区等,之后才进入内核释放用户进程的地址空间。而_exit函数直接进入内核释放用户的地址空间,所有的用户空间的缓冲区内容都将会丢失。

2.4 设置进程所有者

每个进程都有两个用户ID,实际用户ID和有效用户ID。linux下使用setuid来改变一个进程的实际用户ID和有效用户ID。setuid函数的原型:

#include <sys/types.h>
#include <unistd.h>
int setuid(uid_t uid);
参数uid:改变后的新用户ID;函数执行成功返回0,失败返回-1。

详细见linux函数参考手册。只有两种用户可以修改进程的实际用户ID和有效用户ID:跟用户和等于进程的实际用户ID的用户。
一般情况是一个进程需要具有某种权限,将有效用户ID设置为有这种权限的用户ID。当进程不需要这种权限的时候进程将自己的有效用户ID还原,使自己的权限复原。对于函数seteuid(uid_t euid),仅仅改变有效用户ID。同一系列的函数还有setgid和setegid函数,分别类似于setuid和seteuid函数,只不过影响的是组ID。

2.5 调试多进程

gdb调试多进程有两种方法:
@1 设置跟踪流:设置方法如下:
set follow-fork-mode [parent|child]
选择一个进程进行跟踪,另一个进程不受影响。之后在子进程代码设置断点即可。
如果要在fork函数之后断开某个进程的测试,则使用命令:

    set detach-on-fork [on,off]
    #若选中on,则断开调试follow-fork-mode指定的进程。
    #若选中off,gdb将控制父进程和子进程

@2 利用attach命令:gdb调试器中attach命令可以调试一个已经运行的程序,在进程调用fork函数后可以使用attach命令调试子进程。前提是要知道子进程的进程ID,并且子进程能够等待调试的开始,所以在利用attach命令的时候要添加辅助性代码。


3 执行程序

3.1 exec族函数

linux环境下使用exec函数执行一个新的程序,该函数在文件系统中搜索指定路径的文件,并且将该文件内容复制到exec函数的地址空间,取代原来进程的内容,该进程仍然保持父进程的进程空间的内容,只不过该进程的代码段和数据段已经被替换。(注意:exec函数并不创建一个新的进程,虽然进程内容改变了,但是进程ID没变,依然是一个进程)
exec族函数一共有六个:

  1. 其中以l为后缀表示list,说明执行程序的命令行参数以列表的方式提供,并且以NULL结尾,但是参数个数没有限制。接收以逗号分隔的参数列表。
  2. 其中以v为后缀表示vector,说明执行程序的命令行参数以二维数组的方式提供。接收一个以NULL结尾的字符串数组的指针。
  3. 其中以e为后缀表示environment,表示传递给新程序的环境变量列表,这个列表是一个二维数组,每一行是一个环境变量。
  4. 其中以p结尾的exec函数表示第一个参数不是完整的路径名,而是一个程序名,这就需要PATH环境变量与这个参数组合成一个完整的路径。

exec族函数原型如下:

#include <unistd.h>
extern char **environ;
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 execve(const char *filename, char *const argv[], char *const envp[]);

详细见linux函数参考手册

3.2 在程序中执行shell命令
linux下使用system函数调用shell命令。system函数的原型如下:

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

详细见linux函数参考手册。参数command是需要执行的命令。函数的返回值比较复杂,实际上system函数封装了fork、exec、waitpid三个系统调用,其返回值也要根据这几个系统调用的情况来讨论:

  1. 如果fork和waitpid函数执行失败,system函数返回-1。
  2. 如果exec函数执行失败,函数返回表示文件不可执行。
  3. 如果3个函数都执行成功,system函数返回执行程序俄的终止状态。
  4. 如果参数command的值为NULL,system函数返回1,实际上这个可以用于测试系统是否支持system函数。

使用system函数需要仔细看需求分析,一般情况下,system有以下几点优点:

  1. system函数添加了出错处理操作。
  2. system函数添加了信号处理操作
  3. system函数调用了wait函数,保证不会出现僵尸进程。

4 关系操作

对于子进程而言,在其进行退出时的状态可以由父进程得到。得到进程推出信息这样的操作称为关系操作。linux内核为每个终止的子进程保存了一定量的信息,包括进程ID、进程终止状态以及进程的统计信息等。这些信息由父进程得到并且做相应的处理

4.1 等待进程退出的两个函数

在linux下使用wait函数和waitpid函数得到子进程的一些统计信息。wait和waitpid函数的原型:

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

详细见linux函数参考手册
waitpid和wait函数相比有区别有以下3点:

  1. waitpid函数可以指定一个子进程。
  2. waitpid函数可以不阻塞等待一个进程。
  3. waitpid函数支持作业控制。

4.2 僵尸进程

子进程退出的时候,进程的退出状态信息保存在内核中,此时父进程并没有调用wait函数来处理,子进程的进程ID也同样保存在系统的进程列表中,这时的进程称之为僵尸进程。僵尸进程对系统有很大的威胁,它占用系统资源却什么都不做。创建一个僵尸进程是先调用fork函数后,父进程不用wait函数。查看的时候僵尸进程表示为Z。

解决僵尸进程的方法:

  1. 父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。
  2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。
  3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。
  4. fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。

4.3 输出进程统计信息

wait3和wait4函数基本等价于wait函数和waitpid函数,不同的是wait3和wait4函数还能够得到更加详细的信息。这些信息多是关于内核的,通过结构体返回给用户。wait3和wait4函数原型如下:

#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options,struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

详细见linux函数参考手册。要想得到更详细的信息,需要用到值结果参数,对应结构体为rusage。     

发布了289 篇原创文章 · 获赞 47 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/vviccc/article/details/105153559