8.2.3回收进程用户空间资源
在linux系统下,可以通过以下方式结束进程。
显示的调用exit或_exit系统调用。
在main函数中执行return语句。
隐含的离开main函数,例如遇到main函数的“”}。
进程在正常退出前都需要执行注册的退出处理函数,刷新流缓冲区等操作,然后释放进程用户空间的所有资源。而进程的控制块PCB并不在这时释放,仅调用退出函数的进程属于一个僵死进程。
1.exit和return的区别
函数exit用于退出进程。在正式释放资源前,将以反序的方式执行由on_exit()函数和atexit函数注册的清理函数,同时刷新流缓冲区。函数声明:
extern void exit(int __status);
如果执行成功没有返回值,并把参数status(用来标识退出状态)返回给父进程。
c语言关键字return与函数exit在main函数中完成同样的操作,但两者有本质区别。
1.return退出当前函数,exit函数退出当前进程,因此,在main函数里面,return 0和exit(0)完成一样的功能。
2.return仅从子函数中返回,并不退出进程,调用exit时要调用一段终止处理程序,然后关闭所有的IO流。
2._exit函数直接退出
_exit函数不调用任何注册的函数而直接退出进程。
extern void _exit(int __status);
_exit仅把参数status返回给父进程而直接退出,此函数调用后不会返回,而是传递SIGCHLD信号给父进程,父进程可以通过wait函数获得子进程的结束状态,_exit()不会处理标准IO缓冲区,如果需要更新需要调用exit。
3.注册退出处理函数
函数atexit和on_exit用来注册在执行exit函数前执行的操作函数,其实现使用了回调函数的方法。
extern int atexit(void (*__func)(void));
extern int on_exit(void(*__func)(int __status, void *__arg), void *__arg);
两个函数的功能都是告诉进程,在正常退出时执行注册的func函数。两者区别差异仅仅是一个带参数一个不带。
1.第一个参数为退出状态,在执行exit()函数时此参数为exit函数的参数。
2.第二个参数为用户输入的信息,一个无类型指针。用户可以指定一段代码位置或输出信息。
8.2.4回收内核空间资源
进程PCB释放由父进程完成,父进程可以显式的调用wait和waitpid函数来完成。
1.wait()等待子进程结束
调用wait函数的父进程将阻塞式等待该进程的任意一个子进程结束,回收改子进程的内核进程资源。
extern __pid_t wait(__WAIT_STATUS __stat_loc);
如果等待到任意一个子进程结束,将返回当前结束的子进程的PID,同时将子进程的退出状态储存在__stat_loc变量中。
宏WIFEXITED用来判断进程是否是正常退出的,如果是,此值为1.
#define __WIFEXITED(status) __WIFEXITED(__WAIT_INT(status))
#define __WTERMSIG(status) ((status) & 0x7f)
宏WIFSIGNALED用来判断进程是否时因为收到信号而退出的。如果是,此宏的值为1.
#define __WIFEXITED(status) (__WTERMSIG(status) == 0)
#defien __WIFSIGNALeD(status) (((signed char) (((status)&0x7f)+1)>>1)>0)
2.waitpid()等待子进程结束
用户可以使用waitpid()函数来等待指定子进程结束,其函数定义:
extern __pid_t waitpid(__pid_t __pid, int *__stat_loc, int __options);
其中,第一个参数为进程的PID值,设置:
PID>0,表示等待进程的PID为该PID值的进程结束。
PID=-1,表示等待任意子进程结束,相当于wait函数。
PID=0,表示等待的进程与当前进程的进程组PGID一致的进程结束。
PID<-1 , 表示等待进程组PGID是此值的绝对值的进程结束。
第二个参数为调用他的函数中的某个变量地址,用来存储结束进程的结束状态。
第三个参数为等待选项,可以设置为0,或者为WNOHANG和WUNTRACED
#define WNOHANG 1//不阻塞等待
#define WUNTRACED//报告状态信息
8.2.5孤儿进程和僵死进程
孤儿进程:因父进程先退出导致一个子进程被init进程收养的进程为孤儿进程,即孤儿进程的父亲更改为init进程,该进程在孤儿进程推出后回收他的内核资源。
僵死进程:进程已经退出,但他的父亲进程还没有回收内核资源的进程为僵死进程,即该进程在内核空间的PCB没有释放。
8.2.6修改进程用户相关信息
1.access核实用户权限
此函数用来检查当前进程是否拥有对某文件的相应的访问权限。
extern int access(__const char *__name, int __type);
此函数的第一个参数为欲访问的文件(需要包含路径),第二个参数为相应的访问权限,文件权限定义如下:
#define R_OK 4//读权限
#define W_OK 2//写权限
#define X_OK 1//执行权限
#define F_OK 0//文件是否存在
在访问文件之前,都需要检测真实用户号(RUID)是否拥有该文件的访问权限。
2.设置真实的用户RUID
任何一个进程都拥有一个真实用户RUID,默认情况下,该ID为执行此进程的用户。如果要显示的修改此值,可以调用
extern int setuid(__uid_t uid);
此函数有一个参数,即预设值的进程真实用户号(RUID)。
如果当前用户是超级用户,则将设置真实用户号、有效用户号为指定ID,并返回0标识成功。
如果是普通用户,欲设置UID值为自己的UID,则可以修改成功,否则无权修改返回-1.
3.设置进程有效用户号(EUID)
extern int seteuid(__uid_t __uid);
setgid函数可以设置EGID,原理同seteuid。
8.3守候进程及其创建过程
8.3.1守候进程及其创建过程
1.守候进程的特点
守护进程(DAEMON)是在后台运行的一种特殊进程,他脱离于终端,可以避免进程被任何终端产生的信号所打断,他在执行过程中的信息也不再任何终端上显示。守候进程周期性的执行某种任务或等待处理某些发生的事件,Linux大多数服务器就是守候进程实现的。
守候进程可以通过以下方式启动:
在系统启动时由脚本启动,这些启动脚本一般放在/etc/rc.d目录下。
利用inetd超级服务器启动,如telnet等。
由cron命令定时启动以及在终端用nohup命令启动的进程也是守候进程。
2.守候进程的编程要点
1.屏蔽一些有关终端操作的信号
这是为了防止守护进程在没有正常启动起来前,控制终端受到干扰退出或挂起。
for(i=1;i<=31;i++)
signal(SIGTTOU, SIG_IGN);//忽略所有可以忽略的信号,SIGSTOP和SIGKILL不能忽略
2.在后台运行
这是为了避免挂起控制终端将其放入后台执行。方法是在进程中创建子进程,并使其父进程终止,让其子进程在后台执行。
3.脱离控制终端和进程组
因为:
1.一个进程属于一个进程组,进程组号(PGID)就是进程组长的进程号(PID)。
2.同进程中的进程共享一个控制终端,这个控制终端默认是创建进程的终端。
3.一个进程关联额控制终端和进程组通常是从父进程继承下来的,因此,这个子进程仍然会受到父进程终端的影响,因为终端产生的信号会发送给前台进程组的所有进程。
基于以上原因,需要让这个子进程彻底摆脱该终端的影响,需要调用setsid()使进程成为新的会话组长。
调用成功后,调用此函数的进程会成为新的会话组长和新的进程组长,并与原来的进程脱离关系。由于会话过程对于控制终端的独占性,进程同时与控制终端脱离。
4.禁止进程重新打开新的控制终端。
现在,进程已经成为无终端的会话组长。但他可以重新申请打开一个控制终端。因为,只有会话组长才能打开终端,采用的办法是再创建一个子进程,并让其父进程退出,该子进程就不再是会话组长,从而达到目的。
5.关闭打开的文件描述符
因为进程从创建他的父进程那里继承了打开的文件描述符,一般情况下,不再需要,包括标准输入输出(因为守候进程是后台执行)。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸载并引起无法预料的错误。按如下方法进行关闭
#define NOFILE 256//不同的系统有不同的限制
for(i=0;i<NOFILE;i++)//关闭打开的文件描述符
close(i);
6.改变当前工作目录。
进程活动时,其工作目录所在的文件系统不能卸载。因此,一般需要将守候进程的工作目录改变到合适的目录。例如,写日志的进程将工作目录改变到特定目录“/tmp”。
chdir("/tmp");
7.重设文件创建掩码
进程从创建他的父进程那里继承了文件创建掩码,他可能修改守护进程所创建的文件的存取权限。
将文件掩码清除:
umask(0);
8.处理SIGCHLD信号(子进程退出信号)
一种简单方式是将子进程退出的SIGCHLD信号的操作设为SIG_IGN处理方式,让系统帮助回收僵死进程资源。这样,内核在子进程结束时不会产生僵死进程。
signal(SIGCHLD,SIG_IGN);
8.3.2日志信息及其管理
1.日志信息的基本概念
为了告诉系统管理员守候进程的运行情况,特别是出现异常时,守候进程需要输出特定的信息,而守候进程又不能把信息输出到某个终端,因此,守候进程一般采用日志信息的方式输出。在linux系统下,守候进程有两种写日志信息的方式。
1.进程直接与日志文件建立联系(或者自己创建一个独立的日志文件),即open该文件,然后调用write函数写日志。
2.使用日志守候进程
为了便于管理日志文件,系统创建了日志守候进程syslogd专门负责管理日志文件,因此,要向日志文件中写信息,只需要将日志发送给日志守候进程。
日志守候进程syslogd根据配置文件~~/etc/syslog.conf~~ (内核为2.6.18时候使用的是syslog服务
内核为2.6.32以后syslog被命名为rsyslog,所以配置文件名称也不一样。)决定各进程发送的日志信息写入的文件内容。
有了日志守候进程后,用户只需要简单的设置自己输出的日志信息类别及级别就可以将自己的日志信息写入到特定的日志中。
2.建立与日志守候进程的联系
在进程中,调用函数openlog()将与日志守候进程建立联系。
extern void openlog(__const char *__ident, int __option, int __facility);
openlog()将打开的当前程序与日志守候进程之间的联系。
通过
man openlog可以查看参数
参数1:要向每个消息加入的字符串,一般可设置为当前进程名。
参数2:用来描述已打开的选项。
option
The option argument to openlog() is an OR of any of these:
LOG_CONS Write directly to system console if there is an error while sending to system logger.//如果消息无法发送到日志服务,将输出到终端
LOG_NDELAY Open the connection immediately (normally, the connection is opened when the first message is logged).//立即打开
LOG_NOWAIT Don't wait for child processes that may have been created while logging the message. (The GNU C library does not create a child process, so this option has no effect on Linux.)
LOG_ODELAY The converse of LOG_NDELAY; opening of the connection is delayed until syslog() is called. (This is the default, and need not be specified.)//知道调用syslog才打开
LOG_PERROR (Not in POSIX.1-2001 or POSIX.1-2008.) Print to stderr as well.//错误消息同时发送到stderr
LOG_PID Include PID with each message.//日志中包含进程ID
参数3:消息的类型,决定消息写到那个日志文件中
3.写日志信息
syslog()将产生一条日志信息,然后由日志守候进程将其发布到个日志文件中,该函数声明如下:
extern void syslog(int __pri, __const char *__fmt, ...);
第一个参数决定日志级别
第二个参数为日志输出格式
其后可变的参数为输出文档内容
8.3.3守候进程应用示例
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "signal.h"
#include "fcntl.h"
#include "sys/syslog.h"
#include "sys/param.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "time.h"
int init_daemon(const char *pname, int facility)
{
int pid;
int i;
//
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
signal(SIGTSTP, SIG_IGN);
signal(SIGHUP, SIG_IGN);
//
if(pid = fork()){
exit(EXIT_SUCCESS);
}
else if(pid < 0){
perror("fork");
exit(EXIT_FAILURE);
}
//
for(i = 0; i < NOFILE; ++i){
close(i);
}
//
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
//
chdir("/tmp");
umask(0);
signal(SIGCHLD, SIG_IGN);
openlog(pname, LOG_PID, facility);
return 0;
}
int main(int argc, char **argv)
{
FILE *fp;
time_t ticks;
init_daemon(argv[0], LOG_KERN);
while(1){
sleep(1);
ticks = time(NULL);
syslog(LOG_INFO, "%s", asctime(localtime(&ticks)));
}
}
运行结果
root@book-virtual-machine:/home/vicli/2_linux_program/_8_process# ps aux|grep daemon_exp
root 8100 0.0 0.0 4352 76 pts/18 S 16:39 0:00 ./daemon_exp
root 8112 0.0 0.0 16256 1012 pts/18 S+ 16:39 0:00 grep --color=auto daemon_exp
root@book-virtual-machine:/home/vicli/2_linux_program/_8_process# tail /var/log/messages
Aug 18 16:40:39 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:39 2020
Aug 18 16:40:40 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:40 2020
Aug 18 16:40:41 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:41 2020
Aug 18 16:40:42 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:42 2020
Aug 18 16:40:43 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:43 2020
Aug 18 16:40:44 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:44 2020
Aug 18 16:40:45 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:45 2020
Aug 18 16:40:46 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:46 2020
Aug 18 16:40:47 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:47 2020
Aug 18 16:40:48 localhost ./daemon_exp[8100]: Tue Aug 18 16:40:48 2020
root@book-virtual-machine:/home/vicli/2_linux_program/_8_process# kill 8100