Linux进程关系&守护进程

进程组:

每个进程除了是一个单独的进程外,它还属于一个进程组,进程组是一个或多个进程的集合。每个进程除了有一个进程ID外还有一个进程组ID,用于标记该进程属于那个进程组,每个进程组都可以有一个组长进程,组长进程的标识是:其进程ID等于进程组ID,组长进程可以创建一个进程组、创建进程组中的进程、退出(只要进程组中还有一个进程存在,则进程组存在,无论组长进程存在与否),另外,每个进程组的标识是唯一的,通常进程组中的所有原始进程与同一个作业相关联,前台进程组可以接收来自同一终端的各种信号。
示例:

[DELL@localhost lesson14]$ sleep 100 |sleep 200 |sleep 300 &
[1] 4628
[DELL@localhost lesson14]$ ps axj |head -n1
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
[DELL@localhost lesson14]$ ps axj |grep sleep |grep -v grep
  859  4625   850   850 ?           -1 S        0   0:00 sleep 60
 4505  4626  4626  4505 pts/0     4646 S     1000   0:00 sleep 100
 4505  4627  4626  4505 pts/0     4646 S     1000   0:00 sleep 200
 4505  4628  4626  4505 pts/0     4646 S     1000   0:00 sleep 300
[DELL@localhost lesson14]$

说明:
&:表示将进程组放到后台进行。
进程组中的进程:4626、4627、4628。
组长进程:4626,进程组中的第一个进程。
用kill -9杀死组长进程,进程组还是存在的,可以自行尝试。
ps选项说明:
a:不仅列出当前用户的进程,也列出其他用户的进程。
x:不仅列出有控制终端的进程,也列出没有控制终端的进程。
j:表示列出有作业控制相关的信息。

作业:

shell分前后台控制的不是进程,而是作业。shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。
例如:在终端中我们经常会遇到这样的现象,一旦运行一个进程或进程组,shell便没有办法接收用户的输入命令了,当该进程或进程组运行结束,shell便又可以接收用户输入的命令了,那么这是什么原因呢?
原来,当在终端中运行一个作业(进程或进程组)时,会将该作业在前台运行,shell被提到后台,因为只有前台进程才能在控制终端下读取数据,所以shell无法接收用户命令了,当前台作业运行结束后,shell便又被提到前台,所以便又可以接受用户的命令了。
作业类似于进程组的概念,但是作业和进程组并不是没有区别,它们的区别在于:如果作业中的某进程又创建了子进程,那么该子进程不属于作业。
一旦前台作业运行结束,shell就把自己提到前台,如果原来的前台作业中的进程创建的子进程还存在,它自动变成后台进程组。
test.c:

#include<stdio.h>
#include<unistd.h>

int main(){
  pid_t id = fork();
  if(id < 0){ 
    perror("fork");
    return -1; 
  }else if(id > 0){ 
    int i = 5;
    while(i){
      printf("I am father:%d,is %d\n",getpid(),i--);
      sleep(1);
    }   
  }else{
    while(1){
      printf("I am child:%d\n",getpid());
      sleep(1);
    }   
  }
  return 0;
}

说明:当程序运行时,前台新起一个作业,只包含父进程,随后该父进程创建子进程,但该子进程不属于前台作业。5s内shell无法接收命令,因为此时父进程所在的前台作业还在运行,5s后父进程退出,前台作业结束,此时子进程还在运行,子进程会被自动提到后台,shell被提到前台可以接受用户命令了,但子进程还在运行会一直打印消息。
注:后面我们在讨论进程组与作业时并不作特别区别。

会话:

会话是一个或多个进程组的集合,一个会话可以有一个控制终端,这通常是登陆到其上的终端设备(本地终端)或伪终端设备(网络登陆),建立与控制终端连接的会话首进程被称为控制进程,一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组,所以一个会话中应该包含:控制进程、一个前台进程组、任意多个后台进程组、终端。
示例:

[DELL@localhost lesson14]$ sleep 100 |sleep 200 |sleep 300 &
[1] 7777
[DELL@localhost lesson14]$ ps axj |head -n1
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
[DELL@localhost lesson14]$ ps axj |grep sleep |grep -v grep
 4505  7775  7775  4505 pts/0     7802 S     1000   0:00 sleep 100
 4505  7776  7775  4505 pts/0     7802 S     1000   0:00 sleep 200
 4505  7777  7775  4505 pts/0     7802 S     1000   0:00 sleep 300
  859  7801   850   850 ?           -1 S        0   0:00 sleep 60
[DELL@localhost lesson14]$ ps aux |grep -E 4505 |grep -v grep
DELL      4505  0.0  0.0 116820  3540 pts/0    Ss   14:07   0:00 bash
[DELL@localhost lesson14]$ 

说明:
SID:会话id,三个进程属于同一个进程组,由同一个父进程创建,同一个会话(4505)。
那么4505进程是谁呢?
通过ps aux |grep -E 4505 |grep -v grep命令我们可以知道4505进程(会话首进程)是bash进程,并且三个子进程的父进程也是bash进程,即进程组是由会话首进程创建的。

进程组、作业、会话间的关系:
这里写图片描述

守护进程:

初识守护进程:
守护进程也叫精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性的执行某些任务或等待处理某些要发生的事情。Linux中大多数服务器就是用守护进程实现的,同时守护进程还可以完成很多系统任务,所以守护进程是一种很有用的进程。
Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能和用户交互,其它进程都是在用户登录或运行程序时创建的,在运行结束或用户注销时终止,但系统服务进程不受用户登录与注销影响,它们一直在运行,这种进程便是守护进程。
ps axj指令:ps axj指令可以查看系统中的进程(包括守护进程)。
TPGID:进程连接到的终端上的前台进程组ID,如果进程没有连接到终端,则为-1。
COMMAND:此栏说明启动该进程的命令,如果名字用【】包起来,则表示内核线程,这些线程在内核里创建,没有用空间代码,因此没有程序文件名和命令行。通常以k开头表示kernel。守护进程通常以d结尾表示Deamon。

创建守护进程:

 #include <unistd.h>

 int daemon(int nochdir, int noclose);

说明:daemon()函数用于分离自己进程的控制终端,使进程成为后台守护进程在后台运行。
参数:
nochdir:为0时,daemon()更改调用进程的工作目录为根目录,否则不更改。
noclose:为0时,重定向标准输入、标准输出、标注错误文件描述符到/dev/null,否则不重定向。
返回值:因为该函数调用fork()函数,当fork()调用成功,父进程调用_exit(2)退出,所以报出的错误都是子进程输出的。函数调用成功返回0,失败返回-1,并设置errno。

模拟实现daemon函数(创建守护进程):
实现守护进程的重要一步是调用setsid()函数创建一个会话,所以下面先来认识一下setsid()函数:

#include <unistd.h>

pid_t setsid(void);

说明:此函数的用处是创建一个新会话,当调用进程是会话的领头进程时调用失败并返回-1,如果调用成功,使调用进程成为新会话的领头进程,返回新会话的ID,与父进程的会话组和进层组脱离,由于会话对控制终端的独占性,进程同时与控制终端脱离(原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件,而不是控制终端了)。
注:要保证调用进程不是会话的领头进程只需要先调用fork()函数,然后在子进程中调用setsid()函数便可,因为会话领头进程肯定为会话中的第一个进程,当父、子进程在同一个会话中时,子进程肯定不会为会话的第一个进程。

创建守护进程步骤:
1、调用umask()函数设置文件模式创建屏蔽字,用于控制后面进程创建的文件权限,此处设置为0。
2、调用fork()函数,父进程退出,此处有两个目的,一个是:如果该守护进程作为一条普通的shell命令启动,那么父进程退出,使得shell认为该命令执行完成,从而可以继续接收用户的指令。另一个则是:保证子进程不是会话的领头进程,从而保证setsid()函数的正常调用。
3、调用setsid()函数创建一个会话。
4、将当前工作目录更改为根目录。
5、关闭不需要的文件描述符。
6、忽略SIGCHLD信号。

实现代码:
注意:下面的实现代码与上面的步骤有所不同,主要目的在于防止后期的误操作打开前面关闭了的和终端联系的文件描述符。

#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<signal.h>
#include<fcntl.h>
#include<stdlib.h>


void mydaemon(){
  int fd0;
  pid_t id;
  struct sigaction sa;
  //1、调用umask()函数设置文件模式创建屏蔽字的值
  umask(0);
  //2、调用fork()函数,父进程退出(exit)此处有两个目的,
  //一个是:如果该守护进程作为一条普通的shell命令启动,
  //那么父进程退出,使得shell认为该命令执行完成,从而可以
  //继续接收用户的指令。另一个则是:保证子进程不是会话的
  //领头进程,从而保证setsid()函数的正常调用。
  id = fork();
  if(id < 0){
    perror("fork1 error");
    return ;
  }else if(id > 0){
    exit(1);
  }else{
    //3、调用setsid()函数创建会话
    setsid();
    //4、忽略SIGCHLD信号,防止产生僵尸进程。
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if(sigaction(SIGCHLD,&sa,NULL) < 0){
      printf("sigaction error\n");
      return;
    }
    //5、再次调用fork()函数,父进程退出(exit),子进程作为
    //守护进程继续运行,防止后面误操作打开前面关闭的与终端 关联的文件描述符
    //因为只有会话领头进程才能打开终端设备,此时作为会话领头
    //进程的父进程已经退出
    id = fork();
    if(id < 0){
      printf("fork2 error!\n");
      return;
    }else if(id > 0){
      exit(2);
    }else{

      //6、将当前工作目录改为根目录
      if(chdir("/") < 0){
        printf("change child dir error\n");
        return;
      }
      //7、关闭不再需要的文件描述符或重定向到/dev/null
      close(0);
      fd0 = open("/dev/null",O_RDWR);
      dup2(fd0,1);
      dup2(fd0,2);
    }
  }
}


int main(){
  mydaemon();
  while(1){
    sleep(1);
  }
  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值