思考
代码中创建的会话,如何关联控制终端?
新会话关联控制终端的方法
会话首进程成功打开终端设备 (设备打开前处于空闲状态)
- 1、关闭标准输入输出和标准错误输出
- 2、将 stdin 关联到终端设备:STDIN_FILENO => 0
- 3、将 stdout 关联到终端设备:STDOUT_FILENO => 0
- 4、将 stderr 关联到终端设备:STDERR_FILENO => 0
一些相关推论
新会话关联控制终端后,会话中所有进程生命期与控制终端相关
只有会话首进程能够关联控制终端 (会话中的其他进程不行)
进程的标准输入输出与标准错误输出可以进程重定向
- 由描述符 0,1,2 决定重定向的目标位置 (按顺序打开设备)
- 控制终端与进程的标准输入输出以及标准错误输出无直接关系
一个大胆的想法
会话首进程会关联到伪终端从设备
代码示例
会话与终端深度实验
master.c
#define _XOPEN_SOURCE 600
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
char rx = 0;
int master = 0;
int c = 0;
master = posix_openpt(O_RDWR); // gnome-terminal
if( master > 0 )
{
grantpt(master);
unlockpt(master);
printf("Slave: %s\n", ptsname(master));
while( (c = read(master, &rx, 1)) == 1 )
{
if( rx != '\r' )
{
printf("%c", rx);
}
}
close(master);
}
else
{
printf("create pty error...\n");
}
return 0;
}
session.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
int pid = 0;
int i = 0;
if( (pid = fork()) > 0 )
{
printf("parrent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
printf("new: %d\n", pid);
exit(0);
}
else if( pid == 0 )
{
setsid();
sleep(200);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
i += open(argv[1], O_RDONLY); // 0 --> STDIN
i += open(argv[1], O_WRONLY); // 1 --> STDOUT
i += open(argv[1], O_RDWR); // 2 --> STDERR
printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
printf("i = %d\n", i);
}
else
{
printf("fork error...\n");
}
sleep(240);
return 0;
}
在 master.c 中,我们创建一个伪终端,读取伪终端主设备接收到的消息并打印出来
在 session.c 中,我们创建一个子进程并通过 setsid() 让子进程成为会话首进程,此时子进程的标准输入输出和标准错误输出连接到的是父进程的控制终端,标准输入对应文件描述符 0,标准输出对应文件描述符 1,标准错误对应文件描述符 2。第 25 - 27 行,我们将这三个文件描述符关闭,切断了原先的连接;第 29 - 31 行,我们将子进程关联到新的控制终端,并将子进程的标准输入输出和标准错误输出重定向到新的控制终端中去,这样输出的消息是送到新的控制终端中去,而不是送到父进程的控制终端中去
程序运行结果如下图所示:
第一个阶段,子进程通过 setsid() 成为会话首进程,然后 sleep 200s
此时,子进程的 PID == PGID == SID,说明子进程成功成为了会话首进程,但通过 TTY 和 TPGID 两个字段可以看出,此时子进程未关联任何终端
第二个阶段,子进程打开伪终端从设备,与伪终端相关联,并将子进程的标准输入输出和标准错误输出重定向到伪终端从设备中
此时 子进程的 TTY 字段为 pts/2,说明子进程已经成功关联到伪终端 pts/2 了,然后可以看到 master.out 中接收到了子进程要打印出的消息,这是因为子进程重定向标准输入输出和标准错误输出到伪终端从设备中,伪终端从设备接收到消息后会发送给伪终端主设备,master.out 中会将伪终端主设备接收到的消息打印出来
第三个阶段,子进程处于 240s 的睡眠中,我们手动关闭父进程的控制终端,另起一个终端来查看子进程的相关信息
子进程依旧在运行,这是因为子进程已经关联到伪终端 pts/2 中去了,只要 pts/2 没有关闭,则子进程依旧会继续运行
最后,我们将 master.out 程序退出执行,则伪终端 pts/2 也会被关闭,这时我们查看子进程的相关信息
可以看到子进程运行结束了,这是因为子进程关联到了伪终端 pts/2,pts/2 被关闭的话,则这个伪终端所关联的所有进程都会结束运行
什么是守护进程 (Daemon) ?
守护进程是系统中执行任务的后台进程
- 不与任何终端相关联 (不接收终端相关的信号)
- 生命周期长,一旦启动,正常情况下不会终止 (直到系统退出)
- Linux 大多服务器使用守护进程实现 (守护进程名以后缀 d 结尾)
守护进程的创建步骤
1、通过 fork() 创建新进程,成功后,父进程退出
2、子进程通过 setsid() 创建新会话
3、子进程通过 fork() 创建孙进程 (肯定不是会话首进程)
4、孙进程修改模式 umask(),改变工作目录为 "/"
5、关闭标准输入输出和标准错误输出
6、重定向标准输入输出和标准错误输出 ("/dev/null")
守护进程关键点分析
父进程创建子进程是为了创建新会话
子进程创建孙进程是为了避免产生控制进程
孙进程不是会话首进程,所以不能关联终端
重定向操作可以避开奇怪的进程输出行为
创建守护进程
first-d.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char* argv[])
{
int pid = 0;
int i = 0;
if( (pid = fork()) > 0 ) // 1
{
printf("parrent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
printf("child: %d\n", pid);
exit(0);
}
else if( pid == 0 )
{
setsid(); // 2
if( (pid = fork()) > 0 ) // 3
{
printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
printf("groundson: %d\n", pid);
exit(0);
}
if( pid == 0 )
{
// 4
umask(0);
chdir("/");
// 5
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 6
i += open("/dev/null", O_RDONLY); // 0 --> STDIN
i += open("/home/book/dt_linux/10/d.log", O_WRONLY); // 1 --> STDOUT
i += open("/dev/null", O_RDWR); // 2 --> STDERR
printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
while( 1 )
{
// do something
printf("i = %d\n", i++);
sleep(1);
fflush(stdout);
}
}
}
else
{
printf("fork error...\n");
}
return 0;
}
第 44 行,我们将守护进程的标准输出重定向到一个文件中,是因为大多数情况下,我们需要查看守护进程的一些日志,所以我们需要将标准输出重定向到文件中,不需要日志的话,可以将标准输出重定向到 "/dev/null"
第 54 行,我们手动调用 fflush() 是因为,默认写文件是全缓冲,我们写的数据首先会放到一段大小的缓冲区中去,只有这片缓冲区填满时,才会进行 IO 操作,将填满的缓冲区数据写入到磁盘中去,这样是可以提高效率;手动调用 fflush() 后,不管缓冲区有多少数据,都会写到磁盘中去
程序运行结果如下图所示: