重学计算机(十二、守护进程)

时隔多年,每次碰到守护进程就想起当年,当年在大学学linux的时候,需要做个大作业,然后老师给了好多个题目,翻来翻去,发现就这个守护进程最简单,那就选守护进程吧。

选了守护进程的题目之后,发现还是不会做(哎,当年就没想过做linux相关的,真是人算不如天算)。不会做怎么办呢?那就找同学借鉴了(说是借鉴其实就是抄),然后就找到了叶某人的抄了过来,好像当时是完全抄过来的,因为当年确实对守护进程很懵逼。抄完了,那就交作业了。

交了作业,就开始答辩了,我们组是在前面答辩,答辩说了一些,也忘记具体说啥了,反正最后的评分,比抄叶某人的评分高了很多,抄的人分数反而更高,那时候嘲笑了叶某人好久,哈哈哈。

回忆总是美好的,现在该卷还是要卷,开始我们今天的守护进程之旅吧。

12.1 前后台进程

其实前后台进程,不应该放在这里讲的,不过都安排在这里了,就在这里吧,这样也好区别守护进程。

12.1.1 前台进程

先来看看前台进程,前台进程很简单就是运行在前台的,绑定了控制终端的,可以接受控制终端的信号。

我们来写一个前台进程:

#include <stdio.h>

int main()
{
    printf("test\n");
    while(1);
    return 0;
}

当然真正的代码不能像我这样写,这是一个测试代码,我们接着来编译运行:

root@ubuntu:~/c_test/12# gcc test.c -o test
root@ubuntu:~/c_test/12# ./test
test



ls		# 输入ls没有反应

ls		# 输入ls没有反应
^C		# 输入中断键,退出了
root@ubuntu:~/c_test/12# 

这种就是前台进程,我们再来看看属性:

  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  1415   1513   1513   1415 pts/0      1513 R+       0   0:09 ./test

R+:表示正在运行的进程,+是前台进程

TTY:就是控制终端,pts就是网络终端

TPGID:进程连接到的tty(终端)所在的前台进程组的ID

12.1.2 后台进程

接着我们来看看后台进程,后台进程其实也比较简单,启动的时候加一个&,就可以了

root@ubuntu:~/c_test/12# ./test &
[1] 1571
test
root@ubuntu:~/c_test/12# 

接着我们来看看状态:

PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
1415   1571   1571   1415 pts/0      1415 R        0   0:19 ./test

STAT:后面没有+,说明不是前台进程。

TTY:但是终端还是pts/0

但是我们通过终端去发送中断信号,后台进程是接收不到的,只能是关闭终端,终端都关闭了,终端下的进程自然都会关闭。

这个实验,只能自己看了。

12.1.3 nohup命令

在上家公司工作的时候,就发现了这个命令来启动后端程序,现在我们来看看:

root@ubuntu:~/c_test/12# nohup ./test &
[1] 1673
root@ubuntu:~/c_test/12# nohup: ignoring input and appending output to 'nohup.out'

root@ubuntu:~/c_test/12# ls
nohup.out  test  test.c
root@ubuntu:~/c_test/12# cat nohup.out 

好像有那么点像模像样,我们查看一下状态:

PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
1653   1673   1673   1653 pts/0      1653 R        0   0:13 ./test

好像还是有控制终端,我们把控制终端关闭了测试一下。

PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
   1   1673   1673   1653 ?            -1 R        0   5:42 ./test

好神奇哦,关闭了终端,TTY也跟着改了,这样看着就是真正的守护进程了。

nohup命令做了一下事情:

  • 阻止SIGHUP信号发到这个进程。
  • 关闭标准输入。该进程不再能够接收任何输入,即使运行在前台。
  • 重定向标准输出和标准错误到文件nohup.out

这一篇文章不错:[Linux 守护进程的启动方法]

12.2 守护进程

上面吹了这么多水,终于来到了今天的重点,守护进程的实现,我们在代码中实现守护进程,启动的的时候不用带nohup和&,直接./就可以了。

12.2.1 守护进程步骤

  1. 创建子进程,父进程退出(必须的)

    有如下原因:

    第一:父进程有可能是进程组组长(在命令行启动下是肯定的),从而不能够执行后面的setsid函数,子进程继承了父进程的进程组ID,所以子进程一定不是进程组组长,所以子进程一定可以执行setsid。

    第二:父进程的退出,shell会以为这条命令执行结束了,从而让子进程在后台执行,也就是变成孤儿进程。

  2. 在子进程创建新会话(必须)

    这一步是调用setsid函数,也是关键的一步,这一步是脱离了终端,因此终端发送的信号,都不会影响到子进程。子进程调用这个函数之后,会成为新会话的首进程,成为一个新进程组的组长进程,没有控制终端

  3. 修改当前目录为根目录(不是必须)

    只有根目录是一定存在的,如果是其他目录,有可能存在卸载等问题,所以可以修改成根目录。chdir("/")

  4. 重设文件权限掩码(不是必须)

    文件权限掩码是继承父进程的,有可能父进程的权限有点低,所以为了增加守护进程的灵活性,需要重新设置文件掩码。umask(0)。

  5. 再次fork,父进程退出(不是必须)

    daemon可能会打开一个终端设备,这个打开终端设备可能会成为daemon进程的控制终端。既然如此,为了确保万无一失,只有确保daemon不是会话首进程,所以需要再次fork。

  6. 关闭文件描述符(不是必须)

    文件描述符也是继承自父进程的,在守护进程中是不需要这些的,所以都需要关闭。标准输入,标准输出,标准错误这些都不需要,统一关闭。

12.2.2 守护进程的简单实现

先按照上面的步骤写一波守护进程的实现

#include <unistd.h>
#include <stdio.h>
#include <sys/resource.h>
#include <fcntl.h>

void daemonize()
{
    pid_t pid	= -1;
    struct rlimit rl;
    
    // 获取父进程打开文件
    if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
    	printf("getrlimit err\n");
        
    // 1.fork,父进程退出
    pid = fork();
    if(pid < 0)
    {
        printf("fork err\n");
        return 0;
    } else if(pid > 0)
    {
        //父进程退出
        _exit(0);
    }
    
    // 下面都是子进程
   	
    // 2.设置新会话 setsid
    // 创建一个新会话,并且是进程组的组长
    setsid();
    
    // 3.设置根目录 chdir
    chdir("/");
    
    // 4.设置文件掩码 umask
    umask(0);
    
    // 5.再次fork,防止创建一个新的终端
    if((pid = fork()) < 0)
    {
        printf("fork err\n");
        return 0;
    } else if(pid > 0)
    {
        _exit(0);
    }
    
    // 6.关闭标准输入,标准输出,标准错误
    if(rl.rlim_max == RLIM_INFINITY)
        	rl.rlim_max = 1024;
    for(int i=0; i<rl.rlim_max; i++)
        close(i);
    
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
}

就这样吧。

12.2.3 守护进程的进化版

有些守护进程需要做单实例,可以使用文件和记录锁来做单实例,第一次启动守护进程,就会创建文件,并且加一把写锁,之后如果再有守护进程创建,再去写这个文件,就会失败,从而做到了单实例。

还有守护进程有以下的惯例:

  1. 若守护进程使用锁文件,那么该文件通常存储在/var/run目录中,需要超级用户权限才能在此目录下创建文件,锁文件名字通常是name.pid
  2. 若守护进程支持配置选项,那么配置文件通常存放在/etc目录中,配置文件名字为name.conf
  3. 守护进程可用命令行启动,但是也可以在系统初始化脚本启动。(/etc/rc*或/etc/init.d/*)
  4. 守护进程会捕捉SIGHUP信号,然后重新读取配置文件。

写了一个简单的例子,不是完全的守护进程的例子。等之后做项目的时间再写个完整的。

#include <unistd.h>
#include <stdio.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

void daemonize()
{
    pid_t pid	= -1;
    struct rlimit rl;

    // 获取父进程打开文件
    if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
    	printf("getrlimit err\n");
        
    // 1.fork,父进程退出
    pid = fork();
    if(pid < 0)
    {
        printf("fork err\n");
        return ;
    } else if(pid > 0)
    {
        //父进程退出
        _exit(0);
    }
    
    // 下面都是子进程
   	
    // 2.设置新会话 setsid
    // 创建一个新会话,并且是进程组的组长
    setsid();

    // 3.设置根目录 chdir
    chdir("/");
    
    // 4.设置文件掩码 umask
    umask(0);
    
    // 5.再次fork,防止创建一个新的终端
    if((pid = fork()) < 0)
    {
        printf("fork err\n");
        return ;
    } else if(pid > 0)
    {
        _exit(0);
    }

    // 6.关闭标准输入,标准输出,标准错误
    if(rl.rlim_max == RLIM_INFINITY)
        	rl.rlim_max = 1024;
    for(int i=0; i<rl.rlim_max; i++)
        close(i);
    
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
}

#define LOCKFILE    "/var/run/daemon.pid"
#define LOCKMODE    (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

int lockfile(int fd)
{
    struct flock fl;

    fl.l_type = F_WRLCK;
    fl.l_start = 0;
    fl.l_whence = SEEK_SET;
    fl.l_len = 0;

    return (fcntl(fd, F_SETLK, &fl));
}

int already_running(void)
{
    int     fd;
    char    buf[16];

    fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
    if(fd < 0)
    {
        printf("fd open\n");
        exit(1);
    }

    if(lockfile(fd) < 0)
    {
        if(errno == EACCES || errno == EAGAIN) {
            close(fd);
            return 1;
        }
        printf("lockfile\n");
        exit(1);
    }

    ftruncate(fd, 0);
    sprintf(buf, "%ld", (long)getpid());
    write(fd, buf, strlen(buf)+1);
    return 0;
}


int main()
{
    daemonize();

    while(1);

    return 0;
}

12.2.4 glibc中的daemon

其实在glibc中也封装了一个daemon函数,从而帮我们将程序转化成daemon进程。

int
daemon (int nochdir, int noclose)
{
	int fd;

	switch (__fork()) {
	case -1:
		return (-1);
	case 0:
		break;
	default:
		_exit(0);
	}

	if (__setsid() == -1)
		return (-1);

	if (!nochdir)       // 是否切换
		(void)__chdir("/");

	if (!noclose) {		// 是否关闭
		struct stat64 st;

		if ((fd = __open_nocancel(_PATH_DEVNULL, O_RDWR, 0)) != -1
		    && (__builtin_expect (__fxstat64 (_STAT_VER, fd, &st), 0)
			== 0)) {
			if (__builtin_expect (S_ISCHR (st.st_mode), 1) != 0
#if defined DEV_NULL_MAJOR && defined DEV_NULL_MINOR
			    && (st.st_rdev
				== makedev (DEV_NULL_MAJOR, DEV_NULL_MINOR))
#endif
			    ) {
				(void)__dup2(fd, STDIN_FILENO);
				(void)__dup2(fd, STDOUT_FILENO);
				(void)__dup2(fd, STDERR_FILENO);
				if (fd > 2)
					(void)__close (fd);
			} else {
				/* We must set an errno value since no
				   function call actually failed.  */
				__close_nocancel_nostatus (fd);
				__set_errno (ENODEV);
				return -1;
			}
		} else {
			__close_nocancel_nostatus (fd);
			return -1;
		}
	}
	return (0);
}

其实看这个源码跟我们上面讲的也差不多,只不多这是有两个参数。

nochdir:用来控制是否将当前目录切换到根目录。(看代码也理解了)

noclose :用来控制是否将标准输入,标准输出,标准错误重定向到/dev/null。(这个看代码也理解)

12.3 总结

虽然这篇守护进程的代码没有写全,但是原理也都介绍了,具体到项目的时候,在把守护进程写全,因为服务器的代码基本都是守护进程的方式存在的,不急,未来的路还很长。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值