终端
终端是用户与 Linux 内核之间进行交互的桥梁,用户通过终端可以向系统发出命令并查看系统的输出。
当Linux中,控制终端(Controlling Terminal)是一个终端设备,通常与Shell 进程相关联。当用户在终端上登录并启动一个 Shell 进程时,该终端会成为控制终端。这使得用户可以通过终端与 Shell 进行交互,管理作业以及控制进程的执行。
终端设备通常位于 /dev/ 目录下:
终端设备文件:/dev/tty[0-9],例如 /dev/tty0、/dev/tty1 等。
控制台设备:/dev/console
串行端口设备:/dev/ttyS[0-9],例如 /dev/ttyS0、/dev/ttyS1等。
伪终端设备(Pseudo Terminal):/dev/pts/[0-9],例如 /dev/pts/0、/dev/pts/1 等。
用户可以通过这些设备文件来与终端进行交互,包括登录控制终端、管理终端会话以及执行命令操作。
默认情况下,每个进程的标准输入、标准输出和标准错误输出都指向控制终端。标准输入读取用户的键盘输入,标准输出、标准错误将数据显示在显示器上。控制终端被保存在进程PCB中,当通过fork()函数创建子进程,对应的控制终端也会拷贝至子进程。
Linux启动流程如下:
fork+exec fork+exec 账号密码+fork
init------------>getty------------->login---------------->shell
为什么需要终端,直接从键盘和显示器中读取数据不行吗?终端实际上实现了下列功能:
回显:可以让输入字符自动被回显到终端的输出上,如果没有该功能,则会极大影响用户体验。
行编辑:用户输入的字符不是立马送到应用程序,而是在换行以后才能被读取到,提高容错性,当输错时,可以使用退格键重新编辑。
功能键:允许定义功能键。比如最常用的Ctrl+C,杀死前台进程,就是由终端来触发的。终端检测到Ctrl+C输入,会向前台进程组发送SIGTERM信号。
回车换行的转换:定义输入输出如何映射回车换行符。
输入输出流向控制:只有前台进程组能够从终端中读数据、而前台后台程序都能向终端写数据。当后台进程读取终端时,终端会向其发送 SIGTTIN 信号,导致进程被挂起。
获取终端名称-ttyname函数
#include
char *ttyname(int fd);
参数说明
fd:一个与终端相关联的文件描述符。
返回值
如果函数执行成功,返回指向包含终端设备名称的字符串的指针。
如果函数执行失败,函数返回 NULL。
通常情况下,我们可以将标准输入、标准输出或标准错误的文件描述符传递给 ttyname() 函数,以获取与终端设备相关联的名称。
注意
ttyname() 函数的返回值是指向静态分配的缓冲区的指针,因此不应该尝试释放或修改这个指针指向的内存。
会话
会话是一个或多个进程组的集合。用户登录是一个会话的开始。登录之后,用户会得到一个跟用户使用的终端相连的进程,这个进程被称作是这个会话的leader,会话的ID就等于该进程的pid。由该进程fork出来的子进程都是这个会话的成员。
有如下特点:
一个会话只能有一个控制终端。
建立与控制终端连接的会话首进程被称为控制进程。
一个会话中只会有一个前台进程组,其他都是后台进程组。
终端退出将导致会话被关闭,如果一个会话关闭,则该会话下的所有进程组都会收到 SIGHUP 信号。
建立和改变终端与会话的联系只能由会话领导者(session leader)来进行。
查看会话ID-getsid()函数
getsid()函数用于获取指定进程的会话ID(SID)。该函数定义如下:
#include
pid_t getsid(pid_t pid);
参数说明
pid:进程ID,如果指定为0,表示当前进程。
返回值
如果函数执行成功,函数返回指定进程的会话ID。
如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。
设置会话ID-setsid()函数
setsid()函数用于创建一个新的会话(session)并将调用进程设置为该会话的领头进程(session leader)。该函数定义如下:
#include
pid_t setsid(void);
返回值
如果函数执行成功,函数返回新会话的会话ID。
如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。
通过调用 setsid() 函数,进程可以脱离其当前的控制终端,成为一个新的会话的领头进程,这样就可以独立于原来的终端进行运行。
哪些进程无法成为领头进程:
当前进程已经是会话的领头进程。
当前进程是进程组中的组长进程。
想要成功设置会话ID,可以先调用fork,父进程终止,子进程调用setsid。
守护进程(Daemon)
守护进程是一种在后台运行、独立于控制终端的进程。通常在系统启动时启动,并在系统运行期间持续运行,执行特定的任务或提供特定的服务。
特点:
独立于控制终端:守护进程不受控制终端的影响,通常在后台运行。
无法终止:守护进程通常会捕获和忽略终端信号,以防止意外终止。
不输出到控制台:守护进程通常将输出写入日志文件,而非终端。
创建守护进程的步骤:
创建子进程,父进程退出。
调用 setsid() 函数,在子进程中创建新会话。
调用 chdir()函数,更改当前工作目录。进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。
重设文件权限掩码,为了不受终端信号影响,一般会调用 umask() 函数来设置文件掩码。
关闭文件描述符,关闭从父进程继承而来的文件描述符。
重定向标准输入、输出、错误:将标准输入、输出、错误重定向到 /dev/null 或日志文件。
执行守护进程,在完成上述步骤之后,守护进程可执行自己的任务或服务。
示例:
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8
9 void init_daemon()
10 {
11 pid_t pid = fork();
12 if(pid == -1)
13 {
14 perror("fork");
15 exit(-1);
16 }
17 else if(pid > 0)
18 {
19 exit(0);
20 }
21 setsid();
22 if(chdir("/") < 0)
23 {
24 perror("chdir");
25 exit(-1);
26 }
27
28 umask(0);
29 for (int i = 0; i < NOFILE; ++i)
30 {
31 close(i);
32 }
33 }
34
35 int main(int argc, char** argv)
36 {
37 init_daemon();
38 while (1)
39 {
40 FILE *fp = fopen("/my_daemonLog", "a+");
41 if(fp == NULL)
42 {
43 perror("fopen");
44 exit(-1);
45 }
46 time_t t;
47 time(&t);
48 fprintf(fp, "current time is : %s\n", ctime(&t));
49 fclose(fp);
50 sleep(1);
51 }
52 return 0;
53 }
编译运行:
$ gcc main.c
$ sudo ./a.out
根目录下会产生 my_daemonLog 文件,可以通过 cat 命令查看:
$ cat /my_daemonLog
current time is : Mon Mar 4 14:55:37 2024
current time is : Mon Mar 4 14:55:38 2024
如果要删除后台运行的进程:
$ sudo killall ./a.out
