终端
终端(Terminal)也称终端设备,是计算机网络中处于网络最外围的设备。
-
Alt + Ctrl + F1、F2、F3、F4、F5、F6 字符终端 pts(pseudo terminal slave) 指伪终端
-
Alt + F7 图形终端
-
SSH、Telnet… 网络终端
终端的启动流程
文件与 I/O 中讲过,每个进程都可以通过一个特殊的设备文件 /dev/tty 访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty 提供了一个通用的接口,一个进程要访问它的控制终端既可以通过 /dev/tty 也可以通过该终端设备所对应的设备文件来访问。ttyname 函数可以由可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。
简单来说,一个 Linux 系统启动,大致经历如下的步骤:
init -> fork -> exec -> getty -> 用户输入账号 -> login -> 输入密码 -> exec -> bash
硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像**一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘按下 Ctrl+z, 对应的字符并不会被用户程序的 read 读到,而是被线路规程截获,解释成 SIGTSTP 信号发给前台进程,通常会使该进程停止。**线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。
ttyname 函数
由文件描述符查出对应的文件名
- char *ttyname(int fd);
- 成功: 终端名; 失败: NULL,设置 errno
下面我们借助 ttypname 函数,实验看一下各种不同终端所对应的设备文件名
#include <cstdio>
#include <unistd.h>
using namespace std;
int main(void) {
printf("fd 0:%s\n", ttyname(0));
printf("fd 1:%s\n", ttyname(1));
printf("fd 2:%s\n", ttyname(2));
return 0;
}
网络终端
网络终端和图形终端窗口的数目却是不受限制的,这是通过伪终端 (Pseudo TTY) 实现的。一套伪终端由一个主设备 (PTY Master) 和一个 从设备 (PTY Slave) 组成。
进程组
概念和特性:
**进程组,也称之为作业。代表一个或多个进程的集合。每个进程都属于一个进程组。**在 waitpid 函数和 kill 函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
当父进程,创建子进程的时候,默认子进程与父进程属于同一个进程组。**进程组 ID == 第一个进程 ID(组长进程)。**所以,组成进程标识:其进程组 ID == 其进程 ID
可以使用 kill -SIGKILL -进程组 ID(负的)来将整个进程组内的进程全部杀死。
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程中有一个进程存在,进程组就存在,与组长进程是否终止无关。
**进程组生存期:**进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
一个进程可以为自己或子进程设置进程组 ID
进程组操作函数:
getpgrp 函数:
获取当前进程的进程组 ID
- pid_t getpgrp(void);
- 总是返回调用者的进程组 ID;
getpgid 函数:
获取指定进程的进程组 ID
- pid_t getpgid(pid_t pid);
- 成功: 0; 失败: -1, 设置 errno
如果 pid = 0,那么该函数作用和 getpgrp 一样。
练习:查看进程对应的进程组 ID
setpgid 函数:
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
- int setpgid(pid_t pid, pid_t pgid);
- 成功:0; 失败:-1,设置errno
将参 1 对应的进程,加入参 2 对应的进程组中。
注意:
-
如改变子进程为新的组,应 fork 后,exec 前。
-
权级问题。非 root 进程只能改变自己创建的子进程,或有权限操作的进程。
练习:修改子进程的进程组 ID.
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
using namespace std;
int main(void) {
pid_t pid;
pid = fork();
if(pid == -1) {
perror("fork error");
exit(1);
} else if(pid == 0) {
printf("------son pid = %d, parent pid = %d, group pid = %d\n", getpid(), getppid(), getpgrp());
sleep(7);
printf("------son pid = %d, parent pid = %d, group pid = %d\n", getpid(), getppid(), getpgrp());
} else {
sleep(1);
setpgid(pid, pid);
sleep(13);
printf("\n");
printf("------parent pid = %d, pparent pid = %d, group pid = %d\n", getpid(), getppid(), getpgid(getpid()));
}
return 0;
}
会话
创建会话
创建一个会话需要注意以下 6 个注意事项:
-
调用进程不能是进程组组长,该进程变成新会话首进程(session header)
-
该进程成为一个新进程组的组长进程。
-
需有 root 权限(ubuntu 不需要)
-
新会话丢弃原有的控制终端,该会话没有控制终端
-
该调用进程是组长进程,则出错返回
-
建立新会话时,先调用 fork,父进程终止,子进程调用 setsid
getsid 函数
获取进程所属的会话 ID
-
pid_t getsid(pid_t pid);
- 成功: 返回调用进程的会话 ID; 失败:-1, 设置errno
-
pid 为 0 表示查看当前进程 session ID
-
ps ajx 命令查看系统中的进程。参数 a 表示不仅列当前用户的进程,也列出所有其他用户的进程,参数 x 表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数 j 表示列出与作业控制相关的信息。
-
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
setsid 函数
创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID。
-
pid_t setsid(void);
- 成功:返回调用进程的会话 ID; 失败:-1, 设置 errno
-
调用了 setsid 函数的进程,既是新的会长,也是新的组长。
练习:fork 一个子进程,并使其创建一个新会话。查看进程组 ID、会话 ID 前后变化。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
using namespace std;
int main(void) {
pid_t pid;
pid = fork();
if(pid == -1) {
perror("fork error");
exit(1);
} else if(pid == 0) {
printf("----i is son, pid = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgid(0), getsid(0));
sleep(3);
setsid();
printf("Changed:\n");
printf("----i is son, pid = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgid(0), getsid(0));
exit(0);
}
return 0;
}
守护进程
Daemon(精灵)进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或者等待处理某些发生的事件。一般采用以 d 结尾。
/etc/passwd 可以查看许多守护进程。
Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp 服务器;nfs 服务器等。
创建守护进程,最关键的一步是调用 setsid 函数创建一个新的 session, 并成为 session Leader.
创建守护进程模型
-
创建子进程,父进程退出
- 所有工作在子进程中进行形式上脱离了控制终端
-
在子进程中创建新会话
- setsid()函数
- 使子进程完全独立出来,脱离控制
-
改变当前目录为根目录
- chdir 函数
- 防止占用可卸载的文件系统//防止程序正在运行,目录突然消失
- 也可以换成其他路径
-
重设文件权限掩码
- umask()函数
- 防止继承的文件创建屏蔽字拒绝某些权限
- 增加守护进程灵活性
-
关闭文件描述符//将 0/1/2重定向 /dev/null dup2()
- 继承的打开文件不会用到,浪费系统资源,无法卸载。
-
开始执行守护进程核心工作
-
守护进程退出处理程序模型
如何重启顺便打开守护进程:修改配置文件 .bashrc
练习:编写守护进程每隔两秒把当前系统的时间写入文件当中。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
using namespace std;
int main(void) {
pid_t pid;
pid = fork();
if(pid == -1) {
perror("fork error");
exit(1);
} else if(pid > 0) {
return 0;
}
setsid();
int ret = chdir("/home/ljf");
if(ret == -1) {
perror("chdir error");
exit(1);
}
umask(0002);//创建文件的权限会 与 上取反的 002
close(STDIN_FILENO);
open("/dev/null", O_RDWR);
dup2(0, STDOUT_FILENO);
dup2(0, STDERR_FILENO);
while(1) {
sleep(2);
pid_t pid = fork();
if(pid == 0) {
int fd = open("/home/ljf/date.out", O_RDWR|O_CREAT, 0644);
lseek(fd, 0, SEEK_END);
dup2(fd, STDOUT_FILENO);
execlp("date", "date", NULL);
}
wait(NULL);
}
return 0;
}