Files
go-serial/serial_unix.go
liulong 070b7685d5 Linux系统下串口Close相关实现的深度优化
1. 优化了Close方法的资源释放顺序,先释放独占访问权,再关闭实际的端口句柄
2. 添加了句柄有效性检查,避免对无效句柄进行操作
3. 确保所有资源都能正确清理,包括句柄和信号管道
4. 改进了错误处理机制,确保只返回第一个错误
5. 为所有方法添加了端口状态检查,确保在端口关闭后立即返回PortClosed错误
6. 统一了错误处理格式,使用PortError类型包装系统错误
7. 优化了Read方法的错误处理和超时逻辑
8. 为Write、Break、SetMode、SetDTR、SetRTS和GetModemStatusBits方法添加了端口状态检查
2026-03-03 10:27:03 +08:00

545 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
//go:build linux || darwin || freebsd || openbsd
package serial
import (
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"go.bug.st/serial/unixutils"
"golang.org/x/sys/unix"
)
type unixPort struct {
handle int
readTimeout time.Duration
closeLock sync.RWMutex
closeSignal *unixutils.Pipe
opened uint32
}
func (port *unixPort) Close() error {
// 原子操作检查并设置端口关闭状态
if !atomic.CompareAndSwapUint32(&port.opened, 1, 0) {
return nil // 端口已经关闭,直接返回
}
var firstErr error
// 发送关闭信号以取消所有待处理的读操作
if port.closeSignal != nil {
if _, err := port.closeSignal.Write([]byte{0}); err != nil && firstErr == nil {
firstErr = err
}
}
// 检查句柄是否有效,然后执行关闭操作
if port.handle != -1 {
// 释放独占访问权 - 应在关闭handle之前完成
if err := port.releaseExclusiveAccess(); err != nil && firstErr == nil {
firstErr = err
}
// 关闭实际的端口句柄 - 这将解除任何挂起的读写操作
if err := unix.Close(port.handle); err != nil && firstErr == nil {
firstErr = err
}
port.handle = -1 // 标记句柄无效
}
// 关闭信号管道
if port.closeSignal != nil {
if err := port.closeSignal.Close(); err != nil && firstErr == nil {
firstErr = err
}
port.closeSignal = nil // 清除指针
}
return firstErr
}
func (port *unixPort) Read(p []byte) (int, error) {
// 首先检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
var deadline time.Time
if port.readTimeout != NoTimeout {
deadline = time.Now().Add(port.readTimeout)
}
fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD())
for {
// 在每次select之前再次检查端口状态非阻塞检查
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
timeout := time.Duration(-1)
if port.readTimeout != NoTimeout {
timeout = time.Until(deadline)
if timeout < 0 {
// 负超时值在Select(...)中表示"无超时"
timeout = 0
}
}
res, err := unixutils.Select(fds, nil, fds, timeout)
if err == unix.EINTR {
continue // 系统调用被中断,重试
}
if err != nil {
// 如果在端口关闭后遇到错误返回PortClosed错误
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
return 0, &PortError{code: FunctionNotImplemented, causedBy: err}
}
if res.IsReadable(port.closeSignal.ReadFD()) {
// 收到关闭信号
return 0, &PortError{code: PortClosed}
}
if !res.IsReadable(port.handle) {
// 超时
if port.readTimeout == NoTimeout {
continue // 无超时设置,继续等待
}
return 0, nil // 返回0字节表示超时
}
n, err := unix.Read(port.handle, p)
if err == unix.EINTR {
continue // 系统调用被中断,重试
}
// Linux系统特性当读取操作期间端口断开连接时
// 端口会进入"可读但返回零长度数据"的状态。
// https://stackoverflow.com/a/34945814/1655275
if n == 0 && err == nil {
return 0, &PortError{code: PortClosed}
}
if n < 0 { // 确保不返回负数
n = 0
}
// 检查读取操作期间端口是否被关闭
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
if err != nil {
return n, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return n, nil
}
}
func (port *unixPort) Write(p []byte) (n int, err error) {
// 首先检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
n, err = unix.Write(port.handle, p)
if n < 0 { // 确保不返回负数
n = 0
}
if err != nil {
return n, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return n, nil
}
func (port *unixPort) Break(t time.Duration) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
time.Sleep(t)
if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
return nil
}
func (port *unixPort) SetMode(mode *Mode) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
settings, err := port.getTermSettings()
if err != nil {
return &PortError{code: InvalidSerialPort, causedBy: err}
}
if err := setTermSettingsParity(mode.Parity, settings); err != nil {
return err
}
if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil {
return err
}
if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil {
return err
}
requireSpecialBaudrate := false
if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil {
return err
} else if special {
requireSpecialBaudrate = true
}
if err := port.setTermSettings(settings); err != nil {
return &PortError{code: InvalidSerialPort, causedBy: err}
}
if requireSpecialBaudrate {
// MacOSX要求这是最后一个操作否则会产生'Invalid serial port'错误
if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil {
return &PortError{code: InvalidSpeed, causedBy: err}
}
}
return nil
}
func (port *unixPort) SetDTR(dtr bool) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus()
if err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
if dtr {
status |= unix.TIOCM_DTR
} else {
status &^= unix.TIOCM_DTR
}
return port.setModemBitsStatus(status)
}
func (port *unixPort) SetRTS(rts bool) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus()
if err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
if rts {
status |= unix.TIOCM_RTS
} else {
status &^= unix.TIOCM_RTS
}
return port.setModemBitsStatus(status)
}
func (port *unixPort) SetReadTimeout(timeout time.Duration) error {
if timeout < 0 && timeout != NoTimeout {
return &PortError{code: InvalidTimeoutValue}
}
port.readTimeout = timeout
return nil
}
func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return nil, &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus()
if err != nil {
return nil, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return &ModemStatusBits{
CTS: (status & unix.TIOCM_CTS) != 0,
DCD: (status & unix.TIOCM_CD) != 0,
DSR: (status & unix.TIOCM_DSR) != 0,
RI: (status & unix.TIOCM_RI) != 0,
}, nil
}
func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
h, err := unix.Open(portName, unix.O_RDWR|unix.O_NOCTTY|unix.O_NDELAY, 0)
if err != nil {
switch err {
case unix.EBUSY:
return nil, &PortError{code: PortBusy}
case unix.EACCES:
return nil, &PortError{code: PermissionDenied}
}
return nil, err
}
port := &unixPort{
handle: h,
opened: 1,
readTimeout: NoTimeout,
}
// Setup serial port
settings, err := port.getTermSettings()
if err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting term settings: %w", err)}
}
// Set raw mode
setRawMode(settings)
// Explicitly disable RTS/CTS flow control
setTermSettingsCtsRts(false, settings)
if err = port.setTermSettings(settings); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting term settings: %w", err)}
}
if mode.InitialStatusBits != nil {
status, err := port.getModemBitsStatus()
if err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting modem bits status: %w", err)}
}
if mode.InitialStatusBits.DTR {
status |= unix.TIOCM_DTR
} else {
status &^= unix.TIOCM_DTR
}
if mode.InitialStatusBits.RTS {
status |= unix.TIOCM_RTS
} else {
status &^= unix.TIOCM_RTS
}
if err := port.setModemBitsStatus(status); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting modem bits status: %w", err)}
}
}
// MacOSX require that this operation is the last one otherwise an
// 'Invalid serial port' error is returned... don't know why...
if err := port.SetMode(mode); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error configuring port: %w", err)}
}
unix.SetNonblock(h, false)
port.acquireExclusiveAccess()
// This pipe is used as a signal to cancel blocking Read
pipe := &unixutils.Pipe{}
if err := pipe.Open(); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error opening signaling pipe: %w", err)}
}
port.closeSignal = pipe
return port, nil
}
func nativeGetPortsList() ([]string, error) {
files, err := os.ReadDir(devFolder)
if err != nil {
return nil, err
}
ports := make([]string, 0, len(files))
for _, f := range files {
// Skip folders
if f.IsDir() {
continue
}
// Keep only devices with the correct name
if !osPortFilter.MatchString(f.Name()) {
continue
}
portName := devFolder + "/" + f.Name()
// Check if serial port is real or is a placeholder serial port "ttySxx" or "ttyHSxx"
if strings.HasPrefix(f.Name(), "ttyS") || strings.HasPrefix(f.Name(), "ttyHS") {
port, err := nativeOpen(portName, &Mode{})
if err != nil {
continue
} else {
port.Close()
}
}
// Save serial port in the resulting list
ports = append(ports, portName)
}
return ports, nil
}
// termios manipulation functions
func setTermSettingsParity(parity Parity, settings *unix.Termios) error {
switch parity {
case NoParity:
settings.Cflag &^= unix.PARENB
settings.Cflag &^= unix.PARODD
settings.Cflag &^= tcCMSPAR
settings.Iflag &^= unix.INPCK
case OddParity:
settings.Cflag |= unix.PARENB
settings.Cflag |= unix.PARODD
settings.Cflag &^= tcCMSPAR
settings.Iflag |= unix.INPCK
case EvenParity:
settings.Cflag |= unix.PARENB
settings.Cflag &^= unix.PARODD
settings.Cflag &^= tcCMSPAR
settings.Iflag |= unix.INPCK
case MarkParity:
if tcCMSPAR == 0 {
return &PortError{code: InvalidParity}
}
settings.Cflag |= unix.PARENB
settings.Cflag |= unix.PARODD
settings.Cflag |= tcCMSPAR
settings.Iflag |= unix.INPCK
case SpaceParity:
if tcCMSPAR == 0 {
return &PortError{code: InvalidParity}
}
settings.Cflag |= unix.PARENB
settings.Cflag &^= unix.PARODD
settings.Cflag |= tcCMSPAR
settings.Iflag |= unix.INPCK
default:
return &PortError{code: InvalidParity}
}
return nil
}
func setTermSettingsDataBits(bits int, settings *unix.Termios) error {
databits, ok := databitsMap[bits]
if !ok {
return &PortError{code: InvalidDataBits}
}
// Remove previous databits setting
settings.Cflag &^= unix.CSIZE
// Set requested databits
settings.Cflag |= databits
return nil
}
func setTermSettingsStopBits(bits StopBits, settings *unix.Termios) error {
switch bits {
case OneStopBit:
settings.Cflag &^= unix.CSTOPB
case OnePointFiveStopBits:
return &PortError{code: InvalidStopBits}
case TwoStopBits:
settings.Cflag |= unix.CSTOPB
default:
return &PortError{code: InvalidStopBits}
}
return nil
}
func setTermSettingsCtsRts(enable bool, settings *unix.Termios) {
if enable {
settings.Cflag |= tcCRTSCTS
} else {
settings.Cflag &^= tcCRTSCTS
}
}
func setRawMode(settings *unix.Termios) {
// Set local mode
settings.Cflag |= unix.CREAD
settings.Cflag |= unix.CLOCAL
// Set raw mode
settings.Lflag &^= unix.ICANON
settings.Lflag &^= unix.ECHO
settings.Lflag &^= unix.ECHOE
settings.Lflag &^= unix.ECHOK
settings.Lflag &^= unix.ECHONL
settings.Lflag &^= unix.ECHOCTL
settings.Lflag &^= unix.ECHOPRT
settings.Lflag &^= unix.ECHOKE
settings.Lflag &^= unix.ISIG
settings.Lflag &^= unix.IEXTEN
settings.Iflag &^= unix.IXON
settings.Iflag &^= unix.IXOFF
settings.Iflag &^= unix.IXANY
settings.Iflag &^= unix.INPCK
settings.Iflag &^= unix.IGNPAR
settings.Iflag &^= unix.PARMRK
settings.Iflag &^= unix.ISTRIP
settings.Iflag &^= unix.IGNBRK
settings.Iflag &^= unix.BRKINT
settings.Iflag &^= unix.INLCR
settings.Iflag &^= unix.IGNCR
settings.Iflag &^= unix.ICRNL
settings.Iflag &^= tcIUCLC
settings.Oflag &^= unix.OPOST
// Block reads until at least one char is available (no timeout)
settings.Cc[unix.VMIN] = 1
settings.Cc[unix.VTIME] = 0
}
// native syscall wrapper functions
func (port *unixPort) getTermSettings() (*unix.Termios, error) {
return unix.IoctlGetTermios(port.handle, ioctlTcgetattr)
}
func (port *unixPort) setTermSettings(settings *unix.Termios) error {
return unix.IoctlSetTermios(port.handle, ioctlTcsetattr, settings)
}
func (port *unixPort) getModemBitsStatus() (int, error) {
return unix.IoctlGetInt(port.handle, unix.TIOCMGET)
}
func (port *unixPort) setModemBitsStatus(status int) error {
return unix.IoctlSetPointerInt(port.handle, unix.TIOCMSET, status)
}
func (port *unixPort) acquireExclusiveAccess() error {
return unix.IoctlSetInt(port.handle, unix.TIOCEXCL, 0)
}
func (port *unixPort) releaseExclusiveAccess() error {
return unix.IoctlSetInt(port.handle, unix.TIOCNXCL, 0)
}