写点什么

守护进程

用户头像
书旅
关注
发布于: 2020 年 08 月 14 日
守护进程

守护进程概念

守护进程(Daemon)是一种运行在后台的特殊进程

  • 守护进程独立于终端,并在后台周期性的执行任务或等待处理某些发生的事件。(Daemon独立于终端是为了避免进程在执行过程中的信息在终端上显示或者因收到终端上所产生的终端信息而中断。在Linux中从该控制终端开始运行的进程会依附于该控制终端,当控制终端被关闭时,相应的进程都会自动关闭,所以守护进程必须脱离控制终端)

  • 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机才随之一起停止运行

  • 守护进程一般都以root用户权限运行,因为要使用某些特殊的端口或者资源

  • 它们由init进程启动,并且没有控制终端,是一种执行日常事务的进程。所有的提供服务的进程基本上都是守护进程,通常也可以称为服务

  • 它不需要用户输入就能够运行且提供某种服务,不是对整个系统就是对某个用户程序服务

查看守护进程

#ps -efj



  • 守护进程基本上都是以超级用户启动,所以UID为0

  • 没有控制终端,所以TTY为?

Linux系统中常见守护进程

Linux系统的大多数服务器都是通过守护进程实现的,如系统日志进程syslogd、web服务器httpd

创建守护进程

(1)创建子进程,退出父进程

为了脱离终端,需要退出父进程,使得子进程可以在后台执行。在Linux中父进程先于子进程退出会导致子进程变为孤儿进程,而每当系统发现一个孤儿进程,就由有1号进程(init)收养它,这样,原先的子进程就会变成了init进程的子进程

(2)在子进程中创建新的会话

先介绍三个概念:

进程组:是一个或多个进程的集合。进程组由进程组ID来唯一标识。除了进程号PID之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID,且该进程组ID不会因组长进程的退出而受到影响

会话:多个进程组组成一个会话。

控制终端:每个会话可能会拥有一个控制终端(可以理解成我们常见的黑窗口,命令行),建立与控制终端连接的会话首进程叫做控制进程

每个进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。由于在调用fork函数时,子进程拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,为了使子进程不再受到它们的影响,需要调用setsid()来创建一个新的会话

setsid() :用于创建一个新的会话,并且让调用该函数的进程成为该会话组的组长。对于子进程来说其主要有三个作用:

  • 让子进程摆脱原会话的控制

  • 让子进程摆脱原进程组的控制

  • 让子进程摆脱原控制终端的控制

(3)改变当前目录为根目录

使用fork创建的子进程继承了父进程的当前的工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让根目录”/”作为守护进程的当前工作目录。这样就可以避免上述的问题。如有特殊的需求,也可以把当前工作目录换成其他的路径。改变工作目录的方法是使用chdir函数

(4)重设文件权限掩码

文件权限掩码:是指屏蔽掉文件权限中的对应位

例如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限(对应二进制为,rwx, 101)。由于fork函数创建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0(即,不屏蔽任何权限),可以增强该守护进程的灵活性。设置文件权限掩码的函数是umask。通常的使用方法为umask(0)

(5)再fork()一个子进程并终止父进程

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,,所以需要再次fork()一个子进程,并终止父进程。打开一个控制终端的前提条件是该进程必须为会话组组长,而我们通过第二次fork,确保了第二次fork出来的子进程不会是会话组组长

(6)关闭文件描述符

用fork创建的子进程也会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸载。在使用setsid调用之后,守护进程已经与所属的控制终端失去了联系,因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1、2(即,标准输入、标准输出、标准错误输出)的三个文件已经失去了存在的价值,也应该关闭

(7)守护进程退出处理

当用户需要外部停止守护进程时,通常使用kill命令停止该守护进程。所以,守护进程中需要编码来实现kill发出的signal信号处理,达到进程正常退出。

PHP实现守护进程

<?php
class DaemonCommand
{
private $info_dir = "/tmp";
private $pid_file = "";
private $terminate = false; //是否中断
private $workers_count = 0;
private $gc_enabled = null;
private $workers_max = 8; //最多运行8个进程
public function __construct($is_sington = false, $user = 'nobody', $output = "/dev/null")
{
$this->is_sington = $is_sington; //是否单例运行,单例运行会在tmp目录下建立一个唯一的PID
$this->user = $user;//设置运行的用户 默认情况下nobody
$this->output = $output; //设置输出的地方
$this->checkPcntl();
}
//检查环境是否支持pcntl支持
public function checkPcntl()
{
if (!function_exists('pcntl_signal_dispatch')) {
// PHP < 5.3 uses ticks to handle signals instead of pcntl_signal_dispatch
// call sighandler only every 10 ticks
declare(ticks=10);
}
// Make sure PHP has support for pcntl
if (!function_exists('pcntl_signal')) {
$message = 'PHP does not appear to be compiled with the PCNTL extension. This is neccesary for daemonization';
$this->_log($message);
throw new Exception($message);
}
//信号处理
pcntl_signal(SIGTERM, array(__CLASS__, "signalHandler"), false);
pcntl_signal(SIGINT, array(__CLASS__, "signalHandler"), false);
pcntl_signal(SIGQUIT, array(__CLASS__, "signalHandler"), false);
// Enable PHP 5.3 garbage collection
if (function_exists('gc_enable')) {
gc_enable();
$this->gc_enabled = gc_enabled();
}
}
// daemon化程序
public function daemonize()
{
global $stdin, $stdout, $stderr;
global $argv;
set_time_limit(0);
// 只允许在cli下面运行
if (php_sapi_name() != "cli") {
die("only run in command line mode\n");
}
// 只能单例运行
if ($this->is_sington == true) {
$this->pid_file = $this->info_dir . "/" . __CLASS__ . "_" . substr(basename($argv[0]), 0, -4) . ".pid";
$this->checkPidfile();
}
umask(0); //把文件掩码清0
if (pcntl_fork() != 0) { //是父进程,父进程退出
exit();
}
posix_setsid();//设置新会话组长,脱离终端
if (pcntl_fork() != 0) { //是第一子进程,结束第一子进程
exit();
}
chdir("/"); //改变工作目录
$this->setUser($this->user) or die("cannot change owner");
//关闭打开的文件描述符
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$stdin = fopen($this->output, 'r');
$stdout = fopen($this->output, 'a');
$stderr = fopen($this->output, 'a');
if ($this->is_sington == true) {
$this->createPidfile();
}
}
//--检测pid是否已经存在
public function checkPidfile()
{
if (!file_exists($this->pid_file)) {
return true;
}
$pid = file_get_contents($this->pid_file);
$pid = intval($pid);
if ($pid > 0 && posix_kill($pid, 0)) {
$this->_log("the daemon process is already started");
} else {
$this->_log("the daemon proces end abnormally, please check pidfile " . $this->pid_file);
}
exit(1);
}
//----创建pid
public function createPidfile()
{
if (!is_dir($this->info_dir)) {
mkdir($this->info_dir);
}
$fp = fopen($this->pid_file, 'w') or die("cannot create pid file");
fwrite($fp, posix_getpid());
fclose($fp);
$this->_log("create pid file " . $this->pid_file);
}
//设置运行的用户
public function setUser($name)
{
$result = false;
if (empty($name)) {
return true;
}
$user = posix_getpwnam($name);
if ($user) {
$uid = $user['uid'];
$gid = $user['gid'];
$result = posix_setuid($uid);
posix_setgid($gid);
}
return $result;
}
//信号处理函数
public function signalHandler($signo)
{
switch ($signo) {
//用户自定义信号
case SIGUSR1: //busy
if ($this->workers_count < $this->workers_max) {
$pid = pcntl_fork();
if ($pid > 0) {
$this->workers_count++;
}
}
break;
//子进程结束信号
case SIGCHLD:
while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) {
$this->workers_count--;
}
break;
//中断进程
case SIGTERM:
case SIGHUP:
case SIGQUIT:
$this->terminate = true;
break;
default:
return false;
}
}
/**
*开始开启进程
*$count 准备开启的进程数
*/
public function start($count = 1)
{
$this->_log("daemon process is running now");
pcntl_signal(SIGCHLD, array(__CLASS__, "signalHandler"), false); // if worker die, minus children num
while (true) {
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
if ($this->terminate) {
break;
}
$pid = -1;
if ($this->workers_count < $count) {
$pid = pcntl_fork();
}
if ($pid > 0) {
$this->workers_count++;
} elseif ($pid == 0) {
// 这个符号表示恢复系统对信号的默认处理
pcntl_signal(SIGTERM, SIG_DFL);
pcntl_signal(SIGCHLD, SIG_DFL);
if (!empty($this->jobs)) {
while ($this->jobs['runtime']) {
if (empty($this->jobs['argv'])) {
call_user_func($this->jobs['function'], $this->jobs['argv']);
} else {
call_user_func($this->jobs['function']);
}
$this->jobs['runtime']--;
sleep(2);
}
exit();
}
return;
} else {
sleep(2);
}
}
$this->mainQuit();
exit(0);
}
//整个进程退出
public function mainQuit()
{
if (file_exists($this->pid_file)) {
unlink($this->pid_file);
$this->_log("delete pid file " . $this->pid_file);
}
$this->_log("daemon process exit now");
posix_kill(0, SIGKILL);
exit(0);
}
// 添加工作实例,目前只支持单个job工作
public function setJobs($jobs = array())
{
if (!isset($jobs['argv']) || empty($jobs['argv'])) {
$jobs['argv'] = "";
}
if (!isset($jobs['runtime']) || empty($jobs['runtime'])) {
$jobs['runtime'] = 1;
}
if (!isset($jobs['function']) || empty($jobs['function'])) {
$this->log("你必须添加运行的函数!");
}
$this->jobs = $jobs;
}
//日志处理
private function _log($message)
{
printf("%s\t%d\t%d\t%s\n", date("c"), posix_getpid(), posix_getppid(), $message);
}
}
//调用方法1
$daemon = new DaemonCommand(true);
$daemon->daemonize();
$daemon->start(2);//开启2个子进程工作
work();
//调用方法2
$daemon = new DaemonCommand(true);
$daemon->daemonize();
$daemon->addJobs(array('function' => 'work', 'argv' => '', 'runtime' => 1000));//function 要运行的函数,argv运行函数的参数,runtime运行的次数
$daemon->start(2);//开启2个子进程工作
//具体功能的实现
function work()
{
echo "测试1";
}
?>





发布于: 2020 年 08 月 14 日阅读数: 62
用户头像

书旅

关注

公众号:IT猿圈 2019.04.11 加入

还未添加个人简介

评论

发布
暂无评论
守护进程