《精通UNIX下C语言编程及项目实践》学习笔记目录
http://bbs.pediy.com/showthread.php?t=86392
第三篇 并发程序设计
业精于勤, 而荒于嬉.
九、进程控制
进程是程序的一次执行, 是运行在自己的虚拟地址空间的一个具有独立功能的程序. 进程是分配和释放资源的基本单位, 当程序执行时, 系统创建进程, 分配内存和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]$
(a) 全局变量法
UNIX系统中采用一个指针数组来存储全部环境值:
Extern char **environ;
[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]
UNIX环境下操作环境变量的函数如下:
#include <stdlib.h> char *getenv(char *name); int putenv(const char *string);
[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;
[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
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函数完成新程序的执行.
函数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]$
[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]$
当子进程终止时, 它释放资源, 并且发送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托管
在后台进程的基础上, 脱离原来shell的进程组和session组, 自立门户为新进程组的会话组长进程, 与原终端脱离关系
#include <unistd.h> pid_t setsid();
(3) 清除文件创建掩码
进程清除文件创建掩码,代码如下:
umask(0);
为了预防父进程不等待子进程结束而导致子进程僵死, 必须忽略或者处理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]$
《精通UNIX下C语言编程及项目实践》学习笔记目录
http://bbs.pediy.com/showthread.php?t=86392