1、终端(Terminal)
终端是物理设备,只用于输入输出,本身没有强大的计算能力。在计算资源紧张的时代,人们想共享一台计算机,可以通过终端连接到计算机上,将指令输入终端,终端传送给计算机,计算机完成指令后,将输出传送给终端,终端将结果显示给用户,所以有显示器和键盘能够通过串口连接到计算机的设备就叫终端。
2、控制台(Console)
控制台也是物理设备,也用于输入输出,但它直接连接在计算机上,是计算机系统的一部分,所以直接连接在电脑上的键盘和显示器就叫做控制台。计算机启动的时候,所有的信息都会显示到控制台上,而不会显示到终端上。简单的说,能直接显示系统消息的那个终端称为控制台,其他的则称为终端。
3、控制台与终端的区别
终端是通过串口连接上的,不是计算机本身就有的设备,而控制台是计算机本身就有的设备,一个计算机只有一个控制台。也就是说,控制台是计算机的基本设备,而终端是附加设备。
简言之,终端为主机提供了人机接口,每个人都通过终端使用主机的资源。终端有字符终端和图形终端两种。一台主机可以连很多终端。
控制台是一种特殊的人机接口(控制台也是终端的一种), 是人控制主机的第一人机接口,而主机对于控制台的信任度高于其他终端。
4、TTY
TTY 是 Teletype 或 Teletypewriter 的缩写,原来是指电传打字机,后来这种设备逐渐键盘和显示器取代。不管是电传打字机还是键盘显示器,都是作为计算机的终端设备存在的,所以 TTY 也泛指计算机的终端设备。
5、虚拟控制台(Virtual Console)与虚拟终端(Virtual Terminal)
虚拟控制台和虚拟终端是一样的:如果我们只有一台终端,这是我们与计算机之间的用户接口,假如有一天,我们想拥有多个用户接口,那么,一方面我们可以增加终端数目,另一方面,还可以在同一台终端上虚拟出多个终端,它们之间互相不影响,至少看起来互相不影响。这些终端就是虚拟终端。Linux 默认所有虚拟终端都是控制台,都能显示系统消息,所以我们在平时的使用中压根就不区分 Linux 中的终端与控制台。
CTRL+ALT+fn
切换虚拟终端,例如: 我们按下 Ctrl+Alt+F1
时,会进入 tty1。6、模拟终端器:
如今,终端不再是一个需要通过 UART 连接到计算机上物理设备。终端成为内核的一个模块,它可以直接向 TTY 驱动发送字符,并从 TTY 驱动读取响应然后打印到屏幕上。也就是说,用内核模块模拟物理终端设备,因此被称为终端模拟器,所以终端是一种物理设备,而终端模拟器,是一个程序,这些程序用来模拟物理终端。
7、shell
和之前说的几个概念截然不同,之前的几个概念都是与计算机的输入输出相关的,而shell是和内核相关的。内核为上层的应用提供了很多服务,shell在内核的上层,在应用程序的下层。例如,你写了一个 hello world 程序,你并不用显式地创建一个进程来运行你的程序,你把写好的程序交给 shell 就行了,由 shell 负责为你的程序创建进程。
我们在终端模拟器中输入命令时,终端模拟器本身并不解释执行这些命令,它只负责输入输出,真正解释执行这些命令的,是 shell。我们平时使用的 sh, bash, csh 是shell 的不实现。
由于在 linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端(Controlling Terminal),当控制终端被关闭时,相应的进程都会自动关闭:用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell 进程的控制终端,进程中,控制终端是保存在 PCB 的信息中,而 fork 会复制 PCB 中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。
默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
[[10_ Linux 进程通讯#四、进程间通讯之信号|信号]]一节中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl+C 表示 SIGINT,Ctrl+\ 表示 SIGQUIT。
Linux 中所讲的终端,除非特别说明,否则一般都是指控制终端。
(1)tty 命令可以查看当前终端的名字。
yxm@192:~$ tty
/dev/pts/1
(2)也可以通过函数查看终端的名字,函数说明如下:
#include
char *ttyname(int fd);
功能:由文件描述符查出对应的文件名
参数:
fd:文件描述符
返回值:
成功:终端名
失败:NULL
下面我们借助 ttyname 函数,通过实验看一下各种不同的终端所对应的设备文件名:
#include
#include
int main() {
printf("fd 0: %s\n", ttyname(0));
printf("fd 1: %s\n", ttyname(1));
printf("fd 2: %s\n", ttyname(2));
return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
fd 0: /dev/pts/0
fd 1: /dev/pts/0
fd 2: /dev/pts/0
(3)ps -aux 中 TTY 字段:
(4)一般情况下,当前终端也是一个进程,在当前终端开启的程序,它的父进程一般也是当前终端。
进程组,也称之为作业。BSD 于 1980 年前后向 Unix 中增加的一个新特性。代表一个或多个进程的集合。所以进行组由一个或多个共享同一进程组的进程组成。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
每个进程都属于一个进程组。当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组 ID 为第一个进程的 ID,进程组的第一个进程称为组长进程(也叫首进程)。所以,对于组长进程标识而言,其进程组 ID 为其本身进程 ID。进程组的标识符为 PGID:
进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程离开(终止或转移到另一个进程组)组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员:只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组的概念在 [[09_Linux 进程基础#4、进程回收|waitpid 函数]] 和 [[09_Linux 进程基础#2、进程控制命令|kill ]]中都曾使用到。例如可以使用 kill -SIGKILL -进程组ID (负的)来将整个进程组内的进程全部杀死。
前台后台进程
Linux是一种用户控制的多作业操作系统, 系统允许多个系统用户同时提交作业,而一个系统用户可以用多个 shell 登录,每个系统用户可以用一个 shell 提交多个作业。作业有两种运行方式:前台运行和后台运行。
前台进程与后台进程特点如下:
一个进程可以为自己或子进程设置进程组 ID。
#include
pid_t getpgrp(void); /* POSIX.1 version */
功能:获取当前进程的进程组ID
参数:无
返回值:总是返回调用者的进程组ID
pid_t getpgid(pid_t pid);
功能:获取指定进程的进程组ID
参数:
pid:进程号,如果pid = 0,那么该函数作用和getpgrp一样
返回值:
成功:进程组ID
失败:-1
int setpgid(pid_t pid, pid_t pgid);
功能:
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
参数:
将参1对应的进程,加入参2对应的进程组中
返回值:
成功:0
失败:-1
会话是一个或多个进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。 一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
进程组和会话:进程组和会话在进程之间形成了一种两级层次关系,进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。示例:
在某个终端上执行上面两个命令:find / 2 > /dev/null | wc -l &
和 sort < longlist | uniq -c
。
find / 2 > /dev/null | wc -l &
命令后,创建一个新的进程组,该进程组 id 是 658,进程组中包括 find 进程和 wc 进程,它们的父进程 id 是 400,会话 id 也是 400,且该进程组以后台方式运行,所以是后台进程。sort < longlist | uniq -c
命令后,创建一个新的进程组,该进程组 id 是 660,进程组中包括 sort 进程和 uniq 进程,它们的父进程和会话 id 也是 400,默认情况下都是运行在前台,前台进程组才享有控制终端的操作权利。#include
pid_t getsid(pid_t pid);
功能:获取进程所属的会话ID
参数:
pid:进程号,pid为0表示查看当前进程session ID
返回值:
成功:返回调用进程的会话ID
失败:-1
#include
pid_t setsid(void);
功能:
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。调用了setsid函数的进程,既是新的会长,也是新的组长。
参数:无
返回值:
成功:返回调用进程的会话ID
失败:-1
#include
#include
#include
#include
int main(void) {
pid pid = -1;
pid = getsid(0);
if (-1 == pid) {
perror("getsid");
return 1;
}
printf("sid:%d\n", pid);
// 创建一个新的回话
pid = setsid(); // 创建会失败,因为:调用进程不能是进程组组长,该进程变成新会话首进程
if (-1 == pid) {
perror("setsid");
return 1;
}
return 0;
}
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。**文件名一般采用以 d 结尾的名字。**守护进程具备下列特征:
守护进程是一种特殊的后台进程,两者并不完全等价(前置知识:[[11_Linux 终端、进程组、会话、守护进程#二、进程组概念|前台与后台进程]]):
nohup command &
格式运行才能避免影响);nohup command &
格式运行才能避免影响。守护进程的创建步骤如下:
setsid()
函数(调用进程不能是进程组组长)。setsid()
函数创建新的会话umask()
函数chdir()
函数dup2()
使所有这些描述符指向这个设备:关闭文件描述符0、1、2之后,如果守护进程依旧有可能有想这三个文件描述符输出数据,但是文件描述符已经关闭,此时就可能出错,所以要将这三个文件描述符输出的数据重定向到 /dev/null 中,/dev/null 中的数据都会被操作系统自动弃掉。写一个守护进程, 每隔 2s 获取一次系统时间, 将这个时间写入到磁盘文件:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void work(int num) {
// 捕捉到信号之后,获取系统时间,写入磁盘文件
time_t tm = time(NULL);
struct tm * loc = localtime(&tm);
// char buf[1024];
// sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
// ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
// printf("%s\n", buf);
char * str = asctime(loc);
int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
write(fd ,str, strlen(str));
close(fd);
}
int main() {
// 1.创建子进程,退出父进程
pid_t pid = fork();
if(pid > 0) {
exit(0);
}
// 2.将子进程重新创建一个会话
setsid();
// 3.设置掩码
umask(022);
// 4.更改工作目录
chdir("/home/nowcoder/");
// 5. 关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 重定向如果不关闭关闭,就会输出到终端,但是终端提示符依旧可以出现并操作(新的会话,脱离了终端)。重定向关闭,就不会输出到终端。
// 6.业务逻辑
// 捕捉定时信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
// 创建定时器
setitimer(ITIMER_REAL, &val, NULL);
// 不让进程结束
while(1) {
sleep(10);
}
return 0;
}
#include
int daemon(int nochdir, int noclose);
功能:创建一个守护进程
参数:
nochdir:=0 将当前目录更改至“/”
noclose:=0 将标准输入、标准输出、标准错误重定向至“/dev/null”
返回值:
成功:0
失败:-1
参考文章