程序和进程
1 程序
程序(program)是存放在磁盘文件中的可执行文件
2 进程和进程ID(进程是动态的)
程序的执行实例被称为进程(process)。某些操作系统用任务表示正被执行的程序。
每个Linux进程都一定有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一非负整数。
Linux启动过程有个init进程ID号是1,其它进程都是init派生出来的。
3 Linux下的进程结构
Linux系统是一个多进程的系统,进程之间具有并行性、互不干扰的特点。
Linux中进程包含3个段:代码段、数据段、堆栈段

数据段:存放全局变量、常数以及动态数据分配的空间(malloc函数取得的空间)
代码段:存放程序代码
堆栈段:存放子程序的返回地址、子程序的参数以及程序的局部变量
4 init进程
进程ID为1通常是init进程,在自举过程结束时由内核调用。
init进程绝不会终止(除非系统关闭)
它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。
(注:Linux内核空间和用户空间,Linux操作系统和驱动程序在内核空间运行,应用程序在用户空间运行)
5 获取进程标识
#include <sys/types.h> #include <unistd.h> 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
6 fork函数
#include <sys/types.h> #include <unistd.h> pid_t fork(void); //表示当前进程生成一个进程
返回:子进程中为0,父进程中为子进程ID,出错为-1;
[本身有一个进程在运行,派生出一个进程用fork函数调用,把所有的程序段、数据段、代码段全部复制过来,复制相应的堆、有关的程序上下文、有关的进程ID号、许可权限、优先级等所有的全部复制一遍,复制出来的就是子进程,创建的API函数就叫fork]
fork返回两个值,返回0是子进程,大于0父进程。
7 进程创建
由fork创建的新进程被称为子进程(child process)。
该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是子进程的进程ID。
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。
使用fork函数得到的子进程在父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
父、子进程之间的区别是:
(1)fork的返回值
(2)进程ID、不同的父进程ID
(3)子进程的tms_utime、tms_stime、tms_cutime以及tms_ustime设置为0
(4)父进程设置锁,子进程不继承
(5)子进程的未决告警被清除
(6)子进程的未决信号集设置为空集
/*fork.c*/ #include <sys/types.h> #include <sys/unistd.h> #include <stdio.h> #include <stdlib.h> int main { pid_t result; result=fork(); if(result<0) { perror("fork"); exit(-1); } if(result==0) { printf("The return value is %d\n In child process!\n my pid is%d\n",result,getpid()); } if(result>0) { printf("The return %d\n Parent process!\n pid is %d\n",result,getpid()); } }
8 vfork函数(针对的是没有MMC的芯片,所以用vfork优化)
vfork函数的调用序列和返回值与fork相同,但两者的语义不同。
现在很多的实现并不做一个父进程数据段和堆完全拷贝,因为在fork之后经常跟随着exec。作为替代使用了在写时复制(copy-on-write,COW)的技术。这些区域由父、子进程共享,而且内核将它们的存取许可权改变为只读的。如果有进程试图修改这些区域,则内核为有关部分,典型的是虚拟系统中的“页”,做一个拷贝。如:uclinux中的进程创建。
9 exec函数
[exec包含一系列函数,子进程复制了父进程的代码段运行时是没有目标的,fork产生一个子进程只是提供一个程序运行的空间,具体运行那个程序还不确定,最后用exec函数集执行另外一个程序]
在fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。
当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是另一个新程序替换了当前进程的正文、数据、堆和栈段。
#include <unistd.h> int execl(const char *pathname,const char *arg0,.../*(char *)0*/); int execv(const char *pathname,char *const argv[]); int execle(const char *pathname,const char *arg0,.../*(char*)0,char *const envp[]*/); int execve(const char *pathname,char *const argv[],char *const envp[]); int execlp(const char *pathname,const char *arg0,.../*(char *)0*/); int execvp(const char *pathname,char *const argv[]);
六个函数返回:若出错则为-1,若成功则不返回。
参数表的传递有关:
l:表示list
v:表示矢量vector
e:可传递新进程环境变量,execle、execve
p:可执行文件查找方式为文件名,execlp、execvp
10 Linux进程间通信
进程间通信有如下目的
(1)数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间
(2)共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到
(3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
(4)资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供锁和同步机制
(5)进程控制:有些进程希望完全控制另一个进程的执行(如debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
Linux使用的进程间通信方式:
(1)管道(pipe)和有名管道(FIFO)
(2)信号(signal)
(3)消息队列
(4)共享内存
(5)信号量
(6)套接字(socket)
10.1 管道通信
普通的Linux shell都允许重定向,而重定向使用的就是管道。例如:
ps | grep vsftpd [ps显示当前系统的进程,ps不加参数表示前台进程;-e或-a表示后台进程; | 表示管道,grep表示过滤筛选;ps显示所有进程后,然后查询筛选一下vsftpd这个进程]
管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
管道主要用于不同进程间通信。
[root@LinuxServer root]# ps -ef //-e后台 f表示工作目录 [root@LinuxServer root]# ps -ef | grep nfsd //筛选网络服务进程 root 12844 12817 0 10:18 pts/0 00:00:00 grep nfsd [root@LinuxServer root]# ps -e | grep vsftpd 1707 ? 00:00:00 vsftpd //"?"代表后台的一个守护进程,守护进程的控制终端用?表示,守护进程的父进程ID是1,即init
10.1.1 管道创建与关闭
创建一个简单的管道,可以使用系统调用pipe()。它接受一个参数,也就是一个包括两个整数的数组。如果系统调用成功,此数组将包括管道使用的两个文件描述符。创建一个管道之后,一般情况下进程将产生一个新的进程。
系统调用:pipe();
原型:int pipe(int fd[2]);
返回值:如果系统调用成功,返回0.如果系统调用失败返回-1;
errno = EMFILE(没有空闲的文件描述符)
EMFILE(系统文件表已满)
EFAULT(fd数组无效)
注:fd[0]用于读取管道,fd[1]用于写入管道

#include <unistd.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> int main() { int pipe_fd[2]; if(pipe(pipe_fd)<0) { printf("pipe create error\n"); return -1; } else printf("pipe create success\n"); close(pipe_fd[0]); close(pipe_fd[1]); }
10.1.2 管道读写
管道主要用于不同进程间通信。实际上通常先创建一个管道,再通过fork函数创建一个子进程。

子进程写入和父进程读的命名管道

10.1.3 管道读写注意事项
可以通过打开两个管道来创建一个双向管道。但需要在子进程中正确地设置文件描述符。
必须在系统调用fork()中调用pipe(),否则子进程将不会继承文件描述符。
当使用半双工管道时,任何关联的进程都必须共享一个相关的祖先进程。因为管道存在于系统内核之中,所以任何不在创建管道的进程的祖先进程之中的进程都将无法寻址它。而在命名管道中却不是这样。
10.2 标准流管道
与Linux中文件操作有文件流的标准I/O一样,管道的操作也支持基于文件流的模式。接口函数如下:
库函数:popen();
原型:FILE *popen(char *command,char *type);
返回值:如果成功,返回一个新的文件流。如果无法创建进程或者管道,返回NULL。
管道中数据流的方向是由第二个参数type控制的。此参数可以使r或者w,分别代表读或写。但不能同时为读和写。在Linux系统下,管道将会以参数type中第一个字符代表的方式打开。所以如果你在参数type中写入rw,管道将会以读的方式打开。
使用popen()创建的管道必须使用pclose()关闭。其实popen/pclose和标准文件输入/输出流中的fopen()/fclose()十分相似。
库函数:pclose();
原型:int pclose(FILE *stream);
返回值:返回系统调用wait4()的状态。
如果stream无效,或者系统调用wait4()失败,则返回-1.
此库函数等待管道进程运行结束,然后关闭文件流。
库函数pclose()在使用popen()创建的进程上执行wait4()函数。当它返回时,它将破坏管道和文件系统。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #define BUFSIZE 1024 int main() { FILE *fp; char *cmd="ps -ef"; char buf[BUFSIZE]; buf[BUFSIZE]='\0'; if((fp=popen(cmd,"r"))==NULL) perror("popen"); while((fgets(buf,BUFSIZE,fp))!=NULL) printf("%s",buf); pclose(fp); exit(0); }
10.3 命名管道(FIFO)
10.3.1 基本概念
命名管道和一般的管道基本相同,但也有一些显著的不同
(1)命名管道是在文件系统中作为一个特殊的设备文件而存在的。
(2)不同祖先的进程之间可以通过管道共享数据
(3)当共享管道的进程执行完所有的I/O操作以后,命名管道将继续保存在文件系统中以便以后使用。
管道只能由相关进程使用,它们共同的祖先进程创建了管道。但是通过FIFO不相关的进程也能交换数据。
10.3.2 命名管道的创建与操作
命名管道的创建
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname,mode_t mode);
返回:若成功则为0,若出错则为-1
一旦已经用mkfifo创建了一个FIFO,就可用open打开它。一般的文件I/O函数(close、read、write、unlink等)都可用于FIFO。
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
(1)在一般情况中(没有说明O_NONBLOCK),只读打开要阻塞到某个其他进程为写打开此FIFO。类似为写而打开一个FIFO要阻塞到某个其他进程为读而打开它。
(2)如果指定了O_NONBLOCK,则只读打开立即返回,但是如果没有进程已经为读而打开一个FIFO,那么只写打开将出错返回,其errno是ENXIO。
类似于管道,若写一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志。
FIFO相关出错信息:
(1)EACCES(无存取权限)
(2)EEXIST(指定文件不存在)
(3)ENAMETOOLONG(路径名太长)
(4)ENOENT(包含的目录不存在)
(5)ENOSPC(文件系统剩余空间不足)
(6)ENOTDIR(文件路径无效)
(7)EROFS(指定的文件存在于只读文件系统中)
10.4 信号通信
10.4.1 信号概述
信号是软件中断。信号(signal)机制Linux系统中最为古老的进程之间的通信机制。它用于在一个或多个进程之间传递异步信号。
很多条件可以产生一个信号
(1)当用户按某些终端键时,产生信号。在终端上按DELETE键通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。
(2)硬件异常产生信号:除数为0、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的过程产生适当的信号。例如,对执行一个无效存储访问的进程产生一个SIGSEGV.
(3)进程用kill(2)函数将信号发送给另一个进程或进程组。自然有些限制,接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
(4)用户可用kill(1)命令将信号发送给其他进程。此程序是kill函数的界面。常用此命令终止一个失控的后台进程。
(5)当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。这里并不是指硬件产生条件(如被0除),而是软件条件,如SIGURG(在网络连接上传来非规定波特率的数据)、SIGPIPE(在管道的读进程已终止后一个进程写此管道),以及SIGALRM(进程所设置的闹钟时间已经超时)。
内核为进程产生信号,来响应不同事件,这些事件就是信号源。主要信号源如下:
异常:进程运行过程中出现异常
其他进程:一个进程可以向另一个或一组进程发送信号
终端中断:Ctrl-C,Ctrl-\等
作业控制:前台、后台进程的管理
分配额:CPU超时或文件大小突破限制
通知:通知进程某事件发生,如I/O就绪等
报警:计时器到期
10.4.2 Linux中的信号
1)SIGHUP:从终端上发出的结束信号
2)SIGINT:来自键盘的中断信号(Ctrl-C)
3)SIGQUIT:来自键盘的退出信号(Ctrl-\)
4)SIGILL 5)SIGTRAP 6)SIGIOT 7)SIGBUS
8)SIGFPE:浮点异常信号(例如浮点运算溢出)
9)SIGKILL:该信号结束接收信号的进程
10)SIGUSR1 11)SIGSEGV 12)SIGUSR2 13)SIGPIPE
14)SIGALRM:进程的定时器到期时,发送该信号
15)SIGTERM:kill命令发出的信号
16)SIGCHLD:标识子进程停止或结束的信号
17)SIGCONT
18)SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号
19)SIGTSTP 20)SIGTTIN 21)SIGTTOU 22)SIGURG
23)SIGXCPU 24)SIGXFSZ 25)SIGVTALRM 26)SIGPROF
27)SIGWINCH 28)SIGIO 29)SIGPWR
可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作
(1)忽略此信号:大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外如果忽略某些硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是未定义的
(2)捕捉信号:为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。如果捕捉到SIGCHLD信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态
(3)执行系统默认动作:对大多数信号的系统默认动作是终止该进程。
每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5中缺省动作
异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程
退出(exit):不产生core文件,直接终止进程
忽略(ignore):忽略该信号
停止(stop):挂起该进程
继续(continue):如果进程被挂起,则恢复进程的运行。否则忽略信号。
10.4.3 信号处理流程

10.4.3.1 实例
实现功能:进程A发送信号给进程B,进程B处理信号后得到进程A发送的数据
/************************************** *file name:b_process *****************************************/ #include <sys/types.h> #include <signal.h> #include <unistd.h> #include <stdio.h> void func(int a) { printf("signal is SIGINT"); } int main() { signal(SIGINT,func); pause(); }
进程B自己检查看是否可以处理信号:
/************************************************/ dongry@d-linux:~$gcc b_process.c -o bp dongry@d-linux:~$./bp /*输入后进入等待状态 下面的两步完成后会自动打印出要输出的内容 /************************************************/ 打开另外一个命令窗口 dongry@d-linux:~$ps aux //查看./bp的进程ID,假如为6270 dongry@d-linux:~$kill -n SIGINT 6270
/******************************************************* *file name:a_process.c ******************************************************/ #include <sys/types.h> #include <signal.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc,char *argv[]) { pid_t pid; pid=atoi(argv[1]); kill(pid,SIGINT); }
/**************************************************/ dongry@d-linux:~$ ./bp /*等待*/ /*下面两个命令完成后,会自动打印出发送的内容*/ /***********************************************/ /*一个窗口打开命令窗口,假如bp的进程ID为6448*/ dongry@d-linux:~$gcc a_process.c -o ap dongry@d-linux:~$./ap 6448
10.5 信号发送与捕捉
10.5.1 kill()和raise()
kill()不仅可以终止进程,也可以向进程发送其他信号
与kill函数不同的是,raise()函数运行向进程自身发送信号
#include <sys/type.h> #include <signal.h> int kill(pid_t pid,int signo); int raise(int signo);
两个函数返回:若成功则为0,若出错则为-1
kill的pid参数有四种不同的情况
pid>0 将信号发送给进程ID为pid的进程
pid==0 将信号发送给其进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程
pid<0 将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,“所有进程”并不包括系统进程集中的进程
pid==-1 POSIX.1未定义此种情况
10.5.2 alarm和pause函数
使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其默认动作是终止该进程。
#include <unistd.h> unsigned int alarm(unsigned int seconds)
返回:0或以前设置的闹钟时间的余留秒数
参数seconds的值是秒数,经过了指定的seconds秒后会产生信号SIGALRM。
每个进程只能有一个闹钟时间。如果在调用alarm时,以前以为该进程设置过闹钟时间,而且它还没超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前登记的闹钟时间则被新值代换。
如果有以前登记的尚未超过的闹钟时间,而且seconds值是0,则取消以前的闹钟时间,其余留值仍作为函数的返回值。
pause函数使调用进程挂起直至捕捉到一个信号
#include <unistd.h> int pause(void);
返回:-1,errno设置为EINTR
只有执行了一个信号处理程序并从其返回时,pause才返回。
10.6 信号的处理
当系统捕捉到某个信号时,可以忽略该信号或是使用指定的处理函数来处理该信号,或者使用系统默认的方式。
信号处理的主要方法有两种,一种是使用简单的signal函数,另一种是使用信号集函数组。
10.6.1 signal
#include <signal.h> void (*signal (int signo,void(*func)(int)))(int)
返回:成功则为以前的信号处理配置,若出错则为SIG_ERR
func的值是:
(1)常数SIG_IGN
(2)常数SIG_DFL
(3)当接到此信号后要调用的函数的地址。如果指定SIG_IGN,则向内核表示忽略此信号(有两个信号SIGKILL和SIGSTOP不能忽略)。如果指定SIG_DFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,我们称此为捕捉信号。我们称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)
signal函数原型太复杂了,使用下面的typedef则可使其简化
typedef void sign(int); sign *signal(int ,handler*)
10.6.2 信号集函数组
我们需要有一个能表示多个信号——信号集(signal set)的数据类型。将在sigprocmask()这样的函数中使用这种数据类型,以告诉内核不允许发送该信号集中的信号。信号集函数组包含几大模块:创建函数集、登记信号集、检测信号集

10.6.2.1 创建函数集
#include <signal.h> int sigemptyset(sigset_t *set) //初始化信号集合为空 int sigfillset(sigset_t *set) //初始化信号集合为所有信号的集合 int sigaddset(sigset_t *set,int signo); //将指定信号添加到现存集中 int sigdelset(sigset_t *set,int signo); //从信号集中删除指定信号
四个函数返回:若成功则为0,若出错则为-1
int sigismember(const sigset_t *set,int signo); //查询指定信号是否在信号集合中
返回:若真则为1,若假则为0
10.6.2.2 登记信号集
登记信号处理机主要用于决定进程如何处理信号。首先要判断出当前进程阻塞能不能传递给该信号的信号集。这首先使用sigprocmask函数判断检测或更改信号屏蔽字,然后使用sigaction函数改变进程接受到特定信号之后的行为。
一个进程的信号屏蔽字可以规定当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。
#include <signal.h> int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
返回:若成功则为0,若出错则为-1
oset是非空指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
用sigprocmask更改当前信号屏蔽字的方法,how参数设定:
(1)SIG_BLOCK该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含我们希望阻塞的附加信号。
(2)SIG_UNBLOCK该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集的交集。set包含了我们希望解除阻塞的信号。
(3)SIG_SETMASK该进程新的信号屏蔽是set指向的值
如果set是个空指针,则不改变该进程的信号屏蔽字,how的值也无意义。
sigaction函数的功能是检查或修改(或两者)与指定信号向关联的处理动作。此函数取代了Linux早期使用的signal函数。
#include <signal.h> int sigaction(int signo,const struct sigaction *act,struct sigaction *oact);
返回:若成功则为0,若出错则为-1.
参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针非空,则系统返回该信号的原先动作。此函数使用下列结构:
struct sigaction { void (*sa_handler)(int signo); sigset_t sa_mask; int sa_flags; void (*sa_restore); };
sa_handler是一个函数指针,指定信号关联函数,可以是自定义处理函数,还可以SIG_DFL或SIG_IGN.
sa_mask是一个信号集,它可以指定在信号处理程序执行过程中那些信号应当被阻塞。
sa_flags中包含许多标志位,是对信号进行处理的各种选项。具体如下
SA_NODERER\SA_NOMASK:当捕捉到此信号时,在执行其信号捕捉函数时,系统不会自动阻塞此信号
SA_NOCLDSTOP:进程忽略子进程产生的任何SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU信号
SA_RESTART:可让重启的系统调用重新起作用
SA_ONESHOT\SA_RESETHAND:自定义信号只执行一次,在执行完毕后恢复信号的系统默认动作
10.6.2.3 检测信号集
检测信号是信号处理的后续步骤,但不是必须的。sigpending函数运行进程检测“未决”信号(进程不清楚它的存在),并进一步决定对它们作何处理。
sigpenging返回对于调用进程被阻塞不能递送和当前未决的信号集
#include <signal.h> int sigpending(sigset_t *set);
返回:若成功则为0,若出错则为-1;
11 守护进程
11.1 概述
守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,在系统关闭是终止。因为它们没有控制终端,所以说它们是在后台运行的。Linux系统有很多守护进程,它们执行日常事物活动。
守护进程不需要交互,vsftpd就是守护进程,后缀d的都是守护进程。
dongry@d-linux:~$ ps //ps不加参数表示前台服务 PID TTY TIME CMD 2586 pts/2 00:00:00 bash 2612 pts/2 00:00:00 ps dongry@d-linux:~$ ps -ef //-e表示所有进程,-f表示工作模式
11.2 守护进程特征
(1)一个或多个进程组组成一个会话组
一个或多个进程 组成一个进程组
(2)所有守护进程都是以超级用户(用户ID为0)的优先权运行。
(3)没有一个守护进程具有控制终端——终端名称设置为问号(?)、终端前台进程组ID设置为——1。缺少控制终端可能是精灵进程调用了setsid的结果。
(4)除update以外的所有精灵进程都是进程组的首进程,对话期的首进程,而且是这些进程组合对话期中的唯一进程。update是它所在进程组和对话期中的唯一进程,但是该进程组的首进程(可能也是该对话期的首进程)已经终止。
(5)所有这些守护进程的父进程都是init进程。所有守护进程都是由init托管。
11.3 守护进程编程规则
(1)创建子进程,父进程退出
首先做的是调用fork,然后使父进程exit。这样做实现了下面几点:第一,如果该守护进程是由一条简单shell命令启动的,那么使父进程终止是的shell认为这条命令已经执行完成。第二,子进程继承了父进程的进程组ID,但具有一个新的进程ID,这就保证了子进程不是一个进程 组的首进程。这对于下面就要做的setsid调用是必要的前提条件。
(2)调用setsid以创建一个新的会话,并担任该会话组的组长。调用setsid作用有三个:
(a)成为新对话期的首进程
(b)成为一个新进程组的首进程
(c)脱离控制终端(会话组是一个或多个进程组的集合)
setsid()函数格式:
#include <sys/types.h> #include <unist.h> pid_t setsid(void);
函数成功时返回该进程组的ID,出错时返回-1。
(3)改变当前目录为根目录
chdir("/");
从父进程继承过来的当前工作目录可能在一个mnt的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个mnt文件系统中,那么该文件系统就不能被拆卸。
(4)重设文件权限掩码
umask(0); [0表示打开所有权限]
由继承得来的文件方式创建屏蔽字可能会拒绝设置某些许可权。例如,若守护进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则所要求的组可读、写就不能起作用。
(5)关闭不再需要的文件描述符
用fork函数创建的子程序会从父进程那继承一些已经打开的文件,由此为使守护进程就不再持有从其父进程继承来的某些文件描述符。但是,究竟关闭哪些描述符则与具体的精灵进程有关,可以程序中的方法关闭所有文件描述符。
open("/tmp/dameon.log",O_CREAT | O_WRONLY | O_APPEND,0600) //open非缓存 fopen缓存方式

12 守护进程出错处理
由于守护进程完全脱离了控制终端,因此不能像其他程序一样通过输出错误信息到控制台的方式来通知程序员
通常的办法是使用syslog服务,将出错信息输入到"/var/log/message"系统日志文件中去。
syslog是Linux中的系统日志管理服务通过守护进程syslog来维护。
12.1 syslog函数说明
openlog函数用于打开系统日志服务的一个连接;
syslog函数用于向日志文件中写入消息,在这里可以规定消息的优先级、消息的输出格式等;
closelog函数用于关闭系统日志服务的连接。
12.1.1 syslog函数格式
(1)openlog函数
#include <syslog.h> void openlog(char *ident,int option,int facility);
ident:要向每个消息加入的字符串,通常为程序名称
option参数:
LOG_CONS:若日志消息不能通过发送至syslogd,则将该消息写至控制台
LOG_NDELAY:立即打开UNIX域数据报套接口至syslsgd守护进程。通常在记录第一条消息之前,该套接接口不打开。
LOG_PERROR:除将日志消息发送给syslog外,还将它写至标准出错(stderr)
LOG_PID:每条消息都包含进程ID,此选择项可供对每个请求都fork一个子进程的守护进程使用。
openlog的facility参数:
LOG_AUTH:授权程序 login.su,getty,...
LOG_CRONcron 和 at
LOG_DAEMON系统守护进程:ftpd,routed,...
LOG_KERN内核产生的消息
LOG_LOCAL0~7保留由本地使用
LOG_LPR行打系统:lpd,lpc,...
LOG_MAIL邮件系统
LOG_NEWSU senet网络新闻系统
LOG_SYSLOG syslogd守护进程本身
LOG_USER来自其他用户进程的消息
LOG_UUCP UUCP系统
(2)syslog函数
#include <syslog.h> void syslog(int priority,char *format,...);
priority选项(消息优先级)
LOG_EMERG紧急(系统不可使用)(最高优先级)
LOG_ALERT必须立即修复的条件
LOG_GRIT临界条件(例如,硬设备出错)
LOG_ERR出错条件
LOG_WARNING警告条件
LOG_NOTICE正常,但重要的条件
LOG_INFO信息性消息
LOG_DEBUG调试拍错消息(最低优先级)
(3)closelog函数
#include <syslog.h> void closelog(void);