简介:Linux系统中,串口与网口是用于不同通信环境的接口,该程序能够实现串口数据与网络数据之间的转换。本文将介绍串口通信的基础知识、网络编程的相关概念以及程序设计中涉及的关键技术部分。本程序的主要功能包括串口的操作、网络套接字的编程、数据转换处理、多线程或多进程处理、信号处理、日志和错误处理以及配置文件的读取。通过学习本程序,读者能够深入理解Linux系统级编程和网络编程,提升在物联网设备和工业自动化系统开发中的应用能力。
1. 串口通信基本概念介绍
串口通信(Serial Communication),是一种在计算机和设备之间以串行方式传输数据的通信方式,广泛用于各种嵌入式系统、工业控制和通信设备之间。本章将带你认识串口通信的基础知识,深入理解其工作原理及应用。
1.1 串口通信简介
串口通信是基于串行通信协议的一种数据传输方式,它通过一条数据线(加上地线和控制线,共三条)来实现数据的发送和接收。与并行通信相比,串口通信虽然速度较慢,但因硬件连线简单,成本低廉,因而被广泛应用于近距离通信。
1.2 串口通信的工作原理
在串口通信中,数据的发送是按位顺序进行的,每次只发送一个位。通信过程遵循一定的标准协议,如RS-232C、RS-422和RS-485等。这些协议规定了信号电平、数据格式、信号传输速率以及通信距离等参数。
1.3 串口通信的应用场景
串口通信常见于嵌入式系统中,如与微控制器、调制解调器、工业自动化设备、医疗设备等的通信。此外,串口也是计算机常见的外部接口之一,例如,PC上的COM端口就是一种典型的串口形式。
接下来的章节,我们将深入探讨网络编程的基础知识,包括著名的TCP/IP协议栈,它对于理解现代网络通信至关重要。通过对比不同的网络层和协议,我们将建立串口通信与网络编程之间的桥梁,进一步拓展对通信技术的认识和应用。
2. 网络编程基础与TCP/IP协议栈
2.1 网络通信模型
2.1.1 OSI七层模型概述
OSI(Open Systems Interconnection)模型,即开放式系统互联通信参考模型,是由国际标准化组织(ISO)提出的一个概念模型,它将计算机网络通信过程划分为七个层次。从底层到高层依次为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
每层都有其特定的功能与职责,通过层与层之间的接口进行数据交换,每一层使用下一层提供的服务,实现本层的功能,并向上层提供服务。OSI模型为不同厂商和不同类型的网络提供了统一的通信标准和框架,有助于理解和学习网络工作原理。
2.1.2 TCP/IP模型详解
与OSI模型相对应,TCP/IP模型是一个更为简洁的网络通信模型,它通常被视为四层模型,分别为网络接口层、网际层(网络层)、传输层和应用层。
- 网络接口层对应于OSI的物理层和数据链路层,负责数据的帧传输和硬件寻址。
- 网际层对应于OSI的网络层,负责处理数据包在网络中的路由。
- 传输层对应于OSI的传输层,提供端到端的通信服务,如TCP和UDP协议。
- 应用层则整合了OSI会话层、表示层和应用层的功能,包含了各种面向应用的服务协议,如HTTP、FTP等。
TCP/IP模型因其实用性和在互联网上的广泛应用而成为网络通信的事实标准。
2.2 TCP/IP协议族
2.2.1 IP协议原理与应用
IP协议,即互联网协议(Internet Protocol),是网络层的核心协议。它定义了数据包如何在不同的网络中路由和转发。IP协议分为IPv4和IPv6两种版本。
IPv4使用32位地址,支持约43亿个独立的网络地址。而IPv6使用128位地址,理论上可提供340万亿个独立的地址。IPv4在设计上是一个无连接的协议,不保证数据包的顺序和完整性,也不提供流量控制或拥塞控制。
IP协议的主要工作是将数据包从源主机发送到目的主机,经过多个网络设备的转发,可能需要经历路径选择、分片、重组等过程。在IP头中包含了源地址、目的地址、协议类型、生存时间(TTL)等信息。
2.2.2 TCP与UDP协议对比
传输控制协议(TCP)和用户数据报协议(UDP)是TCP/IP协议族中最重要的两种传输层协议。
-
TCP提供面向连接的、可靠的数据传输服务。它通过序列号、确认应答、校验和、流量控制和拥塞控制等机制确保数据传输的可靠性和顺序。TCP适用于需要高度可靠性的应用,如HTTP、FTP等。
-
UDP提供无连接的网络通信服务。它简单、高效,但不保证数据包的顺序、可靠性或完整性。由于其低开销和低延迟的特性,UDP经常用于需要快速传输的应用,如在线视频流、实时游戏等。
2.2.3 应用层协议简介
应用层协议定义了如何进行数据的传输以及数据如何被呈现。常见的应用层协议包括HTTP、HTTPS、FTP、SMTP、DNS等。
HTTP(超文本传输协议)和HTTPS(HTTP安全版)主要用于网页的浏览和数据的检索。FTP(文件传输协议)用于在网络中的计算机之间传输文件。SMTP(简单邮件传输协议)用于发送电子邮件。DNS(域名系统)将人类易读的域名转换为计算机可以理解的IP地址。
这些协议定义了不同的网络通信规则,使得各种应用能够通过网络层和传输层进行有效和准确的通信。
3. 串口操作相关API使用
3.1 Linux下的串口编程接口
3.1.1 termios结构体解析
在Linux系统中,串口通信是通过 termios 结构体来配置和管理的。 termios 定义于 <termios.h> 头文件中,提供了对终端I/O的控制接口。 termios 结构体包含多个标志和设置项,它们共同定义了终端的行为特性,如输入输出处理、信号控制等。
struct termios {
tcflag_t c_iflag; // 输入模式标志
tcflag_t c_oflag; // 输出模式标志
tcflag_t c_cflag; // 控制模式标志
tcflag_t c_lflag; // 本地模式标志
cc_t c_cc[NCCS]; // 控制字符数组
};
-
c_iflag:控制输入模式,可以配置诸如回显、行控制等功能。 -
c_oflag:控制输出模式,如输出处理和输出模式设置。 -
c_cflag:控制硬件设置,如波特率、字符大小、停止位和奇偶校验。 -
c_lflag:控制终端行处理,包括规范模式、回显输入等。 -
c_cc:控制字符数组,如EOF字符、重载字符等。
使用 termios 结构体时,首先通过 tcgetattr() 函数获取当前串口的状态,然后可以根据需要修改相应的标志位。修改完成后,通过 tcsetattr() 函数将新的配置设置到串口上。
3.1.2 串口配置与打开
串口的配置与打开是通过 open() 函数来实现的。在调用 open() 之前,需要通过 termios 结构体来配置串口的各项参数。例如,若要设置波特率为9600、8位数据位、1位停止位和无奇偶校验,需要按照以下步骤操作:
- 打开串口文件,如
/dev/ttyS0。 - 使用
tcgetattr()获取当前串口配置。 - 修改
termios结构体中的c_cflag成员以配置波特率等硬件参数。 - 使用
tcsetattr()应用新配置。 - 配置完成之后,可以使用
read()和write()对串口进行数据的读写操作。
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
int serial_fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);
if (serial_fd == -1) {
perror("open_port: Unable to open /dev/ttyS0 - ");
return -1;
}
struct termios options;
tcgetattr(serial_fd, &options);
cfsetispeed(&options, B9600);
cfsetospeed(&options, B9600);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
tcsetattr(serial_fd, TCSANOW, &options);
以上代码展示了如何打开串口并设置基本的串口参数。需要注意的是,错误处理在此处被省略,实际应用中需要对每个步骤进行验证和错误处理。
3.2 数据读写与控制
3.2.1 串口数据的读取
串口数据的读取通常使用 read() 系统调用。在配置好串口参数后,可以通过 read() 函数从串口读取数据。重要的是确保在读取数据之前串口已经被正确配置。如果串口的 c_cc[VMIN] 和 c_cc[VTIME] 被设置为合理值,那么 read() 调用将会根据这些设置来等待数据的到来。
char buf[1024];
int num_bytes = read(serial_fd, buf, sizeof(buf));
if (num_bytes < 0) {
perror("read_port: read() returned -1 - ");
} else if (num_bytes == 0) {
printf("read_port: No bytes received (end-of-file or disconnected?)\n");
} else {
printf("read_port: %d bytes read into buffer.\n", num_bytes);
// 处理接收到的数据
}
在处理读取的数据时,要考虑到可能存在的错误情况,例如读取超时或文件描述符错误等。因此,在读取串口数据时应该有相应的错误处理机制。
3.2.2 串口数据的发送
发送数据到串口使用 write() 系统调用。在向串口发送数据前,同样需要确保串口已被正确配置。通过 write() 可以将数据一次性发送出去。如果在发送期间发生错误, write() 调用会返回负值。
const char *data_to_send = "Hello, Serial Port!";
int num_bytes_written = write(serial_fd, data_to_send, strlen(data_to_send));
if (num_bytes_written < 0) {
perror("write_port: write() returned -1 - ");
} else {
printf("write_port: %d bytes sent to the device.\n", num_bytes_written);
}
在发送数据后,应该检查 write() 调用的返回值来确认数据是否成功发送。如果 write() 返回的字节数小于请求的字节数,这可能表示发生了错误。
3.2.3 串口状态监控与控制
串口的状态监控通常涉及到检查输入缓冲区是否有数据可读,以及输出缓冲区是否已经清空。在Linux中, select() 和 poll() 函数可以用来监控文件描述符上的事件,包括串口。
fd_set input_mask;
FD_ZERO(&input_mask);
FD_SET(serial_fd, &input_mask);
struct timeval timeout;
timeout.tv_sec = 1; // 1秒超时
timeout.tv_usec = 0;
int select_status = select(serial_fd + 1, &input_mask, NULL, NULL, &timeout);
if (select_status < 0) {
perror("select failed");
} else if (select_status == 0) {
printf("select timeout\n");
} else {
if (FD_ISSET(serial_fd, &input_mask)) {
printf("Data available on the serial port\n");
// 可以使用read()读取数据
}
}
此外,对串口进行流控制也是一个重要方面,包括硬件流控制(RTS/CTS)和软件流控制(XON/XOFF)。这些功能可以通过配置 termios 结构体的相关标志位来启用。
监控和控制串口状态是保证数据能够稳定传输和接收的关键。在复杂的嵌入式系统中,状态监控和流控制是确保通信质量不可忽视的部分。在具体实施时,还需要考虑到硬件的特性和软件设计的要求,灵活使用各种API和工具来实现预期的功能。
4. 网络套接字编程实践
4.1 套接字API简介
套接字(Socket)是一种进程间通信机制,允许运行在不同主机上的应用程序之间进行数据交换。它是实现网络通信的基础,网络编程中使用套接字API来创建、配置、管理网络连接。
4.1.1 套接字类型与创建
套接字API定义了多种类型的套接字,包括流式套接字(SOCK_STREAM)、数据报套接字(SOCK_DGRAM)和原始套接字等。最常用的套接字类型是SOCK_STREAM,它基于TCP协议提供可靠的连接。
创建套接字的系统调用是 socket() ,它会返回一个套接字描述符(file descriptor),用于后续的网络操作。下面是一个创建TCP流式套接字的代码示例:
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int sockfd;
struct sockaddr_in serv_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (sockfd < 0) {
perror("无法创建套接字");
exit(EXIT_FAILURE);
}
// 初始化serv_addr并绑定地址信息等后续操作...
return 0;
}
在这段代码中, AF_INET 表示使用IPv4地址, SOCK_STREAM 指定创建TCP套接字。创建套接字后,通常会继续进行地址绑定、监听、连接等操作。
4.1.2 套接字的配置与绑定
配置套接字包括设置套接字选项和将其与特定的地址和端口绑定。 setsockopt() 函数用于设置套接字选项,而 bind() 函数用于将套接字绑定到一个地址上。例如,让服务器监听特定端口:
#include <netinet/in.h>
// 填充sockaddr_in结构体
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(8080);
// 绑定套接字到地址
if (bind(sockfd, (const struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("绑定失败");
close(sockfd);
exit(EXIT_FAILURE);
}
在该代码中, serv_addr 结构体指定了服务器的地址族(IPv4)、IP地址(INADDR_ANY表示任意地址)和端口号(8080)。 bind() 函数将套接字与这个地址关联起来,使得服务器能够监听来自该地址和端口的连接请求。
4.2 网络通信程序实现
网络通信程序通常采用客户端-服务器模型,分为服务器端和客户端两部分。服务器端监听来自客户端的连接请求,客户端主动连接服务器。下面详细描述实现网络通信程序的关键步骤。
4.2.1 客户端与服务器模型
客户端和服务器模型是网络通信中常用的设计模式。服务器端负责监听连接请求,而客户端负责发起连接。这种模型使系统能够支持多个并发的客户端。
客户端实现的关键步骤包括创建套接字、连接服务器和数据传输:
// 客户端代码
int sockfd;
struct sockaddr_in serv_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
// 连接服务器
if (connect(sockfd, (const struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("连接失败");
close(sockfd);
exit(EXIT_FAILURE);
}
// 数据传输等后续操作...
服务器端则需要创建套接字、绑定地址、监听端口,并接受客户端的连接请求:
// 服务器端代码
int sockfd, newsockfd;
socklen_t clilen;
struct sockaddr_in serv_addr, cli_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定地址并监听端口
bind(sockfd, (const struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(sockfd, 5); // 监听最大5个连接请求
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
// 接收数据等后续操作...
4.2.2 数据的发送与接收
网络通信的本质是数据的发送与接收。套接字API提供了 send() 和 recv() 函数用于实现这些操作。 send() 函数用于发送数据到指定的套接字,而 recv() 函数用于从指定的套接字接收数据。
下面是一个使用 send() 和 recv() 进行数据交互的简单示例:
char buffer[1024];
char *message = "Hello, World!";
int bytes_sent, bytes_received;
// 发送数据
bytes_sent = send(sockfd, message, strlen(message), 0);
if (bytes_sent < 0) {
perror("发送失败");
close(sockfd);
exit(EXIT_FAILURE);
}
// 接收数据
bytes_received = recv(newsockfd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received < 0) {
perror("接收失败");
close(newsockfd);
exit(EXIT_FAILURE);
}
buffer[bytes_received] = '\0'; // 确保字符串结尾
printf("接收到消息:%s\n", buffer);
在这个例子中,服务器端向客户端发送一条消息”Hello, World!”,客户端接收这个消息并打印出来。 send() 和 recv() 函数都返回实际传输的字节数,它们的第三个参数指定了传输的最大字节数。
4.2.3 连接管理与异常处理
网络通信需要考虑异常处理和连接管理。套接字API提供了多种用于处理这些情况的函数,如 shutdown() 和 close() 函数。
shutdown() 函数用于关闭套接字的接收或发送能力,或两者都关闭。这对于明确地告诉对方通信的一方不再发送或接收数据很有用。 close() 函数用于关闭套接字,终止与之关联的网络连接。
在实际应用中,应当合理地处理各种异常情况,如网络中断、数据接收不完整或超时等。例如,客户端可以设置超时来处理服务器响应过慢的情况:
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时为5秒
timeout.tv_usec = 0;
setsockopt(newsockfd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&timeout, sizeof(timeout));
在这个例子中,客户端设置了发送操作的超时时间,如果5秒内服务器没有响应,则认为操作失败。
表格:常见套接字选项及其含义
| 选项名称 | 选项宏 | 含义 |
|---|---|---|
| SO_REUSEADDR | SOL_SOCKET | 允许重用本地地址和端口 |
| SO_KEEPALIVE | SOL_SOCKET | 启用连接保活检测 |
| SO_LINGER | SOL_SOCKET | 设置延迟关闭的行为 |
| TCP_NODELAY | IPPROTO_TCP | 禁用Nagle算法,立即发送数据包 |
| SO_RCVTIMEO/SO_SNDTIMEO | SOL_SOCKET | 设置接收/发送操作的超时时间 |
mermaid格式流程图:TCP连接的建立过程
graph LR
A[客户端发起connect] -->|SYN| B[服务器SYN-SENT]
B -->|SYN-ACK| C[客户端SYN-RECEIVED]
C -->|ACK| D[服务器ESTABLISHED]
D -->|ACK| E[客户端ESTABLISHED]
通过上述内容,我们深入了解了网络套接字编程的核心概念和实践操作,包括套接字API的使用、网络通信模型的实际构建、数据传输的管理、以及连接的建立和维护。掌握了这些知识,可以开发出高效、稳定的网络通信应用程序。
5. 数据转换和协议封装技巧
在现代的网络通信和数据传输中,数据的序列化与反序列化、以及数据包的封装和解封装,是确保数据能够准确无误地在网络中传输的关键技术。本章将探讨这些技术的实现细节和优化技巧。
5.1 数据序列化与反序列化
5.1.1 常见的数据格式如JSON、XML
数据序列化是指将对象状态信息转换为可以存储或传输的形式的过程。在数据交换过程中,常用的数据格式包括JSON、XML等。
-
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。JSON构建于两种结构:键值对集合(在大多数语言中称为对象、记录、结构体、字典或关联数组)和值的有序列表(在大多数语言中称为数组)。JSON的优点包括轻量级、易于阅读和编写,广泛支持多种编程语言,以及良好的互操作性。
-
XML (eXtensible Markup Language) 是一种标记语言,用于存储和传输数据。与JSON相比,XML提供了更多的结构性信息,并支持复杂的嵌套结构,但其在简单性和可读性方面则不如JSON。XML的自描述性和可扩展性使其在需要表示复杂数据结构的场合非常有用。
// JSON示例
{
"name": "John Doe",
"age": 30,
"isEmployee": true
}
<!-- XML示例 -->
<person>
<name>John Doe</name>
<age>30</age>
<isEmployee>true</isEmployee>
</person>
5.1.2 自定义协议格式解析
在一些专业或受限的环境中,开发者可能会设计自己的协议格式以满足特定需求。设计自定义协议时,应考虑以下几个原则:
- 简洁性 :协议应当尽量简单,易于理解和实现。
- 扩展性 :随着应用的发展,协议应能够方便地添加新的字段或类型。
- 高效性 :在保证数据完整性的同时,尽量减少数据传输量。
- 鲁棒性 :协议应能够处理各种异常情况,如数据包的丢失、损坏或顺序错乱。
自定义协议的示例设计:
头信息(1字节) | 类型(1字节) | 长度(2字节) | 数据字段 | 校验和(1字节)
- 头信息用于标识一个数据包。
- 类型字段表示数据包的类型。
- 长度字段用于指明数据字段的字节长度。
- 数据字段是实际传递的数据内容。
- 校验和用于数据包的完整性校验。
5.2 数据封装与解封装
5.2.1 数据包设计原则
数据封装是将要传输的数据加上控制信息,如地址、校验等,封装成一个个的数据包。设计一个好的数据包应当遵循以下原则:
- 明确目的 :每个数据包应有明确的传输目的和接收者。
- 正确性 :数据包中应包含所有必要的信息,以及足够进行错误检测和纠正的信息。
- 安全性 :如果需要,数据包设计应支持加密和认证机制。
5.2.2 协议封装流程实现
以TCP协议为例,封装流程通常涉及以下几个步骤:
- 将数据分割成适合网络传输的段(segments)。
- 将序列号、确认应答号、控制位(如SYN、FIN等)、窗口大小等TCP头部信息添加到数据段上。
- 如果是建立连接的第一个包(SYN包),则还需要添加初始序列号。
- 对整个TCP头部和数据段内容进行校验和计算,并将校验和放在TCP头部的校验和字段。
- 将TCP段交给IP层进行封装,IP层会再添加自己的头部信息(如源地址、目标地址、协议类型等),形成最终的IP数据报。
5.2.3 数据完整性校验
为了确保数据在传输过程中不被篡改,通常会进行数据完整性校验。常见的方法包括:
- 校验和 :对数据包中的所有字节计算一个简单的算术和,并将结果附加到数据包中。接收方在收到数据包后,重新计算一遍校验和,并与收到的值比较。
- 哈希函数 :使用不可逆的哈希算法,如MD5或SHA-1,生成数据的唯一“指纹”,可以检测数据是否被篡改。
// 计算校验和的C语言函数示例
unsigned short calculate_checksum(void *buffer, int length) {
unsigned short *ptr = buffer;
unsigned int sum = 0;
unsigned short result;
for(sum = 0; length > 1; length -= 2) {
sum += *ptr++;
}
if(length == 1) {
sum += *(unsigned char*)ptr;
}
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
result = ~sum;
return result;
}
本章通过对数据序列化与反序列化、协议封装、数据完整性校验的深入解析,强调了数据传输过程中的关键技术和实现策略。通过对这些技术的细致讨论,为读者提供了全面的理解和后续应用的坚实基础。
6. 多线程或多进程并发处理
多线程或多进程并发处理是高级编程技术的重要组成部分,它可以有效提高程序运行效率,支持复杂的业务场景。下面我们将详细介绍多线程编程基础、多进程编程基础以及高级并发处理模式。
6.1 多线程编程基础
6.1.1 线程的创建与控制
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。在多线程程序设计中,我们需要创建多个线程,并对它们进行控制。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void* thread_function(void* arg) {
// 线程运行的代码
printf("Hello from the thread!\n");
return NULL;
}
int main() {
pthread_t thread_id;
int res;
// 创建线程
res = pthread_create(&thread_id, NULL, thread_function, NULL);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
// 等待线程结束
pthread_join(thread_id, NULL);
printf("Thread finished execution\n");
return 0;
}
在这个例子中, pthread_create 函数用于创建一个新线程,而 pthread_join 则用于等待线程结束。
6.1.2 线程同步与互斥机制
多线程在并发执行时,可能会访问共享资源,造成数据不一致等问题。为了保证线程安全,需要使用同步和互斥机制。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_THREADS 5
void* perform_work(void* arg) {
int passed_in_value;
passed_in_value = *((int *)arg);
// 假设这里有一些工作要做
printf("hello from thread %d\n", passed_in_value);
// 为了演示同步,我们使用sleep
sleep(1);
printf("goodbye from thread %d\n", passed_in_value);
pthread_exit(NULL);
}
int main (int argc, const char * argv[]) {
pthread_t threads[NUM_THREADS];
int thread_args[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; ++i) {
printf("In main: creating thread %d\n", i);
thread_args[i] = i;
// 创建线程
pthread_create(&threads[i], NULL, perform_work, (void *)&(thread_args[i]));
}
for (int i = 0; i < NUM_THREADS; ++i) {
printf("In main: joining thread %d\n", i);
// 等待线程完成
pthread_join(threads[i], NULL);
}
return 0;
}
在上面的代码中,我们使用 pthread_join 来确保主线程等待所有线程完成,从而实现线程的同步。
6.2 多进程编程基础
6.2.1 进程的创建与控制
进程是程序的一次执行,是系统进行资源分配和调度的一个独立单位。多进程编程允许程序运行多个独立的执行流。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// 如果返回负值,则创建失败
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Hello from child process!\n");
} else {
// 父进程
printf("Hello from parent process!\n");
}
return 0;
}
在此例中, fork() 函数用于创建子进程。在Linux系统中, fork() 会返回子进程的PID给父进程,而子进程得到0,如果创建失败则返回负值。
6.2.2 进程间通信IPC
进程间通信(IPC)允许不同进程间传输数据和信号。常用的IPC技术包括管道、消息队列、共享内存等。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int pipefd[2];
pid_t cpid;
char buf;
const char *message = "Hello from parent!\n";
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* 子进程 */
close(pipefd[1]); // 关闭写端
while (read(pipefd[0], &buf, 1) > 0) {
write(STDOUT_FILENO, &buf, 1);
}
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]);
_exit(EXIT_SUCCESS);
} else { /* 父进程 */
close(pipefd[0]); // 关闭读端
write(pipefd[1], message, strlen(message));
close(pipefd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
exit(EXIT_SUCCESS);
}
}
在本例中,通过 pipe 函数创建了一个管道,父进程和子进程可以使用这个管道进行通信。
6.3 高级并发处理模式
6.3.1 线程池与进程池设计
在处理大量短时任务时,频繁的线程或进程创建与销毁会造成系统资源的浪费,这时线程池和进程池技术就显得尤为重要。
线程池维护了一个任务队列和一组工作线程,工作线程从任务队列中获取任务并执行,这样可以避免线程的频繁创建与销毁,从而提高性能。
6.3.2 并发模式与性能优化
并发模式通常指同时运行多个独立的计算任务,而性能优化是指在保证正确性的前提下,尽可能提高程序运行效率。
graph LR
A[开始] --> B[创建线程池]
B --> C[分配任务到线程池]
C --> D{检查任务队列}
D -- 队列不空 --> E[从队列中取出任务执行]
D -- 队列空 --> F[等待新任务]
E --> G[回收线程资源]
G --> D
上图是一个简化的线程池工作流程图,展示了线程池如何管理任务和线程资源。
在实际应用中,我们还需要考虑线程池的大小、任务处理策略、线程优先级等因素,以便达到最优的性能和资源利用效率。
简介:Linux系统中,串口与网口是用于不同通信环境的接口,该程序能够实现串口数据与网络数据之间的转换。本文将介绍串口通信的基础知识、网络编程的相关概念以及程序设计中涉及的关键技术部分。本程序的主要功能包括串口的操作、网络套接字的编程、数据转换处理、多线程或多进程处理、信号处理、日志和错误处理以及配置文件的读取。通过学习本程序,读者能够深入理解Linux系统级编程和网络编程,提升在物联网设备和工业自动化系统开发中的应用能力。
1126

被折叠的 条评论
为什么被折叠?



