IPC（Inter-Process Communication）即进程间通信，是计算机科学中的一个重要概念，是指两个不同进程之间数据交换的过程。 Socket 编程原本是为了网络服务的，之前已介绍。后来逐渐发展成一种进程间通信的方式：Unix Domain Socket IPC。它允许在同一台主机上运行的进程之间进行高效的数据传输，无需经过网络协议栈，因此具有低延迟和高性能的特点。通过文件系统中的特殊文件（通常是一个套接字文件），进程可以通过套接字（socket）来进行通信，实现双向的数据传输。 Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 IPC通信的原理与机制 IPC 通信的原理是通过特定的机制，使得不同进程之间能够相互传递信息、协作完成任务。这些机制主要包括共享内存、消息传递、管道以及套接字等。这些机制主要包括以下几种： 1. 共享内存： 原理：通过将某一块内存区域映射到多个进程的地址空间中，实现不同进程之间的数据共享。 优点：可以高效地传递大量数据。 缺点：需要解决进程间的同步和互斥问题，以保证数据的正确性。如果多个进程同时访问共享内存区域，可能会导致数据冲突或不一致。 2. 消息传递： 原理：通过发送和接收消息的方式进行进程间通信。发送方将消息发送到一个消息队列或者直接发送给特定的进程，接收方则从消息队列中读取消息或者通过特定的接收机制接收消息。 优点：可以实现异步通信和进程间的解耦。发送方和接收方可以独立地运行，不需要知道对方的存在和状态。 缺点：需要解决消息的传递顺序和消息的可靠性问题。如果消息丢失或顺序错乱，可能会导致通信失败或数据不一致。 3. 管道： 原理：一种半双工的通信方式，用于连接一个读进程和一个写进程。管道可以使得两个进程能够单向地传递数据，其中一个进程充当写端，将数据写入管道，另一个进程充当读端，从管道中读取数据。 优点：适用于父子进程或者具有亲缘关系的进程之间的通信。 缺点：不适用于无关进程间的通信。同时，管道只能进行单向通信，且通信双方的进程必须同时存在。 4. 套接字： 原理：一种在不同主机间进行网络通信的机制，它可以在应用层与传输层之间建立连接，并通过网络传输数据。套接字可以通过TCP或UDP协议进行通信，可以在同一台主机上的不同进程间或者不同主机间进行通信。 优点：具有灵活性和可扩展性。套接字可以支持多种通信协议和数据传输方式，适用于各种复杂的网络通信场景。 缺点：需要注意网络传输的可靠性和安全性。网络传输过程中可能会出现数据丢失、延迟等问题，同时还需要考虑网络安全和身份验证等问题。 套接字实现 使用AF_UNIX通信域，可声明一个使用本地文件的 socket，其他进程可绑定该文件完成进程间通信。 socket_ipc.c： #include <stdlib.h> #include <stdio.h> #include <stddef.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/stat.h> #include <errno.h> #include <string.h> #include <unistd.h> #define SOCKET_PATH "unix_domain.socket" #define SERVER_MODE 1 #define CLIENT_MODE 2 #define BUF_LEN 1024 #define OUTTEXT ("请输入要发送的信息：") #define OUTSIZE sizeof(OUTTEXT) static struct sockaddr_un socket_addr; static char *buf; void handle_error(char *err_msg) { perror(err_msg); unlink(SOCKET_PATH); // 释放 exit(-1); } void server_mode(int sockfd) { struct sockaddr_un client_addr; int client_fd, msg_len; /**** 服务端接收消息，收到 EOF 结束信息 ****/ // 1. 绑定 if (bind(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) { handle_error("bind"); } // 2. 挂起监听 if (listen(sockfd, 128) < 0) { handle_error("listen"); } // 3. 获取连接 socklen_t client_addr_len = sizeof(client_addr); if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len)) < 0) { handle_error("accept"); } printf("接收到客户端的连接\n"); do { memset(buf, 0, BUF_LEN); msg_len = recv(client_fd, buf, BUF_LEN, 0); if (strncmp(buf, "EOF", 3)) { printf("接收到客户端数据：%s\n", buf); strcpy(buf, "ojbk\n\0"); } else { printf("接收到结算信息，停止接收数据\n"); } // 回复信息 or 结束信息 send(client_fd, buf, strlen(buf), 0); } while (strncmp(buf, "EOF", 3)); unlink(SOCKET_PATH); } void client_mode(int sockfd) { int msg_len; /**** 客户端发送消息，发送 EOF 结束信息 ****/ // 连接服务端 if (connect(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) { handle_error("connect"); } do { write(STDOUT_FILENO, OUTTEXT, OUTSIZE); msg_len = read(STDIN_FILENO, buf, BUF_LEN); if (send(sockfd, buf, BUF_LEN, 0) < 0) { handle_error("send"); } // 清空缓冲区 memset(buf, 0, BUF_LEN); recv(sockfd, buf, BUF_LEN, 0); printf("接收到服务器回复的消息：%s", buf); } while (strncmp(buf, "EOF", 3)); if (shutdown(sockfd, SHUT_WR) < 0) { handle_error("shutdown"); } } int main(int argc, char const *argv[]) { int fd = 0, mode = 0; // 设计结构 // 通过main方法传参的方式 启动不同的客户端和服务端 // 缺省：如果不填写参数 or 填写的参数默认是server if (argc == 1 || strncmp(argv[1], "server", 6) == 0) { mode = SERVER_MODE; } else if (strncmp(argv[1], "client", 6) == 0) { mode = CLIENT_MODE; } else { perror("参数错误"); exit(EXIT_FAILURE); } // 创建socket_ipc通讯 memset(&socket_addr, 0, sizeof(socket_addr)); buf = malloc(BUF_LEN); socket_addr.sun_family = AF_UNIX; strcpy(socket_addr.sun_path, SOCKET_PATH); fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { handle_error("socket"); } switch (mode) { case SERVER_MODE: server_mode(fd); break; case CLIENT_MODE: client_mode(fd); break; default: break; } close(fd); free(buf); return 0; } 测试结果 IPC 通信的应用场景 操作系统：在操作系统中，每个进程都有自己独立的地址空间和资源，它们不能直接访问其他进程的内存空间。然而，在实际的应用中，进程之间需要进行数据交换、共享资源以及协作工作。为了实现这些功能，操作系统提供了IPC机制，允许进程之间进行通信和数据传输。 多线程编程：在多线程编程中，线程之间需要进行数据共享和同步，可以使用基于内存的IPC机制（如共享内存）来实现。 分布式系统：在分布式系统中，不同节点之间需要进行数据交换和通信，可以使用基于消息的IPC机制（如消息队列、套接字）来实现。 注意事项 同步与互斥：对于共享内存等基于内存的IPC机制，需要处理并发访问和同步的问题，避免数据的冲突和竞争。可以使用信号量、互斥锁等同步机制来保证数据的一致性和正确性。 消息传递的可靠性：对于消息传递等基于消息的IPC机制，需要保证消息的传递顺序和可靠性。可以使用消息队列的持久化存储、确认机制等来提高消息的可靠性。 安全性：在使用IPC通信时，需要注意安全性问题。例如，避免敏感数据的泄露、防止恶意进程的攻击等。可以使用加密、身份验证等安全措施来保护通信过程和数据的安全性。
UDP（用户数据报协议）是一种无连接的协议，相比于TCP（传输控制协议），它不需要建立连接、维护连接状态以及进行错误检查和重传，因此通常用于对实时性要求较高、但对数据完整性要求不高的场景，如视频流、在线游戏等。 本文将通过 UDP 展示如何发送和接收数据，适用于简单的应用场景。对于更复杂的需求，可能需要进一步扩展和优化代码。之前已实现 TCP 的接发数据。 Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 使用 Socket 建立多个 TCP 连接 使用Socket来建立多个TCP连接是一种常见的网络通信方式，在客户端和服务器之间进行数据交换。需要注意的是，服务器在处理多个客户端时，应该为每个客户端连接创建一个新的线程或进程，或者使用异步IO来避免阻塞。 2024-09-241 UDP 通讯流程 UDP通讯也使用socket，但是接收和发送的函数与 TCP 不一样。由于 UDP 不存在握手这一步骤，所以在绑定地址之后，服务端不需要 listen，客户端也不需要 connect，服务端同样不需要 accept。只要服务端绑定以后，就可以相互发消息了，由于没有握手过程，两端都不能确定对方是否收到消息，这也是 UDP 协议不如 TCP 协议可靠的地方。 编程基本步骤 服务端流程： 创建套接字，选择套接字类型（如 SOCK_DGRAM 表示数据报套接字） 绑定地址和端口 接收数据 处理数据（可选） 发送响应（可选）：如果需要，使用 sendto()发送响应数据 关闭套接字 客户端流程： 创建套接字 指定服务器地址和端口 发送数据，使用 sendto() 函数发送数据 接收响应（可选）：可以使用 recvfrom() 函数接收数据 关闭套接字 函数介绍 UDP 协议接收和发送数据不再使用 send 和 recv 方法，这两个方法一般用于 TCP 通信，UDP 通信使用 sendto 和 recvfrom方法。 需要包含的头文件： #include <sys/types.h> #include <sys/socket.h> 1. sendto ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); /** * @brief 向指定地址发送缓冲区中的数据（一般用于UDP模式） * * @param sockfd 套接字文件描述符 * @param buf 缓冲区指针 * @param len 缓冲区大小 * @param flags 通信标签，详细减send方法说明 * @param dest_addr 目标地址。如果用于连接模式，该参数会被忽略 * @param addrlen 目标地址长度 * @return ssize_t 发送的消息大小。发送失败返回-1 */ 2. recvfrom ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); /** * @brief 将接收到的消息放入缓冲区 buf 中。 * * @param sockfd 套接字文件描述符 * @param buf 缓冲区指针 * @param len 缓冲区大小 * @param flags 通信标签，详见recv方法说明 * @param src_addr 可以填NULL，如果 src_addr 不是 NULL，并且底层协议提供了消 息的源地址，则该源地址将被放置在 src_addr 指向的缓冲区中。 * @param addrlen 如果src_addr不为NULL，它应初始化为与 src_addr 关联的缓冲 区的大小。返回时，addrlen 被更新为包含实际源地址的大小。如果提供的缓冲区太小， 则返回的地址将被截断；在这种情况下，addrlen 将返回一个大于调用时提供的值。 * @return ssize_t 实际收到消息的大小。如果接收失败，返回-1 */ 示例程序 udp_server.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #define BUFSIZE (1024 * sizeof(char)) #define PORT_S 6666 #define handle_error(cmd, result) \ if (result < 0) \ { \ perror(cmd); \ return -1; \ } int main(int argc, char const *argv[]) { // 使用UDP协议完成客户端和服务端的通讯 // 收到 EOF 作为关闭的信号 int sockfd, temp_result, client_fd; struct sockaddr_in server_addr, client_addr; char *buf = malloc(BUFSIZE); // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 声明IPV4通信协议 server_addr.sin_family = AF_INET; // 我们需要绑定0.0.0.0地址，转换成网络字节序后完成设置 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 端口随便用一个，但是不要用特权端口 server_addr.sin_port = htons(PORT_S); /******** 网络编程 UDP 编程流程 ********/ // 1. socket 创建 sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 数据报 handle_error("socket", sockfd); // 2. 服务端绑定地址 socklen_t server_len = sizeof(server_addr); socklen_t client_len = sizeof(client_addr); temp_result = bind(sockfd, (struct sockaddr *)&server_addr, server_len); handle_error("bind", temp_result); // 3. 直接就可以收发数据 do { // 接收数据存到缓冲 memset(buf, 0, BUFSIZE); // 清空 // 接收数据 temp_result = recvfrom(sockfd, buf, BUFSIZE, 0, (struct sockaddr *)&client_addr, &client_len); handle_error("recvfrom", temp_result); if (strncmp(buf, "EOF", 3) != 0) { printf("接收到来自客户端(%s:%d)信息：%s\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf); strcpy(buf, "OK\n"); } else { printf("收到结束信息，准备关闭\n"); } // 回复数据 temp_result = sendto(sockfd, buf, 4, 0, (struct sockaddr *)&client_addr, client_len); handle_error("sendto", temp_result); } while (strncmp(buf, "EOF", 3) != 0); free(buf); return 0; } udp_client.c #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #define BUFSIZE (1024 * sizeof(char)) #define PORT_S 6666 #define OUTTEXT ("请输入要发送的信息：") #define OUTSIZE sizeof(OUTTEXT) #define handle_error(cmd, result) \ if (result < 0) \ { \ perror(cmd); \ return -1; \ } int main(int argc, char const *argv[]) { // 使用UDP协议完成客户端和服务端的通讯 // 收到 EOF 作为关闭的信号 int sockfd, temp_result, client_fd; struct sockaddr_in server_addr, client_addr; char *buf = malloc(BUFSIZE); // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 声明IPV4通信协议 server_addr.sin_family = AF_INET; // 我们需要绑定0.0.0.0地址，转换成网络字节序后完成设置 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 端口随便用一个，但是不要用特权端口 server_addr.sin_port = htons(PORT_S); /******** 网络编程 UDP 编程流程 ********/ // 1. socket 创建 sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 数据报 handle_error("socket", sockfd); // 2. 客户端不需要绑定地址 socklen_t server_len = sizeof(server_addr); socklen_t client_len = sizeof(client_addr); // 3. 直接就可以收发数据 do { write(STDOUT_FILENO, OUTTEXT, OUTSIZE); // 从控制台读取数据 int buf_len = read(STDIN_FILENO, buf, BUFSIZE - 1); // 字符串最后为 '\0' temp_result = sendto(sockfd, buf, buf_len, 0, (struct sockaddr *)&server_addr, server_len); handle_error("sendto", temp_result); // 接收数据之前需要清空缓存 memset(buf, 0, BUFSIZE); //recvfrom(sockfd, buf, BUFSIZE, 0, (struct sockaddr *)&server_addr, server_len); temp_result = recvfrom(sockfd, buf, BUFSIZE, 0, NULL, NULL); // 地址指针 ==> 之前存在数据，可以为空 handle_error("recvfrom", temp_result); // 处理接收到的数据 if (strncmp(buf, "EOF", 3) != 0) { printf("收到服务端(%s:%d)返回数据：%s\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), buf); } } while (strncmp(buf, "EOF", 3) != 0); free(buf); return 0; } 运行结果 注意事项 1. 无连接性 UDP 是一种无连接的协议，这意味着在通信开始之前不需要建立连接。因此，UDP 通信通常被称为“不可靠的”或“尽最大努力的”传输，因为它不提供确认机制、重传机制或流量控制等功能。 2. 数据报大小限制 UDP 数据报有一个最大长度限制（通常为 65535 字节），这取决于网络环境和配置。如果发送的数据超过这个限制，它将被拆分成多个数据报进行传输。然而，接收端并不保证这些拆分的数据报会按顺序到达或全部到达。 3. 缓冲区管理 UDP 没有严格的发送缓冲区管理。当调用 sendto() 函数时，内核会直接将数据发送到网络层进行传输。然而接收端有一个接收缓冲区来存储到达的数据报。如果接收缓冲区已满，则新的数据报将被丢弃。 4. 应用场景 由于 UDP 的实时性和高效性特点，它通常用于那些对实时性要求较高但对数据完整性要求不高的应用场景，如视频流、在线游戏、实时通信等。 UDP 通信流程是一个相对简单且高效的过程，它适用于那些对实时性要求较高但对数据完整性要求不高的应用场景。然而，由于UDP的无连接性和不可靠性特点，在使用时需要特别注意数据报的大小限制和缓冲区管理等问题。
使用Socket来建立多个TCP连接是一种常见的网络通信方式，在客户端和服务器之间进行数据交换。需要注意的是，服务器在处理多个客户端时，应该为每个客户端连接创建一个新的线程或进程，或者使用异步IO来避免阻塞。 本文将以多线程和多进程两种方式实现，可以运行多个客户端实例来测试服务器是否能处理多个TCP连接。可以简单地在命令行中多次运行客户端脚本，或者写一个循环来多次创建和关闭客户端连接。 在C语言中，实现一个基本的 TCP 服务器和客户端涉及使用 socket 编程接口，这通常包括 socket(), bind(), listen(), accept(), send(), recv() 和 close() 等函数，之前有所介绍。Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 示例程序 以多线程和多进程两种方式实现，支持多个客户端连接服务器。需要注意的是服务器将监听一个特定的端口，并接受来自客户端的连接。一旦连接建立，它将接收来自客户端的消息，并将其回显给客户端，而客户端的端口绑定时自动分配的，当然也可以修改程序成一个指定端口。 基于多线程支持多个 TCP 连接 server.c #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client_then_write(void * arg) { // 读取客户端发送过来的数据 回复收到 int client_fd = *(int *)arg; char * read_buf = NULL; char * write_buf = NULL; ssize_t count = 0, send_count = 0; // 初始化 read_buf = malloc(sizeof(char) * BUFFSIZE); write_buf = malloc(sizeof(char) * BUFFSIZE); if (!read_buf) { printf("服务端读缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } if (!write_buf) { printf("服务端写缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } // 接收数据打印到控制台 printf("从_%d_客户端接收到数据:%s", client_fd, read_buf); // 把收到的消息写到写缓存 strcpy(write_buf, "收到\n"); send_count = send(client_fd, write_buf, BUFFSIZE, 0); if (send_count < 0) { perror("send"); } } // 当客户端输入 ctrl+D 退出循环 // shutdown(client_fd, SHUT_RD); close(client_fd); free(read_buf); free(write_buf); } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT); /******** 网络编程 socket 流程 ********/ // 1. 创建socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定地址 tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("bind", tmp_res); // 3. 进入监听状态 tmp_res = listen(sock_fd, 128); // 等待队列长度，可以随便填 handle_error("listen", tmp_res); // 4. 获取客户端的连接 ： 需要能够接收多个连接 socklen_t cliaddr_len = sizeof(client_addr); while (1) { int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len); // 不同的进程访问全局变量而导致的竞态资源的问题 // 全局只创建了一个线程号实例，每次创建一个线程都会覆盖掉之前的线程号 pthread_t pid_read_write; handle_error("accept", client_fd); printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd); // 和每一个客户端使用一个线程交互 把客户端发送的信息打印到控制台 回复收到 if (pthread_create(&pid_read_write, NULL, read_from_client_then_write, (void *)&client_fd)) { perror("pthread_create"); } // 需要等待线程结束 但是不能挂起等待，否则就无法创建下一个连接 pthread_detach(pid_read_write); } printf("释放资源\n"); close(sock_fd); return 0; } client.c #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define ADDRESS_S ("127.0.0.1") #define ADDRESS_C ("192.168.100.137") #define PORT_S 6666 #define PORT_C 8888 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_server(void * arg) { // 使用recv接收客户端发送的数据 打印到控制台 char * read_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 read_buf = malloc(TYPESIZE); ssize_t count = 0; if (!read_buf) { perror("malloc_server_read_buf"); return NULL; } // 接收数据 // 只要能接受到数据 正常使用 一直挂起 while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } fputs(read_buf, stdout); } printf("服务端请求关闭\n"); free(read_buf); return NULL; } void * write_to_server (void * arg) { // 接收控制台输入的信息 写出去 char * write_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 write_buf = malloc(TYPESIZE); ssize_t count = 0; if (!write_buf) { perror("malloc_server_write_buf"); return NULL; } while (fgets(write_buf, BUFFSIZE, stdin) != NULL) { // 发送数据 count = send(client_fd, write_buf, BUFFSIZE, 0); if (count < 0) { perror("send"); } } printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n"); // 可以具体到关闭某一端 shutdown(client_fd, SHUT_WR); free(write_buf); return NULL; } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 inet_pton(AF_INET, ADDRESS_S, &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT_S); /******** 网络编程 socket 流程 ********/ // 1. 创建socket，只有一个文件描述符 sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定 (自动分配) // 客户端可以不绑定，系统会自动分配一个空闲的端口号给客户端 // 3. 客户端主动连接服务端 tmp_res = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("connect", tmp_res); printf("连接上服务端_%s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); // 创建子线程用于收消息 pthread_t pid_read, pid_write; pthread_create(&pid_read, NULL, read_from_server, (void *)&sock_fd); // 创建子线程用于发消息 pthread_create(&pid_write, NULL, write_to_server, (void *)&sock_fd); // 阻塞主线程 pthread_join(pid_read, NULL); pthread_join(pid_write, NULL); // 释放资源 printf("释放资源\n"); close(sock_fd); return 0; } makefile server:server.c - $(CC) -o $@ $^ client:client.c - $(CC) -o $@ $^ 运行结果 基于多进程支持多个 TCP 连接 客户端程序和Makefile文件与上述一致，无需更改，唯一变动的是服务端程序，需要注意的是如何避免僵尸进程，新建进程后关闭不会使用到的资源。 server.c #include <sys/socket.h> #include <sys/types.h> #include <sys/wait.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <unistd.h> #include <signal.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client_then_write(void * arg) { // 读取客户端发送过来的数据 回复收到 int client_fd = *(int *)arg; char * read_buf = NULL; char * write_buf = NULL; ssize_t count = 0, send_count = 0; // 初始化 read_buf = malloc(sizeof(char) * BUFFSIZE); write_buf = malloc(sizeof(char) * BUFFSIZE); if (!read_buf) { printf("服务端读缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } if (!write_buf) { printf("服务端写缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } // 接收数据打印到控制台 printf("从_%d_客户端接收到数据:%s", client_fd, read_buf); // 把收到的消息写到写缓存 strcpy(write_buf, "收到\n"); send_count = send(client_fd, write_buf, BUFFSIZE, 0); if (send_count < 0) { perror("send"); } } // 当客户端输入 ctrl+D 退出循环 // shutdown(client_fd, SHUT_RD); close(client_fd); free(read_buf); free(write_buf); } /** * 关闭回收所有的子进程，避免僵尸进程 */ void zombie_dealer(int sig) { pid_t pid; int status; // 一个SIGCHLD可能对应多个子进程的退出 // 使用while循环回收所有退出的子进程，避免僵尸进程的出现 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) { printf("子进程: %d 以 %d 状态正常退出，已被回收\n", pid, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("子进程: %d 被 %d 信号杀死，已被回收\n", pid, WTERMSIG(status)); } else { printf("子进程: %d 因其它原因退出，已被回收\n", pid); } } } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 注册信号处理函数 回收子进程的资源 SIGCLD signal(SIGCLD, zombie_dealer); // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT); /******** 网络编程 socket 流程 ********/ // 1. 创建socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定地址 tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("bind", tmp_res); // 3. 进入监听状态 tmp_res = listen(sock_fd, 128); // 等待队列长度，可以随便填 handle_error("listen", tmp_res); // 4. 获取客户端的连接 ： 需要能够接收多个连接 socklen_t cliaddr_len = sizeof(client_addr); while (1) { int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len); // 不同的进程访问全局变量而导致的竞态资源的问题 // 全局只创建了一个线程号实例，每次创建一个线程都会覆盖掉之前的线程号 pthread_t pid_read_write; handle_error("accept", client_fd); // 创建单独的子进程和连接的客户端交互 pid_t pid = fork(); if (pid < 0) { perror("fork"); } else if (pid == 0) { // 子进程 // 关闭不会使用的sock_fd close(sock_fd); printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd); // 从客户端读取数据 并回复 read_from_client_then_write((void *)&client_fd); // 关闭 client_fd close(client_fd); exit(EXIT_SUCCESS); } else { // 父进程 // 关闭掉不使用的client_fd close(client_fd); } } printf("释放资源\n"); close(sock_fd); return 0; } 运行结果 可以看到两个连接所创建的 client_fd 文件描述符都是4，是因为每次创建子进程后，父进程就会释放 client_fd，对应的文件描述符也就被释放，新的连接被创建时则会从可用的最小文件描述符开始占用，因此新的 client_fd 也是4。虽然 client_fd 是一样的，但对应的底层文件描述却是完全不同的。
Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socket 扮演着核心角色，用于实现不同主机之间的进程间通信。 简单来说就是一种编程接口，允许在不同主机上的进程（运行中的程序）通过网络进行数据交换。 Socket 的类型 Socket 主要分为两种类型： 流式 Socket（SOCK_STREAM）：基于 TCP 协议，提供面向连接的、可靠的数据传输服务。 数据传输的顺序与发送的顺序相同，数据不重复，且接收方必须读取了发送方发送的所有数据后才能继续接收后续数据。 数据报 Socket（SOCK_DGRAM）：基于 UDP 协议，提供无连接的、不可靠的数据传输服务。 数据报是独立的，保留数据边界，发送方和接收方之间没有明确的连接，且发送方和接收方都可能不按顺序接收数据。 套接字的组成 一个套接字主要由以下三个属性组成： 网络地址：通常是 IP 地址，用于标识网络上的设备。 端口号：用于标识设备上的特定应用或进程。端口号是一个16位的数字，范围从0到65535。 协议：如 TCP（传输控制协议）或 UDP（用户数据报协议），定义了数据传输的规则和格式。 Socket 编程的基本步骤 Socket 编程的基本步骤大致相同，无论使用的是哪种 Socket 类型（TCP或UDP），但具体细节（如连接管理）会有所不同。以下是一个简化的 TCP Socket 编程步骤： 服务器端 创建Socket：使用 socket() 函数创建一个新的 Socket。 绑定Socket：使用 bind() 函数将Socket与特定的 IP 地址和端口号绑定。 监听连接：使用 listen() 函数使Socket进入被动监听状态，等待客户端的连接请求。 接受连接：使用 accept()函数接受客户端的连接请求，该函数会返回一个新的 Socket，用于与客户端进行通信。 数据收发：使用 read() / write() 或 recv() / send() 函数与客户端进行数据的发送和接收。 关闭连接：数据传输完成后，使用 close() 函数关闭Socket连接。 客户端 创建Socket：使用 socket() 函数创建一个新的 Socket。 连接服务器：使用 connect() 函数与服务器建立连接。 数据收发：使用 read() / write() 或 recv() / send() 函数与服务器进行数据的发送和接收。 关闭连接：数据传输完成后，使用 close() 函数关闭 Socket 连接。 注意事项 在进行 Socket 编程时，需要注意处理网络异常和错误，例如连接失败、数据接收不完整等。 对于 TCP 连接，要注意处理好连接的建立和断开，避免资源泄露。 对于 UDP 连接，虽然不需要建立连接，但也要注意数据包的完整性和顺序性。 在进行跨网络通信时，需要注意防火墙和网络策略的设置，确保数据能够顺利传输。 示例程序 为方便了解连接建立过程，该仅支持同时建立单个 TCP 连接，实现双方可以互相读写。对于相关函数参数并未做详细解释，如需请自行查看相关文档。目前还是很多都在采用 AF_INET（IPv4），使用时需要注意的是网络字节序和主机字节序转化。 server.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client(void * arg) { // 使用recv接收客户端发送的数据 打印到控制台 char * read_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 read_buf = malloc(TYPESIZE); ssize_t count = 0; if (!read_buf) { perror("malloc_server_read_buf"); return NULL; } // 接收数据 // 只要能接受到数据 正常使用 一直挂起 while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } fputs(read_buf, stdout); } printf("客户端请求关闭\n"); free(read_buf); return NULL; } void * write_to_client(void * arg) { // 接收控制台输入的信息 写出去 char * write_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 write_buf = malloc(TYPESIZE); ssize_t count = 0; if (!write_buf) { perror("malloc_server_write_buf"); return NULL; } while (fgets(write_buf, BUFFSIZE, stdin) != NULL) { // 发送数据 count = send(client_fd, write_buf, BUFFSIZE, 0); if (count < 0) { perror("send"); } } printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n"); // 可以具体到关闭某一端 shutdown(client_fd, SHUT_WR); free(write_buf); return NULL; } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res, client_fd; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT); /******** 网络编程 socket 流程 ********/ // 1. 创建socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定地址 tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("bind", tmp_res); // 3. 进入监听状态 tmp_res = listen(sock_fd, 128); // 等待队列长度，可以随便填 handle_error("listen", tmp_res); // 4. 获取客户端的连接 socklen_t client_addr_len = sizeof(client_addr); // 返回的文件描述符才是能够和客户端收发消息的文件描述符 // 如果调用accept之后没有客户端连接，这里会挂起等待 client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &client_addr_len); handle_error("accept", client_fd); printf("与客户端%s:%d建立连接 --> 文件描述符是 %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd); // 创建子线程用于收消息 pthread_t pid_read, pid_write; pthread_create(&pid_read, NULL, read_from_client, (void *)&client_fd); // 创建子线程用于发消息 pthread_create(&pid_write, NULL, write_to_client, (void *)&client_fd); // 阻塞主线程 pthread_join(pid_read, NULL); pthread_join(pid_write, NULL); // 释放资源 printf("释放资源\n"); close(client_fd); close(sock_fd); return 0; } client.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define ADDRESS_S ("127.0.0.1") #define ADDRESS_C ("192.168.100.137") #define PORT_S 6666 #define PORT_C 8888 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_server(void * arg) { // 使用recv接收客户端发送的数据 打印到控制台 char * read_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 read_buf = malloc(TYPESIZE); ssize_t count = 0; if (!read_buf) { perror("malloc_server_read_buf"); return NULL; } // 接收数据 // 只要能接受到数据 正常使用 一直挂起 while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } fputs(read_buf, stdout); } printf("服务端请求关闭\n"); free(read_buf); return NULL; } void * write_to_server (void * arg) { // 接收控制台输入的信息 写出去 char * write_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 write_buf = malloc(TYPESIZE); ssize_t count = 0; if (!write_buf) { perror("malloc_server_write_buf"); return NULL; } while (fgets(write_buf, BUFFSIZE, stdin) != NULL) { // 发送数据 count = send(client_fd, write_buf, BUFFSIZE, 0); if (count < 0) { perror("send"); } } printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n"); // 可以具体到关闭某一端 shutdown(client_fd, SHUT_WR); free(write_buf); return NULL; } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写客户端类型 client_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 inet_pton(AF_INET, ADDRESS_C, &client_addr.sin_addr); // 填写端口号 client_addr.sin_port = htons(PORT_C); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 inet_pton(AF_INET, ADDRESS_S, &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT_S); /******** 网络编程 socket 流程 ********/ // 1. 创建socket，只有一个文件描述符 sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定 tmp_res = bind(sock_fd, (struct sockaddr *)&client_addr, sizeof(client_addr)); handle_error("bind", tmp_res); // 3. 客户端主动连接服务端 tmp_res = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("connect", tmp_res); printf("连接上服务端_%s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); // 创建子线程用于收消息 pthread_t pid_read, pid_write; pthread_create(&pid_read, NULL, read_from_server, (void *)&sock_fd); // 创建子线程用于发消息 pthread_create(&pid_write, NULL, write_to_server, (void *)&sock_fd); // 阻塞主线程 pthread_join(pid_read, NULL); pthread_join(pid_write, NULL); // 释放资源 printf("释放资源\n"); close(sock_fd); return 0; } 在上述例程中，将客户端绑定到了 192.168.100.137 的 8888 端口，192.168.100.137 实际上是本机 IP，此处等价于 localhost 或 127.0.0.1。此外，通常服务端不需要绑定到具体的 IP 和端口，如果不绑定，启动后会操作系统会随机为客户端分配本机的某个端口。这里将客户端绑定至指定的 IP 和端口，主要是为了在分析时便于区分客户端和服务端，实际的客户端程序完全可以省去这一步。 后续 这里是学习中遇到的或想到的问题，以下都是个人总结。 2024-09-24 补充：进程I/O和网络编程的缓冲区。 ① 进程的I/O缓冲区 进程的缓冲区机制是一种中间存储技术，用于在数据处理和传输中暂存数据。对于提高计算效率、优化资源利用、以及确保数据在处理和传输过程中的稳定性和可靠性至关重要。 进程的I/O缓冲区用于文件读写或通过标准输入输出与终端交互。分为输入缓冲区和输出缓冲区。 （1）输入缓冲区存储从文件或其他外部源接收的数据，直到进程准备好处理它。 （2）输出缓冲区暂存要发送到外部设备或系统的数据，直到能够执行写操作。 ② 网络编程中的缓冲区 网络编程中，尤其是使用socket通信时，内核会在内核空间维护网络缓冲区，它也分为输入输出缓冲区。与I/O缓冲区不同，网络缓冲区的管理是由内核来完成的，用户无法干预。当然，用户可以在应用程序中通过在堆空间动态分配内存的方式构建额外的缓冲区。 （1）数据接收 当网络数据到达网络接口（如以太网卡）时，数据首先被操作系统的网络驱动接收。操作系统会将这些数据存储在内核空间的接收缓冲区中。应用程序之后通过读取系统调用（如recv或read），从内核空间的缓冲区中获取这些数据。 （2）数据发送 当应用程序想要发送数据时，它通过写入系统调用（如send或write）将数据传递给内核。内核将这些数据存放在内核空间的发送缓冲区中，然后根据网络协议的需要适时将数据发送到网络上。 ③ 进程的缓冲模式 设置缓冲模式（C语言可以通过调用setvbuf()函数来设置文件流的缓冲模式） 行缓冲（Line Buffering） 全缓冲（Fully Buffering） 无缓冲（No Buffering） 2024-10-16 补充：进程守护与 I/O 多路复用。 进程守护和 I/O 多路复用是计算机科学中的两个重要概念，它们各自在不同的领域发挥着关键作用。以下是对这两个概念的详细解释： 一、进程守护 定义：守护进程（Daemon Process）是在后台运行的一种特殊类型的进程，通常不与任何终端关联。 特点：独立于用户会话，通常在系统启动时启动，并持续运行以执行一些特定的任务或服务。为了提供系统服务、执行定期任务、监听网络请求等目的而设计。 实现步骤（以创建守护进程为例）： 调用 umask 设置文件创建掩码，以确保守护进程创建的文件具有适当的权限。 关闭不再需要的文件描述符，防止它们在后续的操作中干扰。 执行守护进程的实际工作，这通常是一个简单的无限循环，可以根据实际需求来添加守护进程的具体逻辑。 （可选）设置信号处理函数，以确保守护进程在收到特定信号时能够执行适当的操作。 二、I/O多路复用 定义：I/O多路复用（I/O Multiplexing）是一种计算机编程技术，它允许程序能够同时监视多个输入和输出（I/O）流，而不需要为每个流创建一个独立的线程。 原理：使用一组系统调用同时监听多个文件描述符的 I/O 状态，包括可读、可写、异常等，从而实现高效的I/O操作管理。 常见方法： select：早期的一种多路复用方法，允许程序监视多个文件描述符（例如套接字）是否就绪，从而进行读写操作。但每次调用 select 都需要把 fd_set 集合从用户态拷贝到内核态，开销较大，且对被监控的 fd_set 集合大小有限制（通常为 1024）。 poll：类似于 select，但在某些方面更优越，它克服了 select 的一些限制，如没有最大数量限制（但数量过大后性能会下降）。 epoll（Linux 特有）：高效的多路复用技术，通过维护一个事件表来跟踪文件描述符的状态变化，可以更好地处理大量的 I/O 连接。 kqueue（BSD/Mac 特有）：类似于 epoll，高性能的多路复用机制。 优点：提高了程序的效率和响应能力，能够在单个线程中管理多个 I/O 操作。 三、进程守护与 I/O 多路复用的关系 虽然进程守护和 I/O 多路复用是两个不同的概念，但它们在某些应用场景中可以结合使用。例如，一个守护进程可能需要同时处理多个网络连接或文件 I/O 操作，这时就可以利用 I/O 多路复用技术来提高效率和响应能力。通过 I/O 多路复用，守护进程可以在单个线程中同时监视多个 I/O 流，当有事件发生时进行响应，从而避免了为每个 I/O 流创建一个独立线程的开销。 综上所述，进程守护和 I/O 多路复用都是计算机科学中的重要概念，它们在不同的领域发挥着关键作用。在实际应用中，可以根据具体需求将它们结合使用，以实现更高效、更可靠的程序设计和开发。 —由文心大模型3.5生成
线程同步的主要目的是确保多个线程在访问共享资源时能够按照一定的顺序进行，避免因为多个线程同时操作同一资源而导致的数据混乱或错误，以下解释 Linux 中线程同步的竞态条件和锁。 竞态条件和锁 当多个线程并发访问和修改同一个共享资源（如全局变量）时，如果没有适当的同步措施，就会遇到线程同步问题。这种情况下，程序最终的结果依赖于线程执行的具体时序，导致了竞态条件。 竞态条件（race condition）是一种特定的线程同步问题，指的是两个或者以上进程或者线程并发执行时，其最终的结果依赖于进程或者线程执行的精确时序。它会导致程序的行为和输出超出预期，因为共享资源的最终状态取决于线程执行的顺序和时机。为了确保程序执行结果的正确性和预期一致，需要通过适当的线程同步机制来避免竞态条件。 如何避免竞态条件 避免多线程写入一个地址。 给资源加锁，使同一时间操作特定资源的线程只有一个。 常见的锁机制 锁主要用于互斥，即在同一时间只允许一个执行单元（进程或线程）访问共享资源。包括上面的互斥锁在内，常见的锁机制共有三种： （1）互斥锁（Mutex）：保证同一时刻只有一个线程可以执行临界区的代码。 （2）读写锁（Reader/Writer Locks）：允许多个读者同时读共享数据，但写者的访问是互斥的。 （3）自旋锁（Spinlocks）：在获取锁之前，线程在循环中忙等待，适用于锁持有时间非常短的场景，一般是Linux内核使用。 后续 这里是学习中遇到的或想到的问题，以下都是个人总结。 2024-08-31 补充：关于读写锁中出现的写饥饿问题。 ① 问题描述 读写锁的写饥饿问题（Writer Starvation）是指在使用读写锁时，写线程可能无限期地等待获取写锁，因为读线程持续地获取读锁而不断地推迟写线程的执行。这种情况通常在读操作远多于写操作时出现。 ② 解决方案 Linux 提供了可以修改的属性 pthread_rwlockattr_t，默认情况下，属性中指定的策略为“读优先”，当写操作阻塞时，读线程依然可以获得读锁，从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为“写优先”，当写操作阻塞时，读线程无法获取锁，避免了写线程持有锁的时间持续延长，使得写线程获取锁的等待时间显著降低，从而避免写饥饿问题。 ③ 个人总结 没有妥善管理读写操作的优先级，导致写者无限期等待资源。
线程处理是计算机编程中的一个重要概念，它涉及到多线程编程的各个方面，包括线程的创建、调度、同步、互斥、通信以及异常处理等。 线程的作用 线程处理在软件开发中扮演着重要角色，主要体现在以下几个方面： 1. 提高应用程序的响应能力：通过多线程处理，可以将耗时的操作放在后台线程中执行，从而避免阻塞主线程，使得应用程序能够更快地响应用户的操作。 2. 提高程序的吞吐量：在多处理器或多核系统上，多线程处理可以使得多个线程并行执行，从而提高程序的执行效率，减少程序的总体执行时间。 3. 实现复杂的并发操作：在需要处理多个并发任务时，如网络通信、文件操作等，多线程处理提供了一种有效的解决方案。 Linux 线程简介 Linux 中的线程是指轻量级的执行单元，相比于进程，具有以下特点： （1）进程（Process）是正在执行的程序的实例。每个进程都有自己的地址空间、代码段、数据段和打开的文件描述符等资源。线程（Thread）是进程内的一个执行单元，它共享相同的地址空间和其他资源，包括文件描述符、信号处理等，但每个线程都有自己的栈空间。 （2）由于共享地址空间和数据段，同一进程的多线程之间进行数据交换比进程间通信方便很多，但也由此带来线程同步问题。 （3）同一进程的多线程共享大部分资源，除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多，所以多线程比多进程更适合高并发。 线程控制 线程创建 pthread_create #include <pthread.h> /** * 创建一个新线程 * * pthread_t *thread: 指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符 * const pthread_attr_t *attr: pthead_attr_t结构体,这个参数可以用来设置线程的属性, * 如优先级、栈大小等。如果不需要定制线程属性,可以传入 NULL,此时线程将采用默认属性。 * void *(*start_routine)(void *): 一个指向函数的指针,它定义了新线程开始执行时的入口点。 * 这个函数必须接受一个 void * 类型的参数,并返回 void * 类型的结果 * void *arg: start_routine 函数的参数,可以是一个指向任意类型数据的指针 * return: int 线程创建结果 * 成功 0 * 失败 非0 */ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); 每个线程都有一个唯一的标识符（即线程ID），这个标识符是通过 pthread_t 类型的变量来表示的，当pthread_create 成功创建一个线程时，它会将新线程的标识符存储在 thread 参数指向的位置。 pthread_t定义在头文件 <pthreadtypes.h> 中，实际上是 long 类型（long 和 long int 是相同类型的不同写法）的别名。 typedef unsigned long int pthread_t; 新线程执行函数的声明为 void *(*start_routine)(void *)，其入参和返回值都是 void * 指针。我们可以将传递给线程函数的参数包装为结构体，并将其指针作为入参，再在函数内部处理；同理我们可以在线程函数内部将要返回的状态码和返回值包装为结构体，并将指针作为返回值 return。 线程终止 线程终止有以下几种方法： 线程函数执行return语句； 线程函数内部调用 pthread_exit 函数； 其他线程调用 pthread_cancel 函数。 线程终止函数为 pthread_exit： #include <pthread.h> /** * 结束关闭调用该方法的线程，并返回一个内存指针用于存放结果 * void *retval: 要返回给其它线程的数据 */ void pthread_exit(void *retval); 当某个线程调用pthread_exit方法后，该线程会被关闭（相当于return）。线程可以通过retval向其它线程传递信息，retval指向的区域不可以放在线程函数的栈内。其他线程（例如主线程）如果需要获得这个返回值，需要调用pthread_join方法。 #include <pthread.h> /** * 等待指定线程结束，获取目标线程的返回值，并在目标线程结束后回收它的资源 * * pthread_t thread: 指定线程ID * void **retval: 这是一个可选参数，用于接收线程结束后传递的返回值。 * 如果非空，pthread_join 会在成功时将线程的 exit status 复制到 *retval 所指向的内存位置。 * 如果线程没有显式地通过 pthread_exit 提供返回值，则该参数将被设为 NULL 或忽略 * return: int 成功 0 * 失败 1 */ int pthread_join(pthread_t thread, void **retval); 其它终止方式参数说明 #include <pthread.h> /** * @brief 将线程标记为detached状态。POSIX线程终止后，如果没有调用pthread_detach或pthread_join，其资源会继续占用内存，类似于僵尸进程的未回收状态。默认情况下创建线程后，它处于可join状态，此时可以调用pthread_join等待线程终止并回收资源。但是如果主线程不需要等待线程终止，可以将其标记为detached状态，这意味着线程终止后，其资源会自动被系统回收。 * * @param thread 线程ID * @return int 成功返回0，失败返回错误码 */ int pthread_detach(pthread_t thread); /** * @brief 向目标线程发送取消请求。目标线程是否和何时响应取决于它的取消状态和类型 * 取消状态（Cancelability State）：可以是enabled（默认）或disabled。如果取消状态为禁用，则取消请求会被挂起，直至线程启用取消功能。如果取消状态为启用，则线程的取消类型决定它何时取消。 * 取消类型（Cancelability Type）：可以是asynchronous（异步）或deferred（被推迟，默认值）。 * asynchronous：意味着线程可能在任何时候被取消（通常立即被取消，但系统并不保证这一点） * deferred：被推迟意味着取消请求会被挂起，直至被取消的线程执行取消点（cancellation point）函数时才会真正执行线程的取消操作。 * 取消点函数：是在POSIX线程库中专门设计用于检查和处理取消请求的函数。当被取消的线程执行这些函数时，如果线程的取消状态是enabled且类型是deferred，则它会立即响应取消请求并终止执行。man 7 pthreads可以看到取消点函数列表。 * * @param thread 目标线程，即被取消的线程 * @return int 成功返回0，失败返回非零的错误码 * 需要注意的是，取消操作和pthread_cancel函数的调用是异步的，这个函数的返回值只能告诉调用者取消请求是否成功发送。当线程被成功取消后，通过pthread_join和线程关联将会获得PTHREAD_CANCELED作为返回信息，这是判断取消是否完成的唯一方式 */ int pthread_cancel(pthread_t thread); /** * @brief 设置调用线程的取消状态 * PTHREAD_CANCEL_ENABLE：启用取消功能 * PTHREAD_CANCEL_DISABLE：禁用取消功能 * * @param state 目标状态 * @param oldstate 指针，用于返回历史状态 * @return int 成功返回0，失败返回非零错误码 */ int pthread_setcancelstate(int state, int *oldstate); #include <pthread.h> /** * @brief 设置调用线程的取消类型 * PTHREAD_CANCEL_DEFERRED：设置取消类型为推迟 * PTHREAD_CANCEL_ASYNCHRONOUS：设置取消类型为异步 * * @param type 目标类型 * @param oldtype 指针，用于接收历史类型 * @return int 成功返回0，失败返回非零错误码 */ int pthread_setcanceltype(int type, int *oldtype); 测试代码 #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <math.h> #include <string.h> // 定义结构体接收线程结果 typedef struct Result { char *p; int len; } Result; // 如果一个进程的多个线程同时使用到标准的输入输出流，会造成冲突的问题 // 导致只能有一个线程使用到标准到标准的输入输出流。 // 1 号线程 void * one_thread(void * arg) { // 初始化 Result 结构体 Result * result = malloc(sizeof(Result)); // 解析传递的参数 char code = *((char *)arg); // 声明存放读取消息的字符串 char *ans = malloc(101); while (1) { fgets(ans, 100, stdin); if (ans[0] == code) { // 收到回复的消息 free(ans); printf("1 号线程结束\n"); char * one_ans = strdup("1 号线程已被关闭"); result->p = one_ans; result->len = strlen(one_ans); // 结束线程 返回结果 pthread_exit((void *)result); } else { printf("1 号线程正在等待\n"); } } } // 2 号线程 void * two_thread(void * arg) { // 初始化 Result 结构体 Result * result = malloc(sizeof(Result)); // 解析传递的参数 char code = *((char *)arg); // 声明存放读取消息的字符串 char *ans = malloc(101); while (1) { fgets(ans, 100, stdin); if (ans[0] == code) { // 收到回复的消息 free(ans); printf("2 号线程结束\n"); char * one_ans = strdup("2 号线程已被关闭"); result->p = one_ans; result->len = strlen(one_ans); // 结束线程 返回结果 pthread_exit((void *)result); } else { printf("2 号线程正在等待\n"); } } } int main(int argc, char const *argv[]) { // 创建两个线程 pthread_t pid_one; pthread_t pid_two; char one_code = 'o'; char two_code = 't'; Result * one_result = NULL; Result * two_result = NULL; // 创建线程 1 pthread_create(&pid_one, NULL, one_thread, &one_code); // 创建线程 2 pthread_create(&pid_two, NULL, two_thread, &two_code); // 获取线程 1 的结果 pthread_join(pid_one, (void **)&one_result); printf("1 号线程：%s\n，统计：%d\n", one_result->p, one_result->len); // 释放内存 free(one_result->p); free(one_result); // 获取线程 2 的结果 pthread_join(pid_two, (void **)&two_result); printf("2 号线程：%s\n，统计：%d\n", two_result->p, two_result->len); free(two_result->p); free(two_result); return 0; } 后续 这里是学习中遇到的或想到的问题，以下都是个人总结。 2024-08-31 问题：上述代码中命令行输入的消息不以o或t开头，输出信息的线程是不确定的。 解决：由于没有合理控制线程的并发访问，而导致引发竞态条件。
进程通信是指在不同进程之间传输数据或交换信息的过程。由于进程各自拥有独立的内存地址空间，它们之间的数据不能直接通过内存地址来访问，因此需要通过特定的通信机制来实现信息交换。 进程通信的必要性 资源共享：多个进程可能需要访问共享资源，如共享内存、文件等，进程通信是实现资源共享的关键。 协同工作：多个进程可能需要协同完成某项任务，进程间的通信是协调它们工作的重要手段。 状态同步：一个进程的状态变化可能需要通知其他进程，以便它们作出相应的调整。 Linux 中进程间通信方式 只举例管道、消息队列等代码。同时不推荐进行双向操作（双工），因为比较古老，容易出现问题，比如出现混乱，一方发送的消息自己也接收。如果要实现全双工，可以使用两条消息队列（管道）分别负责两个方向的通信。 1. 管道（Pipes） 匿名管道（Anonymous Pipes）：仅存在于内存中的管道，用于具有亲缘关系的进程间通信（如父子进程）。它是一种半双工通信方式，数据只能单向流动。通过pipe()系统调用创建。 #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main(int argc, char const *argv[]) { /* 0 ： 读 1 ： 写 */ int pipefd[2]; // 将程序传递进来的第一个命令行参数 通过管道传输给子进程 if (argc != 2) { fprintf(stderr, "%s请填写需要传递的信息\n", argv[0]); exit(EXIT_FAILURE); } // 创建管道 if(pipe(pipefd) == -1) { perror("创建管道失败!\n"); exit(EXIT_FAILURE); } // 复制父子进程 pid_t cpid = fork(); if (cpid == -1) { perror("fork"); exit(EXIT_FAILURE); } else if (cpid == 0) { // 子进程：读取管道的数据，打印到控制台 close(pipefd[1]); char str[100] = {0}; sprintf(str, "子进程（%d）接收信息\n", getpid()); write(STDOUT_FILENO, str, sizeof(str)); char buf; 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]); // 将数据写入到管道中 printf("父进程（%d）向子进程传递信息\n", getpid()); write(pipefd[1], argv[1], strlen(argv[1])); close(pipefd[1]); waitpid(cpid, NULL, 0); exit(EXIT_SUCCESS); } return 0; } 命名管道（Named Pipes或FIFOs）：也称为FIFO（First In First Out），它在文件系统中以文件的形式存在，因此允许任意进程间进行通信。通过mkfifo()或mknod()系统调用创建。命名管道具有持久性，直到被显式删除。代码如下，这里为收发两端的代码，需要打开两个控制台进行测试。 fifo_read.c #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define PIPO_PATH ("/tmp/myfifo") int main(int argc, char const *argv[]) { if (mkfifo(PIPO_PATH, 0664)) { perror("mkfifo"); if (errno != EEXIST) // 文件已存在 { exit(EXIT_FAILURE); } } // 对有名管道的特殊文件 创建fd int fd = open(PIPO_PATH, O_RDONLY); if (fd == -1) { perror("open"); exit(EXIT_FAILURE); } char buf[100]; ssize_t read_num; // 读取管道信息写入到控制台 while((read_num = read(fd, buf, 100)) > 0) { write(STDOUT_FILENO, buf, read_num); } if(read_num < 0) { perror("read"); close(fd); exit(EXIT_FAILURE); } printf("接受管道信息完成 -->> 进程终止\n"); close(fd); // 释放管道:清除对应的特殊文件 if(unlink(PIPO_PATH)) { perror("unlink"); exit(EXIT_FAILURE); } return 0; } fifo_write.c #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define PIPO_PATH ("/tmp/myfifo") int main(int argc, char const *argv[]) { if (mkfifo(PIPO_PATH, 0664)) { perror("mkfifo"); if (errno != EEXIST) // 文件已存在 { exit(EXIT_FAILURE); } } // 对有名管道的特殊文件 创建fd // 创建完成之后，后续是可以重复使用的，不推荐使用，推荐每次用完释放 int fd = open(PIPO_PATH, O_WRONLY); if (fd == -1) { perror("open"); exit(EXIT_FAILURE); } char buf[100]; ssize_t read_num; // 读取控制台数据写入到管道中 while((read_num = read(STDIN_FILENO, buf, 100)) > 0) { write(fd, buf, read_num); } if(read_num < 0) { perror("read"); close(fd); exit(EXIT_FAILURE); } printf("发送数据到管道完成 -->> 进程终止\n"); close(fd); // 释放管道:清除对应的特殊文件 if(unlink(PIPO_PATH)) { perror("unlink"); exit(EXIT_FAILURE); } return 0; } 2. 信号（Signals） 信号是软件层次上对中断机制的一种模拟，用于通知进程某个事件已经发生。进程可以选择忽略信号、捕捉信号并执行特定的信号处理函数，或者按默认方式处理信号。信号主要用于异步通知，不适用于数据传输。 #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> void sigint_handler(int signum) { printf("\nCOPY THAT __ %d __ SIGNAL : END\n", signum); exit(signum); } int main(int argc, char const *argv[]) { if (signal(SIGINT, sigint_handler) == SIG_ERR) { perror("signal"); return 1; } while (1) { sleep(1); printf("Hello, World!\n"); } return 0; } 3. 消息队列（Message Queues） 消息队列是消息的链表，存放在内核中并由消息队列标识符标识。它允许一个或多个进程向它写入与读取消息。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 #include <fcntl.h> #include <sys/stat.h> #include <mqueue.h> #include <stdio.h> #include <time.h> #include <string.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char const *argv[]) { // 创建消息队列 struct mq_attr attr; // 有用的参数 表示消息队列的容量 attr.mq_maxmsg = 10; attr.mq_msgsize = 100; // 被忽略的消息 在创建消息的时候用不到 attr.mq_flags = 0; attr.mq_curmsgs = 0; char * mq_name = "/parent_child_mq"; mqd_t mqdes = mq_open(mq_name, O_RDWR | O_CREAT, 0664, &attr); if (mqdes == (mqd_t) -1) { perror("mq_open"); exit(EXIT_FAILURE); } // 创建父子进程 pid_t pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 等待接收消息队列中的信息 char read_buf[100]; struct timespec time_info; for (size_t i = 0; i < 10; i++) { // 清空接收数据的缓冲区 memset(read_buf, 0, 100); // 设置接收数据的等待时间 clock_gettime(0, &time_info); time_info.tv_sec += 15; // 等待 15 s // 接受消息队列的数据 打印到控制台 if (mq_timedreceive(mqdes, read_buf, 100, NULL, &time_info) == -1) { perror("mq_timedreceive"); } printf("子进程接收到数据：%s\n", read_buf); } } else { // 父进程 发送消息到消息队列中 char send_buf[100]; struct timespec time_info; for (size_t i = 0; i < 10; i++) { // 清空处理buf memset(send_buf, 0, 100); sprintf(send_buf, "父进程的第%ld次发送消息\n", i + 1); // 获取当前的具体时间 clock_gettime(CLOCK_REALTIME, &time_info); clock_gettime(0, &time_info); time_info.tv_sec += 5; // 等待 5 s // 发送消息 if (mq_timedsend(mqdes, send_buf, strlen(send_buf), 0, &time_info) == -1) { perror("mq_timedsend"); } printf("父进程发送一条消息，休眠 1 s\n"); sleep(1); } } // 最终不管是父进程还是子进程都需要释放消息队列的引用 close(mqdes); // 清除消息队列只需要执行一次 if (pid > 0) { mq_unlink(mq_name); } return 0; } 4. 共享内存（Shared Memory） 共享内存允许多个进程访问同一块内存区域。这种方式通常比较高效，因为数据不需要在进程间复制。但是，使用共享内存需要处理进程间的同步和互斥问题，以防止数据不一致。 #include <sys/mman.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define SHM_SIZE 1024 int main(int argc, char const *argv[]) { char * share; int statu_loc; // 1. 创建一个共享内存对象 char shm_name[BUFSIZ] = {0}; sprintf(shm_name, "/letter%d", getpid()); int fd = shm_open(shm_name, O_RDWR | O_CREAT, 0644); if (fd < 0) { perror("shm_open"); exit(EXIT_FAILURE); } // 2. 设置共享内存对象大小 ftruncate(fd, SHM_SIZE); // 3. 内存映射 share = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (share == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); } // 映射完成，关闭fd连接 不是释放 close(fd); // 4. 使用内存映射实现进程间的通讯 pid_t pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 strcpy(share, "This is a test!\n"); printf("子进程__%d__完成发送\n", getpid()); } else { // 父进程 waitpid(pid, &statu_loc, 0); // 状态可以为 NULL printf("父进程__%d__接收子进程__%d__：%s\n", getpid(), pid, share); // 5. 释放映射区 int res_munmap = munmap(share, SHM_SIZE); if (res_munmap == -1) { perror("munmap"); exit(EXIT_FAILURE); } } // 6. 释放共享内存对象 shm_unlink(shm_name); return 0; } 5. 套接字（Sockets） 套接字是一种更为通用的进程间通信机制，它不仅可用于同一主机上的进程间通信，还可用于不同主机间的网络通信。套接字可以基于不同的协议族（如UNIX域套接字、TCP/IP套接字等）和类型（如流式套接字、数据报套接字等）来实现。 代码 MakeFile 编写 CC := gcc FAILNAME:FALINAME.c # FAILNAME : 文件名 -$(CC) -o $@ $^ -./$@ -rm ./$@
常用的文件I/O函数及其基本用法。 函数用法 IOFILE.c #include <stdio.h> #define FILENAME_IO ("io.txt") #define FILENAME_TX ("text.txt") #define TEXT_PUTS (" litter noob..\n") #define TEXT_PRINTF ("Justin_Wu") #define BUF_MAX_SIZE (100) int main(int argc, char const *argv[]) { /* * char *__restrict __filename： 字符串表示要打开的文件名称 * char *__restrict __modes：访问模式 * （1）r：只读模式 如果没有文件会报错 * （2）w：只写模式 如果文件存在清空文件 如果不存在创建新文件 * （3）a：只追加写模式 如果文件存在末尾追加写 如果不存在创建新文件 * （4）r+：读写模式 文件必须存在 写入是从头一个一个覆盖 * （5）w+：读写模式 如果文件存在清空文件 如果不存在创建新文件 * （6）a+：读追加写模式 如果文件存在末尾追加写 如果不存在创建新文件 * return：FILE * 结构体指针 表示一个文件 * 报错返回 NULL * FILE *fopen (const char *__restrict __filename, const char *__restrict __modes) */ FILE * iofile = fopen((char *)FILENAME_IO, "a+"); if (iofile == NULL) { printf("Not File!\n"); } else { printf("Opened Success!\n"); } /** int fputc (int __c, FILE *__stream); * int __c：ASCII码对应的char * FILE *__stream：打开的一个文件 * return：成功返回对于的char 失败返回EOF * 读写权限记录在fopen方法中的参数 */ int put_result = fputc(97, iofile); if (put_result == EOF) { printf("Writed Word is File!\n"); } else { printf("Writed '%c' !\n", put_result); } /** fputs (const char *__restrict __s, FILE *__restrict __stream); * char *__restrict __s：需要写入的字符串 * FILE *__restrict __stream：需要写入的文件 * return：成功返回非法整数（0， 1） 失败返回EOF * 读写权限记录在fopen方法中的参数 */ int puts_result = fputs((char *)TEXT_PUTS, iofile); if (put_result == EOF) { printf("Writed Word is File!\n"); } else { printf("Writed Word is Success!\n"); } /** fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...) * FILE *__restrict __stream：需要打开的文件 * const char *__restrict __s：带格式化的长字符串 * ... 可变参数：填入格式化的长字符串 * return：成功返回写入的字符的个数 不包含换行符 失败返回EOF */ int printf_result = fprintf(iofile, "This is a fprintf\n\t\t%s", TEXT_PRINTF); if (printf_result == EOF) { printf("Writed Word is File!\n"); } else { printf("Writed Word is Success!\n"); } /** int fgetc (FILE *__stream); * FILE *__stream：需要打开的文件 * return：成功返回读取到的一个字节 如果失败返回EOF（出错或者文件的末尾） */ FILE * iofile_tx = fopen((char *)FILENAME_TX, "r"); while (fgetc(iofile_tx) != EOF) { printf("%c", fgetc(iofile_tx)); } printf("\n"); /** char * fgets (char *__restrict __s, int __n, FILE *__restrict __stream) * 读取文件的内容 * char *__restrict __s：接收读取到的字符串 * int __n：接收数据的长度 * FILE *__restrict __stream：打开要读取的文件 * return：成功返回读取到字符串 如果失败返回NULL，可以直接使用while */ char buf[BUF_MAX_SIZE]; while (fgets(buf, sizeof(buf), iofile_tx)) { // 光标已经移到最后，打印为空 printf("%s", buf); } /** int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...) * FILE *__restrict __stream：打开的文件 * char *__restrict __format：带格式化的字符串（固定格式接收） * ... 可变参数：填写格式化的字符串（接收数据提前声明的变量） * return：成功匹配到的参数个数，如果批评失败返回0 报错或者文件结束EOF */ char name[50]; int age; char wife[50]; int scanf_result = fscanf(iofile, "%s %d %s", name, &age, wife); if (scanf_result != EOF) { printf("Success!\n"); } else { printf("MATCH：%d,Road File!\n", scanf_result); } /** int fclose (FILE *__stream); * FILE *__stream：需要关闭的文件 * return： 成功返回 0 失败返回EOF（负数）通常关闭文件失败会直接报错 */ int result = fclose(iofile); if (result == EOF) { printf("Closed FAIL!\n"); } else if (!result) { fclose(iofile_tx); printf("Closed Success!\n"); } else { printf("Not File!\n"); } return 0; } 标准输入/输出/错误 stdin_out_err_test.c #include <stdio.h> #include <stdlib.h> #define BUF_SIZE (sizeof(char) * 100) int main() { char * buf = (char *)malloc(BUF_SIZE); // 从标准输入中读取数据 fgets(buf, BUF_SIZE, stdin); printf("buf：%s\n", buf); return 0; // 标准输出 fputs(buf, stdout); printf("\n"); // 错误输出 fputs(buf, stderr); printf("\n"); free(buf); } 系统调用 unixstdio.c #include <unistd.h> #include <fcntl.h> #include <stdio.h> #define FILE_NAME "io.txt" #define BUF_SIZE 1024 int main() { /** int open (const char *__path, int __oflag, ...); * const char *__path: 文件路径 * int __oflag: 用于指定打开文件的方式,可以是以下选项的组合: * (1) O_RDONLY: 以只读方式打开文件 * (2) O_WRONLY: 以只写方式打开文件 * (3) O_RDWR: 以读写方式打开文件 * (4) O_CREAT: 如果文件不存在,则创建一个新文件 * (5) O_APPEND: 将所有写入操作追加到文件的末尾 * (6) O_TRUNC: 如果文件存在并且以写入模式打开,则截断文件长度为0 * 还有其他标志,如O_EXCL（当与O_CREAT一起使用时,只有当文件不存在时才创建新文件）、O_SYNC（同步I/O）、O_NONBLOCK（非阻塞I/O）等 * 可选参数: mode -> 仅在使用了O_CREAT标志且文件尚不存在的情况下生效,用于指定新创建文件的权限位 权限位通常由三位八进制数字组成,分别代表文件所有者、同组用户和其他用户的读写执行权限 * return: (1) 成功时返回非负的文件描述符。 * (2) 失败时返回-1，并设置全局变量errno以指示错误原因。 */ // Linux操作系统有文件保护权限，默认创建的文件会被删除掉其它用户的写权限 int fd = open(FILE_NAME, O_RDONLY | O_CREAT, 0755); if (fd == -1) { perror("open"); return -1; } /** ssize_t read (int __fd, void *__buf, size_t __nbytes); * int __fd: 一个整数,表示要从中读取数据的文件描述符 * void *__buf: 一个指向缓冲区的指针,读取的数据将被存放到这个缓冲区中: * size_t __nbytes:一个size_t类型的整数,表示要读取的最大字节数 系统调用将尝试读取最多这么多字节的数据, * 但实际读取的字节数可能会少于请求的数量 * return: (1) 成功时,read()返回实际读取的字节数 这个值可能小于__nbytes, * 如果遇到了文件结尾（EOF）或者因为网络读取等原因提前结束读取 * (2) 失败时,read()将返回-1 */ char buf[BUF_SIZE]; int ret = read(fd, buf, sizeof(buf)); if (ret == -1) { perror("read"); return -1; } printf("read data: %s\n", buf); /** ssize_t write (int __fd, const void *__buf, size_t __n); * int __fd: 一个整数,表示要将数据写入到的文件描述符 * const void *__buf: 一个指向缓冲区的指针,写入的数据需要先存放到这个缓冲区中 * size_t __n:一个size_t类型的整数,表示要写入的字节数 write()函数会尝试写入__n个字节的数据, * 但实际写入的字节数可能会少于请求的数量 * return: (1) 成功时,write()返回实际写入的字节数 这个值可能小于__n,如果写入操作因故提前结束, * 例如: 磁盘满、网络阻塞等情况 (2) 失败时,write()将返回-1 */ ret = write(fd, buf, sizeof(buf)); if (ret == -1) { perror("write"); return -1; } /** int close (int __fd); * int __fd:一个整数,表示要关闭的文件描述符 * return: (1) 成功关闭时 返回0 (2) 失败时 返回-1 */ ret = close(fd); if (ret == -1) { perror("close"); return -1; } /** * 立即终止当前进程，且不进行正常的清理操作，如关闭文件、释放内存等。 * 这个函数通常在程序遇到严重错误需要立即退出时使用，或者在某些情况下希望避免清理工作时调用。 * void _exit (int status); * void _Exit (int __status); * * void exit (int status); * 终止当前进程,但是在此之前会执行3种清理操作 * (1) 调用所有通过atexit()注册的终止处理函数(自定义) * (2) 刷新所有标准I/O缓冲区(刷写缓存到文件) * (3) 关闭所有打开的标准I/O流(比如通过fopen打开的文件) * int status: 父进程可接收到的退出状态码 0表示成功 非0表示各种不同的错误 */ _exit(0); return 0; } 使用 MakeFile gcc := CC IOFILE: IOFILE.c -$(CC) -o $@ $^ -./$@ -rm ./$@ stdin_out_err_test: stdin_out_err_test.c -$(CC) -o $@ $^ -./$@ -rm ./$@ unixstdio: unixstdio.c -$(CC) -o $@ $^ -./$@ -rm ./$@
环境配置 MCU：STM32F103RC 步进驱动芯片：BDR6622T IDE：STM32CubeMX + Keil5 步进芯片原理图 & MX 配置 原理图： STM32CubeMX： 实现代码 /* USER CODE BEGIN 0 */ void motor_run(uint8_t direction,uint8_t step){ if(0 == direction){ //正转 switch(step){ case 0: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); break; case 1: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); break; case 2: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; case 3: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; } }else{ //反转 switch(step){ case 0: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; case 1: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; case 2: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); break; case 3: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); break; } } } void runstep(){ uint8_t direction = 0; int step = 500; for(int i = 0; i < step; i++){ motor_run(direction, i % 4); HAL_Delay(1); } HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); } /* USER CODE END 0 */ /* USER CODE BEGIN 2 */ runstep(); /* USER CODE END 2 */ //改变runstep()中的direction就是改变方向，改变step就是改变步数。 学习链接 button { position: relative; display: inline-block; cursor: pointer; outline: none; border: 0; vertical-align: middle; text-decoration: none; background: transparent; padding: 0; font-size: inherit; font-family: inherit; } button.learn-more { width: 12rem; height: auto; } button.learn-more .circle { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); position: relative; display: block; margin: 0; width: 3rem; height: 3rem; background: #282936; border-radius: 1.625rem; } button.learn-more .circle .icon { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); position: absolute; top: 0; bottom: 0; margin: auto; background: #fff; } button.learn-more .circle .icon.arrow { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); left: 0.625rem; width: 1.125rem; height: 0.125rem; background: none; } button.learn-more .circle .icon.arrow::before { position: absolute; content: ""; top: -0.29rem; right: 0.0625rem; width: 0.625rem; height: 0.625rem; border-top: 0.125rem solid #fff; border-right: 0.125rem solid #fff; transform: rotate(45deg); } button.learn-more .button-text { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); position: absolute; top: 0; left: 0; right: 0; bottom: 0; padding: 0.75rem 0; margin: 0 0 0 1.85rem; color: #282936; font-weight: 700; line-height: 1.6; text-align: center; text-transform: uppercase; } button:hover .circle { width: 100%; } button:hover .circle .icon.arrow { background: #fff; transform: translate(1rem, 0); } button:hover .button-text { color: #fff; } 小智学长
51 单片机是不支持硬件 I2C 的，所以我们要驱动 I2C 设备，只能通过代码实现软件 I2C，简单来说就是通过 IO 口模拟 I2C 的时序。 代码实现： #include <reg52.h> sbit sda = P0 ^ 1; sbit scl = P0 ^ 2; /* 50kbit/s = 1s 50k周期 = 1周期 = 1/50ks = 20us 10us发送1bit，1ms=100bit 1s = 100kbit */ void delay10us() //@11.0592MHz { unsigned char i; i = 2; while (-- i); } void i2c_start() { scl = 1; sda = 1; delay10us(); //起始信号建立时间，高电平时间持续大于4.7us sda = 0; //SDA拉低，下降沿 delay10us(); //起始信号保持时间 } void i2c_stop() { scl = 1; sda = 0; delay10us(); //停止信号建立时间，SDA高电平时间持续大于4.7us sda = 1; //SDA拉高，上升沿 delay10us(); //总线空闲时间保持 } void i2c_write_bit(unsigned char databit) { scl = 0; //开始发送前，设置scl为0 if (databit == 1) { //设置需要发送的数据 sda = 1; } else { sda = 0; } delay10us(); scl = 1; delay10us(); } void i2c_write_byte(unsigned char datasend) { /* 0x20 = 0b0010 0000 & 0b1000 0000 --> 检测第一位 0x20 = 0b0010 0000 << 1 0b0100 0000 --> 检测下一位 */ int i = 0; for (i = 0; i < 8; i ++) { if (datasend & 0x80) // 判断最高位是否为 1 { i2c_write_bit(1); // 输出高电平 } else { i2c_write_bit(0); // 输出低电平 } datasend <<= 1; //左移一位，for循环8次，直到数据发送完 } } unsigned char i2c_read_bit() { unsigned char databit = 0; scl = 0; //每bit接收前，先设置scl为0 delay10us(); //scl为低电平的时间 scl = 1; //拉高scl开始接收数据 delay10us();//等待数据稳定的时间 if (sda) { databit = 1; } else { databit = 0; } return databit; } unsigned char i2c_read_byte() { unsigned char value = 0; unsigned char i = 0; sda = 1; // 处于空闲状态 for (i = 0; i < 8; i ++) { if (i2c_read_bit() == 1) { value |= 0x01; } else { value |= 0x00; } if (i < 7) // 防止数据丢失 /* 每次向左移动一位以后，如果sda=1的时候就把最后一位置1， sda=0的时候则不用置，因为向左移动就有一个0了 */ value <<= 1; } scl = 0; //接收完成后，设置scl为0 delay10us(); return value; } // 应答信号 void i2c_ack() { scl = 0; sda = 0; //SDA拉低，发出应答信号 delay10us();; scl = 1; delay10us(); scl = 0; } // 等待应答信号 void i2c_wait_ack() { unsigned char time = 0; scl = 0; sda = 1; //sda为高电平释放总线，然后检测应答信号是否被拉低 delay10us(); scl = 1; delay10us(); while (sda) //sda为高电平，就表示没有检查到ACK， { delay10us(); time ++; if (time > 100) break; } scl = 0; delay10us(); } void i2c_nack() { scl = 0; sda = 1; //SDA拉高，发出非应答信号 delay10us(); scl = 1; delay10us(); scl = 0; } void main() { i2c_start(); // 开始 i2c_write_byte(0x55); // 发送数据 i2c_wait_ack(); // 等待应答 i2c_stop(); // 结束 while (1) { } } 学习链接 button { position: relative; display: inline-block; cursor: pointer; outline: none; border: 0; vertical-align: middle; text-decoration: none; background: transparent; padding: 0; font-size: inherit; font-family: inherit; } button.learn-more { width: 12rem; height: auto; } button.learn-more .circle { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); position: relative; display: block; margin: 0; width: 3rem; height: 3rem; background: #282936; border-radius: 1.625rem; } button.learn-more .circle .icon { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); position: absolute; top: 0; bottom: 0; margin: auto; background: #fff; } button.learn-more .circle .icon.arrow { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); left: 0.625rem; width: 1.125rem; height: 0.125rem; background: none; } button.learn-more .circle .icon.arrow::before { position: absolute; content: ""; top: -0.29rem; right: 0.0625rem; width: 0.625rem; height: 0.625rem; border-top: 0.125rem solid #fff; border-right: 0.125rem solid #fff; transform: rotate(45deg); } button.learn-more .button-text { transition: all 0.45s cubic-bezier(0.65, 0, 0.076, 1); position: absolute; top: 0; left: 0; right: 0; bottom: 0; padding: 0.75rem 0; margin: 0 0 0 1.85rem; color: #282936; font-weight: 700; line-height: 1.6; text-align: center; text-transform: uppercase; } button:hover .circle { width: 100%; } button:hover .circle .icon.arrow { background: #fff; transform: translate(1rem, 0); } button:hover .button-text { color: #fff; } 小智学长