文章目录
1 进程通信
1.1 匿名管道
父子进程间通信
// 管道的数据是单向流动的:
// 操作管道的是两个进程, 进程A读管道, 需要关闭管道的写端, 进程B写管道, 需要关闭管道的读端
// 如果不做上述的操作, 会对程序的结果造成一些影响, 对管道的操作无法结束
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
// 1. 创建匿名管道, 得到两个文件描述符
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe");
exit(0);
}
// 2. 创建子进程 -> 能够操作管道的文件描述符被复制到子进程中
pid_t pid = fork();
if(pid == 0)
{
// 关闭读端
close(fd[0]);
// 3. 在子进程中执行 execlp("ps", "ps", "aux", NULL);
// 在子进程中完成输出的重定向, 原来输出到终端现在要写管道
// 进程打印数据默认输出到终端, 终端对应的文件描述符: stdout_fileno
// 标准输出 重定向到 管道的写端
dup2(fd[1], STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
perror("execlp");
}
// 4. 父进程读管道
else if(pid > 0)
{
// 关闭管道的写端
close(fd[1]);
// 5. 父进程打印读到的数据信息
char buf[4096];
// 读管道
// 如果管道中没有数据, read会阻塞
// 有数据之后, read解除阻塞, 直接读数据
// 需要循环读数据, 管道是有容量的, 写满之后就不写了
// 数据被读走之后, 继续写管道, 那么就需要再继续读数据
while(1)
{
memset(buf, 0, sizeof(buf));
int len = read(fd[0], buf, sizeof(buf));
if(len == 0)
{
// 管道的写端关闭了, 如果管道中没有数据, 管道读端不会阻塞
// 没数据直接返回0, 如果有数据, 将数据读出, 数据读完之后返回0
break;
}
printf("%s, len = %d\n", buf, len);
}
close(fd[0]);
// 回收子进程资源
wait(NULL);
}
return 0;
}
1.2 有名管道
写管道的进程
#include <fcntl.h>
#include <sys/stat.h>
int main()
{
// 1. 创建有名管道文件
int ret = mkfifo("./testfifo", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(0);
}
printf("管道文件创建成功...\n");
// 2. 打开管道文件
// 因为要写管道, 所有打开方式, 应该指定为 O_WRONLY
// 如果先打开写端, 读端还没有打开, open函数会阻塞, 当读端也打开之后, open解除阻塞
int wfd = open("./testfifo", O_WRONLY);
if(wfd == -1)
{
perror("open");
exit(0);
}
printf("以只写的方式打开文件成功...\n");
// 3. 循环写管道
int i = 0;
while(i<100)
{
char buf[1024];
sprintf(buf, "hello, fifo, 我在写管道...%d\n", i);
write(wfd, buf, strlen(buf));
i++;
sleep(1);
}
close(wfd);
return 0;
}
读管道的进程
#include <fcntl.h>
#include <sys/stat.h>
int main()
{
// 1. 打开管道文件
// 因为要read管道, so打开方式, 应该指定为 O_RDONLY
// 如果只打开了读端, 写端还没有打开, open阻塞, 当写端被打开, 阻塞就解除了
int rfd = open("./testfifo", O_RDONLY);
if(rfd == -1)
{
perror("open");
exit(0);
}
printf("以只读的方式打开文件成功...\n");
// 2. 循环读管道
while(1)
{
char buf[1024];
memset(buf, 0, sizeof(buf));
// 读是阻塞的, 如果管道中没有数据, read自动阻塞
// 有数据解除阻塞, 继续读数据
int len = read(rfd, buf, sizeof(buf));
printf("读出的数据: %s\n", buf);
if(len == 0)
{
// 写端关闭了, read解除阻塞返回0
printf("管道的写端已经关闭, 拜拜...\n");
break;
}
}
close(rfd);
return 0;
}
2 线程同步
2.1 信号
Linux 中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,由于信号的结构简单所以天生不能携带很大的信息量,但是信号在系统中的优先级是非常高的。
2.2 c线程同步方式
互斥锁
// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
读写锁
#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 这个函数可以有效的避免死锁
// 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
条件变量
#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
信号量
#include <semaphore.h>
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数
int sem_destroy(sem_t *sem);
// 参数 sem 就是 sem_init() 的第一个参数
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);
// 调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);
// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t *sem, int *sval);
3 套接字
3.1 概念
BSD Socket 提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
#include <arpa/inet.h>
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int
// 这套api主要用于 网络通信过程中 IP 和 端口 的 转换
// 将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);
// 将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);
3.2 IP 地址转换
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
3.3 sockaddr 数据结构
// 在写数据的时候不好用
struct sockaddr {
sa_family_t sa_family; // 地址族协议, ipv4
char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
struct in_addr
{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
3.4 套接字函数
// 创建一个套接字
int socket(int domain, int type, int protocol);
// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 给监听的套接字设置监听
int listen(int sockfd, int backlog);
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssiz