守护线程、守护进程

时间:2021-04-15 14:55:51

1. 几点认识:

  1. java中有两类线程:user thread(用户线程),daemon thread(守护线程)
  2. 守护线程为其他线程的运行提供服务,例如GC线程(垃圾回收线程),内存管理线程。
  3. 虚拟机判断程序执行结束的标准时不考虑守护线程:如果user thread全部撤离,daemon thread因为无服务对象,所以虚拟机也就退出了。
  4. public final void setDaemon(boolean on) :用户自行设定守护线程
  5. 是JVM模拟了操作系统中的“守护进程”而定义出的一种机制。但与守护进程有所不同
    1. 不能将正在运行的线程设置为守护线程:thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。(守护进程是创建后,让其脱离会话+进程组+控制终端控制)
    2. 在Daemon中创建的线程也是守护线程。(守护进程fork()出的进程不是守护进程,因为其父进程不再是init进程)
    3. 不是所有的应用都可以分配给守护线程来服务。比如读写操作或计算逻辑,因为在daemon还没操作时,虚拟机已经退出了。

  例子:

public class DaemonThreadTest {
    public static void main(String[] args){
        Thread t1 = new MyThread();
        Thread t2 = new Thread( new MyDaemon() );
        t2.setDaemon(true);
        t1.start();
        t2.start();
    }
}

class MyDaemon implements Runnable{
    public void run(){
        for( long i=0; i<9999999L; i++ )
            System.out.println("后台线程第" + i + "次执行!");
        try{
            Thread.sleep(7);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}

class MyThread extends Thread{
    public void run(){
        for( int i=0; i<5; i++ )
            System.out.println("线程第" + i + "次执行!");
        try{
            Thread.sleep( 7 );
        }catch( InterruptedException e ){
            e.printStackTrace();
        }            
    }
}

 

输出:

后台线程第0次执行!
线程第0次执行!
后台线程第1次执行!
线程第1次执行!
后台线程第2次执行!
线程第2次执行!
后台线程第3次执行!
线程第3次执行!
后台线程第4次执行!
...
后台线程第339次执行!

 

即前台线程保证执行完毕,后台线程不一定。

2. 守护进程

  1. 定义:Linux或Unix操作系统中,在系统引导时会开启很多服务,这些服务就是守护进程,即后台服务进程。例如amd(自动安装NFS守候进程)、Lpd(打印服务器)等。
  2. 生存期长。通常在系统引导装入是启动,在系统关闭时终止。
  3. 独立于控制终端并且周期性地执行某种任务。从而其在执行过程中的信息不会在终端显示,并且也不会终端进程信息所打断。
    • 终端:Linux中,每个系统与用户交流的界面称为终端(terminal),每个从此终端开始运行的进程都会依附于此终端,该终端就称为这些进程的控制终端。
    • 当控制终端被关闭时,相应的用户进程都会自动关闭。而守护进程能突破这种限制,即它在系统关闭使才会终止。
  4. 什么时候使用守护进程?
    • 如果不想让某个进程因为用户/终端/其他变化而受到影响,则将该进程设为守护进程。
  5. 创建过程:
    1. 创建子进程,父进程退出。
      • 即创建子进程后,显示退出父进程,造成在终端这一进程已运行完毕的假象。之后的操作都由子进程完成。形式上做到与控制终端脱离。
      • 孤儿进程:父进程先于子进程退出,则称为孤儿进程。系统发现一个孤儿进程后,就自动有1号进程(init进程)收养,即该子进程称为init进程的子进程。
    2. 在子进程中创建新会话。
      • 继承:调用fork()函数,子进程会拷贝父进程的所有会话期、进程组、控制终端等。需要重设这些,才使子进程真正的与控制终端脱离
      • 进程组:一个或多个进程集合。每个进程有进程pid,进程组有组ID。pid和进程组ID都是一个进程的必备属性。每个进程组都有组长进程,其进程号等于进程组ID。当进程组ID不受组长进程退出的影响。
      • 会话期:一个或多个进程组集合。通常,一个会话始于用户登录,终于用户退出,这之间的所有进程都属于该会话期。
      • setsid:创建新会话,并担任该会话组组长。有3个作用:
        • 让进程摆脱原会话的控制
        • 让进程摆脱原会话组的控制
        • 让进程摆脱原控制终端的控制
    3. 改变当前目录为根目录
      • 继承:fork()创建的子进程还拷贝了父进程的当前工作目录。需要重设。
      • 进程运行中,当前目录所在文件系统是不能卸载的,即原工作目录无法卸载。可能造成很多麻烦,如需要进入单用户模式。所以必须重设当前目录。
      • chdir("/"):重设为根目录
    4. 重设文件权限掩码
      • 文件权限掩码:屏蔽掉文件权限中的对应位。有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。
      • 继承:fork()创建的子进程还继承了父进程的文件权限掩码。
      • umask(0):重设为0,灵活性更强。
    5. 关闭文件描述符
      • 继承:fork()创建的子进程从父进程继承了一些已经打开了的文件。被打开的进程可能永远不会被守护进程使用,却消耗资源。所以必须手动关闭文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)。
    6. 守护进程退出处理
      • 可能需要支持用户在外部手动停止守护进程运行,通常使用kill命令。编码实现kill发出的signal信号处理,达到线程正常退出。
signal(SIGTERM, sigterm_handler);
void sigterm_handler(int arg)
{
_running = 0;
}

  创建守护进程的一个实例:

守护线程、守护进程守护线程、守护进程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAXFILE 65535
void sigterm_handler(int arg);
volatile sig_atomic_t _running = 1;
int main()
{
pid_t pc,pid;
int i,fd,len,flag = 1;
char *buf="this is a Dameon\n";
len = strlen(buf);
pc = fork(); //第一步
if(pc<0){
printf("error fork\n");
exit(1);
}
else if(pc>0)
exit(0);
pid = setsid(); //第二步[1] 
if (pid < 0)
perror("setsid error");
chdir("/"); //第三步
umask(0); //第四步
for(i=0;i<MAXFILE;i++) //第五步
close(i);
signal(SIGTERM, sigterm_handler);
while( _running )
{
if( flag ==1 &&(fd=open("/tmp/daemon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
perror("open");
flag=0;
exit(1);
}
write(fd,buf,len);
close(fd);
usleep(10*1000); //10毫秒
}
}
void sigterm_handler(int arg)
{
_running = 0;
}
createDeamon