0 前言
进程(process)和线程(thread)是操作系统的基本概念。
- 进程(process)
进程:程序的一次执行,其内存空间是天然独立的。
进程是指计算机中正在运行的程序的实例。当一个程序被执行时,它会被加载到内存中,并在操作系统的控制下运行。每个正在运行的程序都会被赋予一个独特的标识符,称为进程标识符(PID),用于唯一标识该进程。
进程可以是后台服务、用户交互的应用程序或系统内部组件等。每个进程都拥有自己的内存空间、计算资源和执行环境。进程之间相互独立,彼此隔离,这样可以确保一个进程的崩溃或错误不会影响其他进程的正常运行。
操作系统通过进程管理(调度器,scheduler)来调度和控制进程的执行。它负责分配和管理计算资源,以确保各个进程能够按照一定的优先级和调度算法进行执行。通过进程管理,操作系统可以监控进程的状态、提供进程间通信的机制,并为应用程序提供统一的运行环境。
- 线程(thread)
线程:CPU的基本调度单位,其内存空间是天然共享的。
线程是进程中的执行单元。一个进程可以拥有多个线程,每个线程都是独立执行的,都有自己的程序计数器、堆栈和局部变量等。线程共享进程的资源,包括内存和文件句柄等。由于线程之间可以共享同一进程的内存空间,这使得线程之间的通信和数据共享更加高效,相比于多个独立进程,线程的切换开销更小。
线程的主要优点包括并发执行、提高系统性能、简化程序设计、实现任务的异步操作等。但需要注意的是,线程之间共享内存可能导致资源竞争和安全性问题,因此需要采取适当的同步机制来确保数据安全(防止并发修改,而导致的数据不一致)。
线程可以被用户创建和管理,也可以由操作系统自动创建和管理。在多线程编程中,开发人员可以创建并发执行的线程来提高程序的效率和响应性。操作系统也可以创建一些后台线程来执行管理任务,如处理用户输入、维护网络连接等。
操作系统对进程和线程的管理,有以下三点:
- 以多进程形式,允许多个任务同时运行;
- 以多线程形式,允许单个任务分成不同的部分运行;
- 提供协调机制,一方面防止进程之间产生冲突,另一方面允许线程之间共享资源;
1 并行与并发
- 并发:
宏观上同时进行,微观上交替进行——CPU资源共享,线程。 - 并行:
宏观上同时进行,微观上同时进行——进程。
2 多线程编程
要使用C语言实现多线程编程,我们需要引用#include <pthread.h>。
pthread.h是一个C语言的头文件,用于引入多线程编程相关的函数、数据结构和宏定义。其提供了一组函数和数据结构来支持POSIX线程库(即pthread)。通过使用pthread库,可以创建、同步和管理多个线程,使得程序可以并发执行,提高性能和效率。
通过包含pthread.h头文件,可以使用以下功能:
- 创建和管理线程:
pthread_create、pthread_exit、pthread_join等函数可用于创建线程、线程的退出和等待线程执行完毕。 - 线程同步:
pthread_mutex_t、pthread_cond_t等数据结构和相关函数提供了互斥锁、条件变量等机制,用于实现线程间的同步与互斥。 - 线程属性:
pthread_attr_t结构和相关函数用于设置和获取线程的属性,如栈大小、调度策略等。 - 取消和终止线程:
pthread_cancel、pthread_kill等函数用于发送取消信号或终止线程的执行。 - 线程局部存储:
pthread_key_t、pthread_getspecific、pthread_setspecific等函数和数据结构用于实现线程特定数据(Thread-Specific Data,TSD)。
3 进程间通讯
3.1 进程间通信的方法
进程间通信(IPC)是指不同进程之间传递信息(数据)的方式,可以通过以下几种方式进行进程间通信:
- 管道(pipe)
管道是一种半双工的通信方式,数据只能在一个方向上流动(例如,从父进程到子进程),一般是通过 pipe() 系统调用创建,数据有大小限制,只能在亲缘关系的进程间使用。
- 命名管道(FIFO)
命名管道是另一种半双工的通信方式,也有大小限制,但它可以在无亲缘关系的进程间建立通信。可以使用 mkfifo() 或 mknod() 系统调用创建。
- 共享内存(shared memory)
共享内存方式可以让两个或多个进程共享同一块物理内存,一个进程对共享内存的写入会影响到其他使用该内存区域的进程。常用的系统调用有 shmget()、shmat()、shmdt()、shmctl()。
- 消息队列(message queue)
消息队列是一种存放在内核中的消息链表,每个消息都有一个类型和一个数据部分,进程可以根据指定的类型进行接收。常用的系统调用有 msgget()、msgsnd()、msgrcv()、msgctl()。
- 信号量(semaphore)
信号量是一种计数器,用于实现进程间的互斥与同步。它可以保证在同一时刻只有一个进程访问一段共享资源,是一种低级别的同步原语。常用的系统调用有 semget()、semop()、semctl()。
- 套接字(socket)
套接字是一种通过网络进行进程间通信的方式,可以实现不同计算机之间的通信,基于TCP/IP协议进行传输,不限于同一主机。常用的系统调用有 socket()、bind()、listen()、accept()、connect()、send()、recv()。
3.2 套接字
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,是在计算机网络中用于在不同主机之间进行通信的一种方式。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
套接字可以看作是建立在传输层协议(如TCP或UDP)之上的一种抽象,它定义了一组规范和函数,用于创建网络连接、发送和接收数据,以及管理网络通信。
在编程中,套接字通常由一个IP地址和一个端口号组成,用于标识网络上的具体节点。通过套接字,应用程序可以建立网络连接,发送数据到远程主机,并接收来自远程主机的数据。套接字提供了一种面向连接或无连接的通信方式,具体取决于所使用的传输层协议。
在C/C++语言中,可以使用标准的网络编程库来进行socket开发,一般需要包含以下头文件:
#include <sys/socket.h> #include <arpa/inet.h>
3.2.1 socket.h
socket.h是C语言中的一个头文件,提供了一系列的函数api和数据结构,用于在网络编程中管理套接字(socket),包括创建、连接、绑定、发送和接收数据等操作。
/** * @brief 在使用 TCP socket 时,用于接受 client端 连接请求 * @param [IN] server端 socket 文件描述符 * @param [OUT] 用于存储 client端 的地址信息(IP地址和端口号) * @param [OUT] 用于存储 client端 的地址信息长度 * @return <0:出现了错误 * =0:处于阻塞状态,等待client连接 * >0:返回一个 socket 代表 client,用于和client通信(该socket会继承原client端 socket 的所有属性) */ int accept (int, struct sockaddr *__peer, socklen_t *); /** * @brief 在使用 TCP socket 时,用于接受 client端 连接请求 * @param [IN] server端 socket 文件描述符 * @param [OUT] 用于存储 client端 的地址信息(IP地址和端口号) * @param [OUT] 用于存储 client端 的地址信息长度 * @param [IN] 用于设置标志 * SOCK_NONBLOCK:将新的socket设置为非阻塞模式,非阻塞模式下 accept4() 函数会立即返回,不会阻塞程序执行 * SOCK_CLOEXEC: 将新的socket设置为在执行 exec 系列函数时自动关闭,避免了在 exec 后需要手动关闭套接字的麻烦 * @return <0:出现了错误 * =0:处于阻塞状态,等待client连接 * >0:返回一个 socket 代表 client,用于和client通信(该socket会继承原client端 socket 的所有属性) */ int accept4 (int, struct sockaddr *__peer, socklen_t *, int flags); /** * @brief 分配一个本地地址与socket进行绑定,使socket能够监听指定的端口并接受来自其他socket的连接请求 * @param [IN] server端 socket 文件描述符 * @param [IN] 一个指向包含了要绑定的本地地址信息的指针 * @param [IN] 本地地址信息长度 * @return * @note 地址信息包含 * sin_family:地址族(如 IPv4 或 IPv6) * sin_addr: IP地址 * sin_port: 端口号 */ int bind (int, const struct sockaddr *__my_addr, socklen_t __addrlen); /** * @brief 用于在client与server建立连接时,将 client socket 连接到指定的目标地址(server) * @param [IN] client端 socket 文件描述符 * @param [IN] 一个指向要连接的远程服务器的目标地址信息指针 * @param [IN] 目标地址信息长度 * @return 0: 与server端连接成功 * -1:连接失败,并通过设置全局变量 errno 来指示错误的具体原因 */ int connect (int, const struct sockaddr *, socklen_t); /** * @brief 获取与本地socket相连接的远程socket的主机信息,包括远程主机的 IP 地址和端口号 * @param [IN] 本地 socket 文件描述符 * @param [OUT] 用于存储远程主机的地址信息 * @param [OUT] 用于存储地址信息长度 * @return 0: 获取远程主机信息成功 * -1:获取远程主机信息失败,并通过设置全局变量 errno 来指示错误的具体原因 * @note 在调用 getpeername() 函数之前,必须已经建立了连接,否则该函数会失败并设置 errno 为 ENOTCONN。 * 可以在客户端 connect() 成功之后,或者在通过 accept() 函数接受客户端连接之后,获取远程主机的地址信息 */ int getpeername (int, struct sockaddr *__peer, socklen_t *); /** * @brief 通常在 server 端程序中使用,用来获取socket绑定的本地地址信息 * @param [IN] 本地 socket 文件描述符 * @param [OUT] 用于存储本地地址信息 * @param [OUT] 用于存储地址信息长度 * @return 0: 获取本地地址信息成功 * -1:获取本地地址信息失败,并通过设置全局变量 errno 来指示错误的具体原因 * @note 在调用 getsockname() 函数之前,必须已经调用了 bind() 函数将socket绑定到本地地址 * 否则该函数会失败并设置 errno 为 EINVAL */ int getsockname (int, struct sockaddr *__addr, socklen_t *); /** * @brief 用于将本地 socket 设置为监听状态,准备接受client的连接请求 * @param [IN] 本地 socket 文件描述符 * @param [IN] 指定发送连接请求的client的排队队列最大长度 * @return 0: 函数执行成功 * -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 */ int listen (int, int __n); /** * @brief 用于从已连接的 socket 接收数据,并将数据存储到缓冲区中 * @param [IN] socket 文件描述符 * @param [OUT] 一个指向接收数据的缓冲区的指针 * @param [IN] 指定接收缓冲区长度 * @param [IN] 标志,用于控制接收行为的特性(例如接收过程中阻塞或非阻塞) * @return -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * =0:表示连接已关闭 * >0:表示实际接收到的字节数 * @note recv() 函数是一个阻塞调用,即如果没有可用的数据来接收,它将一直阻塞直到有数据到达为止 * 如果需要进行非阻塞的接收操作,可以通过设置套接字为非阻塞模式或使用其他技术 */ ssize_t recv (int, void *__buff, size_t __len, int __flags); /** * @brief 用于从指定的套接字接收数据,并同时获取发送方的地址信息 * @param [IN] socket 文件描述符 * @param [OUT] 一个指向接收数据的缓冲区的指针 * @param [IN] 指定接收缓冲区长度 * @param [IN] 标志,用于控制接收行为的特性(例如接收过程中阻塞或非阻塞) * @param [OUT] 用于存储发送方的地址信息 * @param [OUT] 用于存储地址信息长度 * @return -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * =0:表示连接已关闭 * >0:表示实际接收到的字节数 * @note 该函数通常用于无连接的套接字,如 UDP 套接字。 * 在接收到数据时,可以获取发送方的地址信息,以便对消息进行回复或做进一步处理 * recvfrom() 函数是一个阻塞调用,即如果没有可用的数据来接收,它将一直阻塞直到有数据到达为止 * 如果需要进行非阻塞的接收操作,可以通过设置套接字为非阻塞模式或使用其他技术 */ ssize_t recvfrom (int, void *__buff, size_t __len, int __flags, struct sockaddr *__from, socklen_t *__fromlen); /** * @brief 用于从指定的套接字接收多部分数据,并将数据存储到指定的缓冲区中 * @param [IN] socket 文件描述符 * @param [OUT] 一个指向用于接收数据和其他相关信息的缓冲区指针 * @param [IN] 标志,用于控制接收行为的特性(例如接收过程中阻塞或非阻塞) * @return -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * =0:表示连接已关闭 * >0:表示实际接收到的字节数 * @note recvmsg() 函数支持从多个缓冲区中接收数据 * 且可以同时获取发送方的地址信息和与接收相关的附加信息(如 IP 头或控制消息) */ ssize_t recvmsg(int s, struct msghdr *msg, int flags); /** * @brief 用于向已连接的套接字发送数据 * @param [IN] socket 文件描述符 * @param [IN] 指向要发送数据的缓冲区的指针 * @param [IN] 要发送的数据长度 * @param [IN] 标志,用于控制发送行为的特性(例如发送过程中阻塞或非阻塞) * @return -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * >0:表示实际发送的字节数 */ ssize_t send (int, const void *__buff, size_t __len, int __flags); /** * @brief 用于向指定的套接字发送多部分数据 * @param [IN] socket 文件描述符 * @param [IN] 指向要发送的数据和发送相关的其他信息的缓冲区的指针 * @param [IN] 标志,用于控制发送行为的特性(例如发送过程中阻塞或非阻塞) * @return -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * >0:表示实际发送的字节数 */ ssize_t sendmsg(int s, const struct msghdr *msg, int flags); /** * @brief 用于通过未连接的套接字向指定目标发送数据 * @param [IN] socket 文件描述符 * @param [IN] 指向要发送数据的缓冲区的指针 * @param [IN] 要发送的数据长度 * @param [IN] 标志,用于控制发送行为的特性(例如发送过程中阻塞或非阻塞) * @param [IN] 一个指向目标地址信息的指针 * @param [IN] 目标地址信息长度 * @return -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * >0:表示实际发送的字节数 * @note 与 sendmsg() 类似,sendto() 函数也是一个阻塞调用 * 如果发送缓冲区已满,它将一直阻塞直到有足够的空间来发送数据 * 如果需要进行非阻塞的发送操作,可以通过设置套接字为非阻塞模式或使用其他技术 */ ssize_t sendto (int, const void *, size_t __len, int __flags, const struct sockaddr *__to, socklen_t __tolen); /** * @brief 用于设置指定套接字的选项 * @param [IN] socket 文件描述符 * @param [IN] 选项的级别(协议层或套接字层) * @param [IN] 要设置的选项名称,可以是各种选项常量 * @param [IN] 一个指向设置选项值的缓冲区的指针 * @param [IN] 选项值的长度 * @return =0:调用成功 * -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * @note 通过 setsockopt() 函数可以设置各种不同的套接字选项,用于控制套接字的行为和特性 * 套接字选项的可用值和具体行为取决于选项的级别和套接字类型。 * 在使用 setsockopt() 函数之前,应该查阅相关文档以了解每个选项的作用和适用条件 */ int setsockopt (int __s, int __level, int __optname, const void *optval, socklen_t __optlen); /** * @brief 用于获取指定套接字的选项值 * @param [IN] socket 文件描述符 * @param [IN] 选项的级别(协议层或套接字层) * @param [IN] 要设置的选项名称,可以是各种选项常量 * @param [OUT] 一个指向保存选项值的缓冲区的指针 * @param [OUT] 一个指向保存选项值长度的变量的指针 * @return =0:调用成功 * -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * @note 通过 getsockopt() 函数可以获取各种不同的套接字选项的值,例如,可以获取套接字的超时时间、缓冲区大小等 */ int getsockopt (int __s, int __level, int __optname, void *__optval, socklen_t *__optlen); /** * @brief 用于关闭一个已经打开的套接字,即停止套接字进行输入和输出操作 * @param [IN] 要关闭的 socket 文件描述符 * @param [IN] 指示关闭方式的标志 * SHUT_RD:停止套接字的读取操作,该套接字将无法从接收缓冲区中读取数据 * SHUT_WR:停止套接字的写入操作,该套接字将无法向发送缓冲区中写入数据 * SHUT_RDWR:同时停止套接字的读取和写入操作 * @return =0:调用成功 * -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 */ int shutdown (int, int); /** * @brief 用于创建一个新的socket * @param [IN] 套接字的地址族,指定了套接字的通信域 * AF_INET:IPv4 地址族 * AF_INET6:IPv6 地址族 * AF_UNIX:本地通信 * AF_LOCAL:本地通信 * @param [IN] 套接字的类型,用于指定套接字的通信方式和连接类型 * SOCK_STREAM:流套接字,提供面向连接的、可靠的、基于字节流的通信,如 TCP 套接字 * SOCK_DGRAM:数据报套接字,提供无连接的、不可靠的、固定长度的消息传输,如 UDP 套接字 * SOCK_RAW:原始套接字,用于直接访问底层网络层协议 * @param [IN] 指定使用的协议 * 0:自动选择合适的协议 * IPPROTO_TCP:TCP 协议 * IPPROTO_UDP:UDP 协议 * @return 返回一个socket文件描述符 * -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 */ int socket (int __family, int __type, int __protocol); /** * @brief 用于确定套接字是否处于带外标记(Out-of-band mark)的位置 * @param [IN] socket 文件描述符 * @return =0:socket当前不处于带外标记的位置 * >0:socket当前处于带外标记的位置 * -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 */ int sockatmark (int __fd); /** * @brief 用于创建一对相互连接的套接字 * @param [IN] 套接字的地址族,指定了套接字的通信域 * @param [IN] 套接字的类型,用于指定套接字的通信方式和连接类型 * @param [IN] 指定使用的协议 * @param [IN] 存储创建的两个套接字的文件描述符, * __fds[0] 表示第一个套接字的文件描述符, * __fds[1] 表示第二个套接字的文件描述符 * @return =0:调用成功 * -1:函数执行失败,并通过设置全局变量 errno 来指示错误的具体原因 * @note 创建的一对套接字可以用于在两个进程之间进行全双工的通信,类似于管道。 * 通过一对套接字可以进行双向的数据传输,可以在一个套接字上发送数据,接收数据则需要在另一个套接字上进行 */ int socketpair (int __domain, int __type, int __protocol, int __fds[2]); /** * @brief 用于通过服务名(Service Name)和协议名(Protocol Name)获取服务信息的数据 * @param [IN] 字符串类型的服务名(例如:"http") * @param [IN] 字符串类型的协议名(例如:"tcp") * @return 指向包含了获取到的服务信息的指针 * @note 该函数的返回值指向的是静态分配的内存,如果要在多次调用之间保存数据,请将获取到的数据复制到其他内存区域中存储 */ struct servent *getservbyname (const char *__name, const char *__proto);
3.2.2 inet.h
inet.h是C语言中的一个标准库头文件,它提供了一些网络编程中与地址转换相关的函数和宏定义,用于在IPv4和IPv6地址之间进行转换,以及在网络地址和主机字节序之间进行转换。
/** * @brief 用于从一个有效的 IPv4 地址的二进制网络字节序整数中获取其主机部分(LAN 地址) * @param [IN] 一个有效的 IPv4 地址的二进制网络字节序整数 * @return IPv4 地址的主机部分(LAN 地址) */ in_addr_t inet_lnaof (struct in_addr); /** * @brief 用于从一个有效的 IPv4 地址的二进制网络字节序整数中获取其网络部分(WAN 地址) * @param [IN] 一个有效的 IPv4 地址的二进制网络字节序整数 * @return IPv4 地址的网络部分(WAN 地址) */ in_addr_t inet_netof (struct in_addr); /** * @brief 根据给定的网络号和主机号创建一个完整的 IPv4 地址 * @param [IN] 网络部分(WAN 地址)的值 * @param [IN] 主机部分(LAN 地址)的值 * @return 对应的 IPv4 地址的二进制网络字节序整数 */ struct in_addr inet_makeaddr (unsigned long , unsigned long); /** * @brief 将IPv4地址的点分十进制表示转换为二进制网络字节序的整数表示 * @param [IN] 一个有效的 IPv4 地址字符串 * @param [OUT] 对应的二进制网络字节序的整数 * @return 1:成功转换并存储了 IPv4 地址 * 0:地址不合法或转换失败 */ int inet_aton (const char *, struct in_addr *); /** * @brief 将IPv4地址的二进制网络字节序的整数表示转换为点分十进制表示 * @param [IN] 一个有效的 IPv4 地址网络字节序 * @return 对应的点分十进制表示的字符串 * @note 该接口为线程不安全的,因为它是基于静态缓冲区生成字符串的,因此不应该在多线程环境下同时调用它多次 * 由于返回值是指向静态缓冲区的指针,因此必须在下一次对该函数进行调用之前使用或复制返回的字符串值 */ char *inet_ntoa (struct in_addr); /** * @brief 将IPv4或IPv6地址的点分十进制字符串表示转换为对应的二进制网络字节序的整数表示 * @param [IN] 地址族,可能的取值为 AF_INET 或 AF_INET6,分别用于 IPv4 和 IPv6 地址转换 * @param [IN] 一个指向代表 IPv4 或 IPv6 地址的字符串的指针 * @param [OUT] 一个指向用于存放二进制数据的缓冲区的指针 * @return 1:转换成功 * 0:转换失败 */ int inet_pton (int, const char *, void *); /** * @brief 将IPv4或IPv6地址的网络字节序表示转换为对应的点分十进制字符串表示 * @param [IN] 地址族,可能的取值为 AF_INET 或 AF_INET6,分别用于 IPv4 和 IPv6 地址转换 * @param [IN] 一个指向包含二进制网络字节序 IP 地址的内存块的指针 * @param [OUT] 一个指向缓冲区的指针,它将用于存放转换得到的点分十进制字符串格式的 IP 地址 * @param [OUT] 缓冲区的大小 * @return 转换成功:返回指向缓冲区的指针 * 转换失败:返回 NULL */ const char *inet_ntop (int, const void *, char *, socklen_t);
3.2.3 字节序转换
在大多数情况下,网络协议都采用大端字节序(即最高位字节存储在最低的地址处),而主机字节序则因操作系统和处理器的不同而有所差别。在使用网络协议进行通信时,为了避免数据传输过程中因字节序不一致而导致的错误,需要使用以下函数帮助实现主机字节序和网络字节序之间的相互转换。
/** * @brief 用于将一个32位的网络字节序转换为主机字节序 * @param * @return */ uint32_t ntohl(uint32_t); /** * @brief 用于将一个16位的网络字节序转换为主机字节序 * @param * @return */ uint16_t ntohs(uint16_t); /** * @brief 用于将一个32位的主机字节序转换为网络字节序 * @param * @return */ uint32_t htonl(uint32_t); /** * @brief 用于将一个16位的主机字节序转换为网络字节序 * @param * @return */ uint16_t htons(uint16_t);