1.普通进程
普通进程有对应的终端,如果终端退出,那么对应的进程也就消失了,并且终端被占住了,输入各种命令这个终端都没有反应。
普通进程的父进程是一个 bash。
2.守护进程
守护进程是一种长期运行的进程(守护进程的生存期不一定长,但一般应该这样做),一般是操作系统启动的时候它就启动,操作系统关闭的时候它才关闭。
守护进程跟终端无关联,也就是说它们没有控制终端,所以控制终端退出,也不会导致守护进程退出。
守护进程是在后台运行的,不会占着终端,终端可以执行其他命令。
Linux 操作系统本身是有很多的守护进程在默默地运行,维持着系统的日常活动。
init
是系统守护进程,它负责启动各运行层次特定的系统服务,所以很多进程的 PPID 是 init
,并且这个 init
也负责收养孤儿进程。
大多数守护进程都是以超级用户特权运行的,守护进程没有控制终端,TTY
这列显示的是 ?
- 内核守护进程以无控制终端方式启动
- 普通守护进程无控制终端可能是守护进程调用了
setsid
的结果
3.文件描述符
文件描述符其实就是一个正数,用来标识一个文件。当我们打开一个存在的文件或者创建一个新文件,操作系统都会返回这个文件描述符(其实就是代表这个文件的),后续对这个文件的操作的一些函数,都会用到这个文件描述符作为参数。
Linux 中有三个特殊的文件描述符,数字分别为 0
、1
、2
。
- 0:标准输入(键盘),对应的符号常量叫
STDIN_FILENO
- 1:标准输出(屏幕),对应的符号常量叫
STDOUT_FILENO
- 2:标准错误(屏幕),对应的符号常量叫
STDERR_FILENO
类 Unix 操作系统默认从 STDIN_FILENO
读数据,向 STDOUT_FILENO
来写数据,向 STDERR_FILENO
来写错误。
类 Unix 操作系统中“一切皆文件”,与其说把标准输入、标准输出、标准错误都看成文件,倒不如说像看待文件一样看待标准输入、标准输出、标准错误,像操作文件一样操作标准输入、标准输出、标准错误。
同时,程序一旦运行起来,这三个文件描述符 0
、1
、2
会被自动打开,自动指向对应的设备。
虽然文件描述符是数字,但为了更好地理解,我们可以把文件描述符看成指针。
4.输入输出重定向
输出重定向:在命令行中用 >
即可。
输入重定向:在命令行中用 <
即可。
5.空设备
/dev/null
是一个特殊的设备文件,它丢弃一切写入其中的数据(像黑洞一样)。
6.守护进程的实现范例
守护进程编写规则:
-
fork()
一个子进程出来,然后父进程退出。fork()
的目的是想成功调用setsid()
来建立新会话,目的是让子进程有单独的 sid,而且子进程也成为了一个新进程组的组长进程,同时子进程不关联任何终端了。 -
调用
umask(0)
,其中umask
是个函数,将其设置为 0 0 0,不要让它来限制文件权限,以免引起混乱。 -
守护进程是在后台运行,它不应该从键盘上接收任何东西,也不应该把输出结果打印到屏幕或者终端上来,所以,一般按照江湖规矩,我们要把守护进程的标准输入、标准输出重定向到空设备,从而确保守护进程不从键盘接收任何东西,也不把输出结果打印到屏幕。
-
守护进程虽然可以通过终端启动,但是和终端不挂钩。守护进程可以用命令启动,如果想开机启动,则需要借助系统初始化脚本来启动。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
// 创建守护进程,创建成功则返回1,否则返回-1
int ngx_daemon()
{
int fd;
switch (fork()) // fork()子进程
{
case -1:
// 创建子进程失败,这里可以写日志等等
return -1;
case 0:
// 子进程,走到这里,直接break
break;
default:
// 父进程,直接退出
exit(0);
}
// 只有子进程流程才能走到这里
if (setsid() == -1) // 脱离终端,终端关闭,将跟此子进程无关
{
// 记录错误日志等等
return -1;
}
umask(0); // 设置为0,不要让它来限制文件权限,以免引起混乱
fd = open("/dev/null", O_RDWR); // 打开空设备,以读写方式打开
if (fd == -1)
{
// 记录错误日志等等
return -1;
}
// dup2先关闭STDIN_FILENO(这是规矩,已经打开的描述符,动它之前,先close)
// dup2复制文件描述符,类似于指针赋值,把第一个参数指向的内容赋给了第二个参数,相当于指针指向null,让/dev/null成为标准输入
if (dup2(fd, STDIN_FILENO) == -1)
{
// 记录错误日志等等
return -1;
}
// dup2先关闭STDOUT_FILENO(这是规矩,已经打开的描述符,动它之前,先close)
// dup2复制文件描述符,类似于指针赋值,把第一个参数指向的内容赋给了第二个参数,相当于指针指向null,让/dev/null成为标准输出
if (dup2(fd, STDOUT_FILENO) == -1)
{
// 记录错误日志等等
return -1;
}
if (fd > STDERR_FILENO) // fd应该是3,这个应该成立
{
if (close(fd) == -1) // 释放资源,这样这个文件描述符就可以被复用,不然这个数字(文件描述符)会被一直占着
{
// 记录错误日志等等
return -1;
}
}
return 1;
}
int main(int argc, char* const* argv)
{
if (ngx_daemon() != 1)
{
// 创建守护进程失败,可以做失败后的处理,比如写日志等等
return 1;
}
else
{
// 创建守护进程成功,执行守护进程中要干的活
for (;;)
{
sleep(1);
// 这里就算打印也没用,现在标准输出指向空设备(/dev/null),打印不出任何结果(不显示任何结果)
printf("休息1秒,进程id=%d!\n", getpid());
}
}
return 0;
}
7.守护进程不会收到的信号
守护进程不会收到来自内核的 SIGHUP
信号,也就是说,如果守护进程收到了 SIGHUP
信号,那么肯定是另外的进程发的。
很多守护进程把 SIGHUP
信号作为通知信号,表示配置文件已经发生改动,守护进程应该重新读入其配置文件。
守护进程不会收到来自内核的 SIGINT
信号(Ctrl+C)、SIGWINCH
信号(终端窗口大小改变)。