更多linux知识:inux目录索引
1. 进程间关系
1.0 进程组
进程组是一个或多个进程的集合。
每个进程除了有一个进程ID之外,还属于一个进程组。
这些进程通常与同一作业相关联,可以接受来自同一终端的各种信号。
每个进程组有唯一进程组ID,每个进程组可以有一个组长进程,其进程ID和进程组ID一致。
组长进程可以创建一个进程组,创建该组中进程,然后终止。
只要某个进程组中有一个进程存在,该进程组就存在,与组长进程是否终止无关。
从进程组的创建到最后一个进程离开的时间去称为进程组的生命周期
例子:
1.1 作业
1. 进程组在某种意义上可称之为作业,在我们所用的shell当中,shell对于前后台的控制的是作业(或进程组);
2. shell对作业的控制,可以运行一个前台作业和多个后台作业;
3. 在前台新起作业,shell是无法运行,因为他被提到了后台。但是如果前台进程退出,shell就又被提到了前台,所以可以继续接受用户输入。
注意:进程组和作业的唯一区别-----进程组的进程创建出来的子进程还是数属于进程组。但作业中的进程创建出来的子进程不属于这个作业
一个现象:
先看下面代码
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
return 1;
}else if(pid == 0){//子进程
while(1)
{
printf("child(%d)# I am running!\n",getpid());
sleep(1);
}
}else{
int i = 5;
while(i){//父进程运行5秒退出
printf("parent(%d)# I am going to dead... %d\n",getppid(),i--);
sleep(1);
}
}
return 0;
}
分析: fork创建出子进程,默认在前台运行,此时我们输入ls 命令不会有起任何作用;5秒过后,父进程退出,此时ls 命令又可以显示出文件了;这是因为当父进程退出后,子进程被1号进程托管,自动成为后台作业,此时shell又被提到前台,此时就可以正常运行
1.2 会话
会话(Session)是一个或多个进程组的集合。
一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。
建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。
所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。
几个例子
//创建后台作业
[so@localhost 守护进程]$ sleep 100 | sleep 200 | sleep 300 &
[1] 13391
[so@localhost 守护进程]$ sleep | 400 | sleep 500 &
[2] 13394
//查看后台作业
[so@localhost 守护进程]$ jobs
[1]- Running sleep 100 | sleep 200 | sleep 300 &
[2]+ Running sleep | 400 | sleep 500 &
//将后台作业提到前台
[so@localhost 守护进程]$ fg 1
sleep 100 | sleep 200 | sleep 300
^Z //使用ctrl+z 将其放在后台
[1]+ Stopped sleep 100 | sleep 200 | sleep 300
//此时可以看到此进程是暂停状态
[so@localhost 守护进程]$ jobs
[1]+ Stopped sleep 100 | sleep 200 | sleep 300
[2]- Running sleep | 400 | sleep 500 &
//使用bg可以让进程在后台运行
[so@localhost 守护进程]$ bg
[1]+ sleep 100 | sleep 200 | sleep 300 &
[so@localhost 守护进程]$ jobs
[1]- Running sleep 100 | sleep 200 | sleep 300 &
[2]+ Running sleep | 400 | sleep 500 &
//将其替代前台,并将其杀死
[so@localhost 守护进程]$ fg 1
sleep 100 | sleep 200 | sleep 300
^C
[so@localhost 守护进程]$ fg 2
sleep | 400 | sleep 500
^C
2. 守护进程
2.0 什么是守护进程
守护进程也叫精灵进程,是在后台运行的一种特殊进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件;
2.1守护进程的特点
- 是系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互
- 除守护进程外其他进程都是在用户登录或运行程序时创建,在系统注销时终止,但系统服务程序不受用户登录注销的影响(原因:它跟终端没有关系)
- 自成会话,自成进程组
补充:我们可以用ps axj命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用 户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数 j 表示列出与作业控制相关的信息。
linux下查看守护进程
凡是TPGID⼀一栏写着-1的都是没有控制终端的进程,也就是守护进程。
在COMMAND⼀一列⽤用[]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。
init进程我们已经很熟悉了,udevd负责维护/dev目录下的设备文件,acpid负责电源管理,syslogd负责维护/var/log下的日志文件可以看出,守护进程通常采用以d结尾的名字,表示Daemon。`
2.2 守护进程存在的原因
控制终端因为某些原因会发送一些信号,接受到信号的进程去执行这些信号的默认处理动作会导致进程退出。这就使得进程不能正常的处理某些任务,所以就需要像守护进程这样接受不到信号的进程。让进程独立与控制终端,执行某些任务或处理某些事件。
2.3 创建守护进程的几个重要部分
1. fork() 两次
第一次fork是为了调用setsid,因为调用setsid函数的进程一定不是进程组的组长,我们让父进程创建出子进程,然后退出,此时子进程一定不是进程组的组长
2.调用setsid
这是创建守护进程最重要的一步,调用setsid创建一个会话,让调用此函数的进程成为话首进程
setsid函数
#include <unistd.h>
id_t setsid(void);
// 该函数调用成功时返回新创建的Session的id(其实也就是当前进程的id),出错返回-1。
// 如果调用这个函数的进程是进程组的组长,那么 setsid() 就会出错,返回 -1。
// 要保证当前进程不是进程组的组长也很容易,只要先fork再调用setsid就行了。
// fork创建的子进程和父进程在同一个进程组中,进程组的组长必然是该组的第一个进程,
// 所以子进程不可能是该组的第一个进程,也就不可能会是组长,在子进程中调用setsid就不会有问题了。
成功调用该函数的结果是:
当前进程成为会话首进程(控制进程),当前进程的 id 就是会话的id。
创建一个新的进程组,当前进程成为进程组的组长,当前进程的 id 就是进程组的 id。
如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。
所谓失去控制终端是指,原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件而不是控制终端了。
3.将当前工作目录改成根目录
守护进程会保护当前目录下的文件或目录不被删除或卸载
那么如果某一时刻需要对该目录下的 文件进行删除操作,就无执行了
为什么不能删除或卸载守护进程当前目下的文件或目录?
因为避免出现因为删除或卸载当前目录下的文件或目录而导致守护进程运行失败
代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
void mydaemon(void)
{
int i;
int fd0;
pid_t pid;
struct sigaction sa;
umask(0);//将文件模式创建屏蔽字为0,此时你想创建什么权限就是什么权限,因为子进程辉复制父进程的文件权限屏蔽字,此时你创建的文件权限依然没有某一写权限
//2. 调用fork,创建子进程,让父进程退出,此时保证子进程不是一个进程组的组长进程
if((pid = fork()) < 0){
perror("fork");
}else if(pid > 0){//创建出子进程,父进程退出
exit(0);
}
setsid();// 1.子进程调用setsid创建一个新会话
//以下是维护守护进程
sa.sa_handler = SIG_IGN;//忽略子进程的信号
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if(sigaction(SIGCHLD,&sa, NULL) < 0){//注册子进程退出忽略信号
return;
}
//再次fork,创建出孙子进程,让孙子进程去维护这个守护进程,此时孙子进程一定不是话首进程,并且不会受控制终端的影响,原先创建的进程是进程组组长,话首进程,仍然有能力打开控制终端,因此创建算子进程避免当前进程组组长误打开控制终端
if((pid = fork() < 0)){
perror("fork");
return;
}
else if(pid != 0){
exit(0);
}
//更改当前目录,因为守护进程会保护当前目录下的文件和目录不会被删除;这是因为当你删除了某一文件,就可能导致守护进程运行失败
if(chdir("/") < 0){
printf("chdir dir error\n");
return;
}
close(0);//将不用的文件描述符进行关闭,因为守护进程没有终端,不支持从标准输入读取和输出
fd0 = open("/dev/null", O_RDWR);
dup2(fd0,1);
dup2(fd0,2);
}
int main()
{
daemon(0,0);
while(1){
sleep(1);
}
return 0;
}
4. 系统提供的daemon函数
#include <unistd.h>
int daemon(int nochdir, int noclose);
// 参数
// 当 nochdir为 0 时,当前目录变为根目录,否则不变;
// 当 noclose为 0 时,标准输入、标准输出和错误输出重定向为/dev/null,也就是不输出任何信息,否则照样输出。
// 返回值:
// deamon()调用了fork()
// 如果fork成功,那么父进程就调用_exit(2)退出,
// 所以看到的错误信息 全部是子进程产生的。
// 如果成功函数返回0,否则返回-1并设置errno。
例子:
#include <stdio.h>
#include <unistd.h>
int main()
{
daemon(0, 0);
while(1){
sleep(1);
}
return 0;
}