前提
在Linux的串口驱动开发中,通常都是以TTY框架作为其驱动开发框架。
使用 TTY 框架开发串口驱动,可以让串口设备具备标准的终端特性,并能通过 /dev/ttyX 这样的设备文件访问。关于终端的详细解释,请参考我的另一篇博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145431655
由于咱们的串口设备的驱动是基于TTY框架开发的,那么咱们打开的一个串口设备,同样也能同时作为串口终端来使用。这一点其实就是咱们本博文中要讲的问题的来源。
所以,看下面的内容前,你要谨记,本文下面内容的前提是假设我们的串口驱动是基于TTY的驱动框架开发的。
我的提问
提问:假设有一个ELF程序A,它在串口终端/dev/ttyS0中被打开运行,那么程序A的进程是不是就是以串口终端/dev/ttyS0作为自己的控制终端?
回答:
是的,如果一个 ELF 程序 A 在 串口终端 /dev/ttyS0
中被打开并运行,那么它的进程默认会把 /dev/ttyS0
作为自己的控制终端(Controlling Terminal),除非该进程在运行时主动关闭或更改控制终端。
对提问的回答
1. 进程的控制终端规则
在 Linux 中,进程的控制终端(Controlling Terminal)是指进程组(Process Group)与终端设备之间的绑定关系。通常:
-
登录 Shell(如
bash
)的控制终端- 当你使用
getty
登录 Linux 并进入 Shell(如/bin/sh
或/bin/bash
),这个 Shell 进程会把 当前终端(TTY) 设为自己的控制终端。 - 如果你是在
/dev/ttyS0
上登录的,Shell 进程的控制终端就是/dev/ttyS0
。
- 当你使用
-
启动程序 A 的控制终端
- 如果你从 Shell 运行 ELF 程序 A,例如:
./A
- A 进程会继承 Shell 的控制终端,即
/dev/ttyS0
。 - 这意味着:
A
进程可以从/dev/ttyS0
读取标准输入(stdin
)。A
进程的标准输出(stdout
)和标准错误输出(stderr
)都会默认写入/dev/ttyS0
。A
进程可能会受到终端信号影响,例如:- 按
Ctrl+C
会发送SIGINT
,终止A
。 - 按
Ctrl+Z
会发送SIGTSTP
,暂停A
。
- 按
- A 进程会继承 Shell 的控制终端,即
- 如果你从 Shell 运行 ELF 程序 A,例如:
-
如何验证?
- 运行
A
后,在另一个 Shell 中执行:ps -o pid,pgid,sid,tpgid,tty,comm -p $(pgrep A)
tty
字段应该显示ttyS0
,表明该进程的控制终端是/dev/ttyS0
。
- 运行
2. 影响进程与进程的控制终端之间的关系与行为的一些方式
(1)后台运行
如果 A
进程在 Shell 中后台运行:
./A &
它仍然会把 /dev/ttyS0
作为控制终端,但不会直接与用户交互。
(2)nohup
运行
如果 A
是通过 nohup
运行的:
nohup ./A &
A
仍然继承/dev/ttyS0
作为控制终端,但:stdout
和stderr
默认会被重定向到nohup.out
。A
不会收到SIGHUP
信号(即使终端关闭,进程仍然继续运行)。
(3)setsid()
让进程脱离控制终端成为一个类似于守护进程的存在
如果 A
在代码里调用:
setsid();
- 这会让
A
脱离控制终端,成为一个新的会话(Session)和进程组(Process Group)的领导者。 - 之后,
A
不再属于/dev/ttyS0
,不会受到终端信号(如SIGHUP
、SIGINT
)的影响。从而使得进程脱离控制终端成为一个类似于守护进程的存在。
如果 setsid()
执行成功:
ps
命令的tty
字段会变成?
(表示没有控制终端)。
如果 setsid()
失败:
- 可能
A
本身已经是会话首进程,导致setsid()
调用失败(返回-1
)。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
if (setsid() == -1) {
perror("setsid failed");
exit(EXIT_FAILURE);
}
printf("Process is now detached from controlling terminal.\n");
while (1) {
sleep(1);
}
return 0;
}
(4)O_NOCTTY
影响
分两种情况来讲这个问题。
第一种情况:A 进程没有控制终端;
第二种情况:A进程有控制终端。
下面详细分析这两种情况:
第一种情况: A 进程没有控制终端
A 进程没有控制终端时,如果 A
在运行时用下面的语句打开 /dev/ttyS0
:
fd = open("/dev/ttyS0", O_RDWR);
此时由于没有设置标志位O_NOCTTY
,又由于此时A进程没有控制终端,那么此时A 进程会自动把 /dev/ttyS0
设为自己的控制终端。
注意:A进程把/dev/ttyS0
设定为自己的控制终端的这种行为与/dev/ttyS0
是不是一个已经被打开的串口终端无关,即不管此时/dev/ttyS0
是否是一个串口终端,A进程都可以把 /dev/ttyS0
设为自己的控制终端。
我们可以看到,本来嘛,A进程是没有控制终端的,但是因为A程序中打开设备文件/dev/ttyS0
的操作,使得它有了控制终端,更加不幸的是,如果/dev/ttyS0
已经或后来被打开作为一个串口终端,那么这个串口终端的某些信号会影响到我们的进程A,比如终端关闭时产生的终端挂断信号(SIGHUP)会让进程A也终止运行,这很有可有不是我们希望发生的。
那么如何使得一个没有控制终端的进程A在打开串口设备文件 /dev/ttyS0
时,不被绑定上控制终端呢,那就是设置标志位O_NOCTTY
,比如用下面语句打开 /dev/ttyS0
:
fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
当设置了标志位O_NOCTTY
,那么A进程则不会把 /dev/ttyS0
设为自己的控制终端。这样,即使 /dev/ttyS0
已经或后来被打开成为一个串口终端,这个串口终端的所有行为也不会影响到我们的进程A了。
第二种情况:A进程有控制终端
当A程序打开设备文件/dev/ttyS0
时,由于此时进程A已有控制终端了,所以此时进程A不会把 /dev/ttyS0
设为新的控制终端。即此时此刻,你设不设置标志位O_NOCTTY
对进程A的控制终端都没有影响。
3. 结论
(1)默认情况
- 如果 ELF 程序
A
是在/dev/ttyS0
终端运行的,那么A
进程的控制终端就是/dev/ttyS0
。 - 这意味着:
A
可以使用stdin
从/dev/ttyS0
读取输入。A
的stdout
、stderr
会打印到/dev/ttyS0
。A
会受到/dev/ttyS0
终端的控制信号(如Ctrl+C
)。
(2)如何避免进程绑定控制终端
nohup ./A &
:防止SIGHUP
影响,但仍然绑定/dev/ttyS0
。setsid()
:完全脱离控制终端,成为无终端的守护进程。open("/dev/ttyS0", O_RDWR | O_NOCTTY);
:即使A
打开/dev/ttyS0
,也不会变成控制终端。
(4)如何查看进程的控制终端
可以使用:
ps -o pid,pgid,sid,tpgid,tty,comm -p $(pgrep A)
示例输出:
PID PGID SID TPGID TTY COMMAND
1234 1234 1234 1234 ttyS0 A
如果 TTY
是 ?
,说明 A
没有控制终端。