1. 几点认识:
- java中有两类线程:user thread(用户线程),daemon thread(守护线程)
- 守护线程为其他线程的运行提供服务,例如GC线程(垃圾回收线程),内存管理线程。
- 虚拟机判断程序执行结束的标准时不考虑守护线程:如果user thread全部撤离,daemon thread因为无服务对象,所以虚拟机也就退出了。
- public final void setDaemon(boolean on) :用户自行设定守护线程
- 是JVM模拟了操作系统中的“守护进程”而定义出的一种机制。但与守护进程有所不同
- 不能将正在运行的线程设置为守护线程:thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。(守护进程是创建后,让其脱离会话+进程组+控制终端控制)
- 在Daemon中创建的线程也是守护线程。(守护进程fork()出的进程不是守护进程,因为其父进程不再是init进程)
- 不是所有的应用都可以分配给守护线程来服务。比如读写操作或计算逻辑,因为在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. 守护进程
- 定义:Linux或Unix操作系统中,在系统引导时会开启很多服务,这些服务就是守护进程,即后台服务进程。例如amd(自动安装NFS守候进程)、Lpd(打印服务器)等。
- 生存期长。通常在系统引导装入是启动,在系统关闭时终止。
- 独立于控制终端并且周期性地执行某种任务。从而其在执行过程中的信息不会在终端显示,并且也不会终端进程信息所打断。
- 终端:Linux中,每个系统与用户交流的界面称为终端(terminal),每个从此终端开始运行的进程都会依附于此终端,该终端就称为这些进程的控制终端。
- 当控制终端被关闭时,相应的用户进程都会自动关闭。而守护进程能突破这种限制,即它在系统关闭使才会终止。
- 什么时候使用守护进程?
- 如果不想让某个进程因为用户/终端/其他变化而受到影响,则将该进程设为守护进程。
- 创建过程:
- 创建子进程,父进程退出。
- 即创建子进程后,显示退出父进程,造成在终端这一进程已运行完毕的假象。之后的操作都由子进程完成。形式上做到与控制终端脱离。
- 孤儿进程:父进程先于子进程退出,则称为孤儿进程。系统发现一个孤儿进程后,就自动有1号进程(init进程)收养,即该子进程称为init进程的子进程。
- 在子进程中创建新会话。
- 继承:调用fork()函数,子进程会拷贝父进程的所有会话期、进程组、控制终端等。需要重设这些,才使子进程真正的与控制终端脱离。
- 进程组:一个或多个进程集合。每个进程有进程pid,进程组有组ID。pid和进程组ID都是一个进程的必备属性。每个进程组都有组长进程,其进程号等于进程组ID。当进程组ID不受组长进程退出的影响。
- 会话期:一个或多个进程组集合。通常,一个会话始于用户登录,终于用户退出,这之间的所有进程都属于该会话期。
- setsid:创建新会话,并担任该会话组组长。有3个作用:
- 让进程摆脱原会话的控制
- 让进程摆脱原会话组的控制
- 让进程摆脱原控制终端的控制
- 改变当前目录为根目录
- 继承:fork()创建的子进程还拷贝了父进程的当前工作目录。需要重设。
- 进程运行中,当前目录所在文件系统是不能卸载的,即原工作目录无法卸载。可能造成很多麻烦,如需要进入单用户模式。所以必须重设当前目录。
- chdir("/"):重设为根目录
- 重设文件权限掩码
- 文件权限掩码:屏蔽掉文件权限中的对应位。有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。
- 继承:fork()创建的子进程还继承了父进程的文件权限掩码。
- umask(0):重设为0,灵活性更强。
- 关闭文件描述符
- 继承:fork()创建的子进程从父进程继承了一些已经打开了的文件。被打开的进程可能永远不会被守护进程使用,却消耗资源。所以必须手动关闭文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)。
- 守护进程退出处理
- 可能需要支持用户在外部手动停止守护进程运行,通常使用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 "; len = strlen(buf); pc = fork(); //第一步 if(pc<0){ printf("error fork "); 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; }