目录
2.windows和linux使用system启动进程的区别
[2.3.TCP 监听端口继承问题详解](#2.3.TCP 监听端口继承问题详解)
[3.Linux 进程创建与端口继承问题的终极解决方案: posix_spawn](#3.Linux 进程创建与端口继承问题的终极解决方案: posix_spawn)
1.背景
在ubuntu系统中,在主进程中去启动另外一个进程,当这个进程退出后,主进程监听的端口被启动的进程引用了,造成主进程和另外一个进程共同监听同一端口,关闭主进程,端口仍在监听,再次启动主进程,就提示端口占用。用ss -tulnp命令显示如下图所示:

关键信息:
| 协议 | 状态 | 接收队列 | 发送队列 | 本地地址:端口 | 外部地址 | 关联进程信息 |
|---|---|---|---|---|---|---|
| tcp | LISTEN | 0 | 4096 | 0.0.0.0:8088 | 0.0.0.0:* | dmserver(pid=4227)、DataContainerSe(pid=3976) |
| tcp | LISTEN | 0 | 4096 | 0.0.0.0:2121 | 0.0.0.0:* | dmserver(pid=4227)、DataContainerSe(pid=3976) |
tcp:表示 TCP 协议(面向连接、可靠的传输层协议)LISTEN:端口处于监听状态,等待外部连接请求0 4096:接收 / 发送队列长度,0 表示无积压待处理数据,4096 为队列最大容量0.0.0.0:端口:表示监听本机所有网卡的对应端口,可接受任意 IP 的连接8088:常见的 HTTP/HTTPS 替代端口,常用于 Web 服务、代理或自定义应用2121:FTP 服务的常用替代端口(标准 FTP 为 21),用于文件传输
0.0.0.0:*:外部地址未指定,代表等待任意客户端连接- 进程信息 :两个端口均由
dmserver(主服务进程,PID 4227)和DataContainerSe(数据容器服务,PID 3976)共同占用,说明这两个服务在这两个端口上提供网络服务
同样的代码,在windows上用system启动这个进程没有什么问题,怎么在ubuntu下就出现问题了呢?
2.windows和linux使用system启动进程的区别
2.1.核心差异总览
| 特性 | Windows system() |
Linux system() |
|---|---|---|
| 底层实现 | CreateProcess(..., "cmd.exe /c command") |
fork() + execve("/bin/sh -c command") + waitpid() |
| 进程创建模型 | 一次性创建新进程,无进程复制 | 先复制父进程(COW),再替换为新程序 |
| 资源继承 | 仅继承标记为「可继承」且bInheritHandles=TRUE的句柄 |
默认继承所有未设置FD_CLOEXEC的文件描述符 |
| TCP 端口继承 | 默认不继承(需显式设置bInheritHandle=TRUE) |
默认继承监听 socket fd,导致端口占用 |
| 阻塞行为 | 父进程阻塞,等待子进程退出 | 父进程阻塞,等待子进程退出 |
| 信号处理 | 子进程不继承父进程信号掩码,SIGCHLD无意义 |
父进程阻塞SIGCHLD,忽略SIGINT/SIGQUIT |
| 性能 | 无进程复制开销,性能较好 | 大内存进程fork()有 COW 开销,性能较差 |
| 错误返回 | 成功返回 0,失败返回非 0,GetLastError()获取详情 |
成功返回子进程退出码,失败返回 - 1,errno设置错误码 |
2.2.底层实现深度解析
1.Linux system()执行流程
cpp
// 伪代码,POSIX标准实现
int system(const char *command) {
pid_t pid = fork(); // 1. 复制父进程,子进程继承所有fd
if (pid == -1) return -1;
if (pid == 0) { // 子进程
// 2. 执行shell命令
execl("/bin/sh", "sh", "-c", command, (char *)NULL);
_exit(127); // exec失败,返回127
}
// 父进程
int status;
// 3. 等待子进程退出,阻塞父进程
if (waitpid(pid, &status, 0) == -1) return -1;
return WEXITSTATUS(status);
}
关键问题 :fork()会完整复制父进程文件描述符表,包括 TCP 监听 socket 的 fd,导致子进程继承监听端口引用,即使父进程退出,端口仍被子进程占用
2.Windows system()执行流程
cpp
// 伪代码,Windows实现
int system(const char *command) {
STARTUPINFO si = {0};
PROCESS_INFORMATION pi;
si.cb = sizeof(si);
// 1. 构造cmd.exe命令行
char cmd[MAX_PATH];
sprintf(cmd, "cmd.exe /c %s", command);
// 2. 创建新进程,默认bInheritHandles=FALSE
BOOL success = CreateProcess(
NULL, cmd, NULL, NULL,
FALSE, // 关键:默认不继承父进程句柄
0, NULL, NULL, &si, &pi
);
if (!success) return -1;
// 3. 等待子进程退出,阻塞父进程
WaitForSingleObject(pi.hProcess, INFINITE);
// 4. 获取退出码
DWORD exit_code;
GetExitCodeProcess(pi.hProcess, &exit_code);
// 5. 关闭进程/线程句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return exit_code;
}
核心区别 :CreateProcess默认不继承父进程句柄(bInheritHandles=FALSE),子进程不会自动获得监听 socket 句柄
2.3.TCP 监听端口继承问题详解
1.Linux:默认继承,端口占用风险高
- 问题本质 :
fork()复制父进程所有文件描述符,包括监听 socket 的 fd,这些 fd 指向内核中同一个 socket 对象,引用计数 + 1 - 现象 :父进程退出后,子进程仍持有监听 socket fd,端口无法释放,导致服务重启时
bind()失败(Address already in use) - 风险 :子进程若调用
accept(),会与父进程争抢新连接,导致业务逻辑混乱
2.Windows:默认不继承,安全性更高
- 双重限制 :子进程继承句柄需满足两个条件:
- 句柄创建时设置
SECURITY_ATTRIBUTES.bInheritHandle=TRUE CreateProcess调用时设置bInheritHandles=TRUE
- 句柄创建时设置
- TCP 套接字特殊情况 :
- Winsock 套接字本身就是句柄,但默认创建时不可继承
- 即使手动设置
bInheritHandle=TRUE,某些 LSP(分层服务提供程序)可能导致继承失效
- 默认行为 :
system()调用CreateProcess时bInheritHandles=FALSE,子进程不会继承监听端口句柄,无需担心端口占用
2.4.解决端口继承问题
1.Linux 解决方案(双重保障)
| 方案 | 代码示例 | 适用场景 | |
|---|---|---|---|
创建时设置SOCK_CLOEXEC |
`int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);` | Linux 2.6.27+,推荐优先使用 |
fcntl设置FD_CLOEXEC |
fcntl(sockfd, F_SETFD, FD_CLOEXEC); |
兼容旧系统 / 第三方库创建的 socket | |
posix_spawn显式关闭 |
posix_spawn_file_actions_addclose(&fa, sockfd); |
替代system(),精细控制 fd,无fork()开销 |
2.Windows 解决方案(默认安全,无需额外操作)
system()默认不继承监听端口句柄,无需特殊处理- 若需主动控制句柄继承:
cpp
// 创建可继承的监听socket
SECURITY_ATTRIBUTES sa = {0};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE; // 允许子进程继承
SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
3.Linux 进程创建与端口继承问题的终极解决方案: posix_spawn
posix_spawn 是 POSIX 标准定义的轻量级进程创建接口 ,用于替代传统的 fork() + exec() 组合,核心优势是可以精细控制子进程的文件描述符、信号、进程属性 ,完美解决你之前遇到的「system() 子进程继承 TCP 监听端口」的问题,同时性能更优,适合 Linux 下的 C/C++ 服务端开发。
3.1.核心函数与原理
1.标准函数原型
cpp
#include <spawn.h>
#include <sys/wait.h>
// 主函数:创建子进程
int posix_spawn(
pid_t *restrict pid, // 输出:子进程PID
const char *restrict path, // 可执行文件的绝对路径
const posix_spawn_file_actions_t *restrict file_actions, // 核心:控制文件描述符
const posix_spawnattr_t *restrict attrp, // 控制进程属性(信号、调度等)
char *const restrict argv[], // 命令行参数(argv[0]为程序名,以NULL结尾)
char *const restrict envp[] // 环境变量(以NULL结尾,传environ则继承父进程)
);
// 变体:从PATH环境变量查找可执行文件(类似execvp)
int posix_spawnp(
pid_t *pid, const char *file,
const posix_spawn_file_actions_t *file_actions,
const posix_spawnattr_t *attrp,
char *const argv[], char *const envp[]
);
2.关键辅助函数
| 函数 | 核心作用 |
|---|---|
posix_spawn_file_actions_init() / destroy() |
初始化 / 销毁文件动作对象(必须配对,避免内存泄漏) |
posix_spawn_file_actions_addclose() |
向子进程添加「关闭指定文件描述符」的动作(解决端口继承的核心) |
posix_spawn_file_actions_adddup2() |
向子进程添加「重定向文件描述符」的动作(如重定向 stdout 到日志) |
posix_spawnattr_init() / destroy() |
初始化 / 销毁进程属性对象 |
posix_spawnattr_setflags() |
设置进程属性标志(如POSIX_SPAWN_SETSIGMASK控制信号掩码) |
3.执行流程对比(与system())
| 特性 | system(command) |
posix_spawn() |
|---|---|---|
| 底层实现 | fork() + execve(/bin/sh -c command) + waitpid() |
Linux 2.6+ 为内核系统调用,无fork()的 COW 开销 |
| 阻塞性 | 父进程阻塞,等待子进程退出 | 父进程非阻塞,立即返回子进程 PID |
| 文件描述符 | 子进程默认继承所有未设置FD_CLOEXEC的 fd |
可通过file_actions显式控制 fd(关闭 / 重定向) |
| 信号处理 | 子进程继承父进程信号掩码,SIGCHLD被阻塞 |
可通过attrp精细控制信号掩码、默认处理 |
| 性能 | 大内存进程fork()有 COW 开销,性能差 |
无fork()开销,适合高频创建子进程 |
| 僵尸进程 | 自动waitpid,无僵尸进程 |
需手动waitpid,否则产生僵尸进程 |
3.2.完整的代码
cpp
#define _GNU_SOURCE
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <cstring>
char** environ;
int main() {
std::string strPath = "/opt/dmdbms/bin"; // 替换为实际达梦二进制目录
const char* target_dir = strPath.c_str();
const char* cmd = "./DmServiceDMSERVER";
char* argv[] = {const_cast<char*>(cmd), const_cast<char*>("start"), NULL};
pid_t pid;
posix_spawnattr_t attr;
posix_spawn_file_actions_t fa;
if (posix_spawn_file_actions_init(&fa) != 0) {
perror("posix_spawn_file_actions_init");
return 1;
}
if (posix_spawnattr_init(&attr) != 0) {
perror("posix_spawnattr_init");
posix_spawn_file_actions_destroy(&fa);
return 1;
}
// 切换子进程工作目录
if (posix_spawn_file_actions_addchdir_np(&fa, target_dir) != 0) {
perror("posix_spawn_file_actions_addchdir_np");
posix_spawn_file_actions_destroy(&fa);
posix_spawnattr_destroy(&attr);
return 1;
}
// 关闭所有≥3的文件描述符,彻底杜绝端口继承
int ret = posix_spawn_file_actions_addclosefrom_np(&fa, 3);
if (ret != 0) {
perror("posix_spawn_file_actions_addclosefrom_np");
posix_spawn_file_actions_destroy(&fa);
posix_spawnattr_destroy(&attr);
return 1;
}
// 启动子进程
ret = posix_spawn(&pid, cmd, &fa, &attr, argv, environ);
// 清理资源
posix_spawn_file_actions_destroy(&fa);
posix_spawnattr_destroy(&attr);
if (ret == 0) {
waitpid(pid, NULL, 0);
std::cout << "SUCCESS START DMSERVER\n";
} else {
perror("posix_spawn failed to start DMSERVER");
return 1;
}
return 0;
}
glibc 版本要求 :addchdir_np/addclosefrom_np是 glibc 2.29 + 引入的 GNU 扩展,旧版本(如 CentOS 7、银河麒麟 V10 早期版本)不支持。
兼容性方案(旧 glibc 系统):
若系统 glibc < 2.29,无法使用addclosefrom_np,需替换为遍历关闭 fd的兼容写法:
cpp
// 替换 addclosefrom_np
#include <sys/resource.h>
static void close_all_fds_except_stdio(posix_spawn_file_actions_t *fa) {
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) != 0) {
perror("getrlimit failed");
return;
}
for (rlim_t fd = 3; fd < rl.rlim_cur; fd++) {
posix_spawn_file_actions_addclose(fa, fd);
}
}
// 调用方式
close_all_fds_except_stdio(&fa);