linux系统编程
常识
unistd.h
<unistd.h> UNIX standard header
主要就是UNIX系统级接口:进程,IO, 文件描述符,系统调用
- 类型:
pid_t, uid_t, size_t, ssize_t, off_t, intptr_t, stdin-0,..
- 进程:
fork, exec*, getpid, _exit, getppid
- 文件与IO:
open, read,.. fsync, pipe
- 目录:
rmdir, chdir, getcwd
- 用户:
getuid..
- 系统控制:
sleep, alarm, pause, sysconf
文件
常见权限数字:
0600:创建者可读写
0644:自己读写,别人只读
0666:所有人读写
► struct stat存储文件或目录状态信息。
#include <sys/stat.h>
struct stat {
dev_t st_dev; // 文件所在设备的 ID
ino_t st_ino; // 文件的 inode 编号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 文件的硬链接数
uid_t st_uid; // 文件所有者的用户 ID
gid_t st_gid; // 文件所有者的组 ID
dev_t st_rdev; // 如果文件是设备文件,则是设备类型
off_t st_size; // 文件的字节数
blkcnt_t st_blocks; // 文件占用的块数(每块 512 字节)
time_t st_atime; // 上次访问时间
time_t st_mtime; // 上次修改时间
time_t st_ctime; // 上次状态改变时间
….
};
📌用法1:通过stat()获取状态。如文件大小、修改时间
stat("example.txt", &st)
fstat()://获取已经打开的文件描述符对应文件的状态,避免路径解析的开销。
📌用法2:文件类型判断宏
if (S_ISDIR(st.st_mode)) {
printf("It is a directory.\n");
} else if (S_ISREG(st.st_mode)) {
printf("It is a regular file.\n");
}
FILE对象相对fd, write
这些系统调用多了缓冲区。
fopen
"r+"
读写(不清空) ;"w+"
读写(清空) ; "w+b"
读写二进制,清空
fwrite
fwrite(..size, n,..)
初衷:就是为了自动计算数组结构体等的写入。
char buf[1024];
fwrite(buf, 1, 1024, fp);
DB db[5];
fwrite(db, sizeof(DB), 5, fp);
unlink
rm
命令实现的系统调用,删除文件。实际上是接触访问链。(linux文件系统机制Innode)
ftruncate
指定文件长度
进程
for (size_t i = 0; i < 10; i++)
{
pid_t pid;
if ((pid = fork()) == 0) {
worker_process_func();
exit(0); // 🦀必须exit显示退出,否则后面就是子进程fork
} else if (pid < 0)
{
printf("Fork failed\n");
}
}
for (size_t i = 0; i < 10; i++)
{
/* code */
wait(NULL); // 🦀父进程必须显示处理子进程退出状态:sigchild信号或者wait。不然就会称为僵尸进程。即便父进程关了,子进程的父亲是1了,称为孤儿进程。
}
进程关系
会话
建立一个新会话setsid()
- 如果进程是进程组组长,出错。 一般先调用fork,然后使父进程终止。子进程即继承进程组ID,但不是进程组组长。
- 如果进程不是进程组组长,则创建新会话成功。具体地:
- 该进程成为会话首进程。
- 该进程成为一个新进程组的组长。
- 该进程没有控制终端。
守护进程
守护进程
系统守护进程
cron:定期日期实践执行。
初始化守护进程
if((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if(pid != 0) /* parent */
exit(0);
setsid();
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if(sigaction(SIGHUP, &sa, NULL) < 0) {
err_quit("%s : can't ignore SIGHUP", cmd);
}
- 创建一个会话:(a)新进程会直接成为会话的首进程。(b)成为一个新进程组的组长进程。(c)没有控制终端
- 信号处理 :确保后续守护进程不会持有终端,且不受控制终端影响。
SIGHUP信号会在终端关闭时候发送给进程,这里SIG_IGN忽略处理,使得终端关闭不影响进程运行。
sigemptyset(&sa.sa_mask); 表明处理SIGHUP信号时不会阻塞其他信号。
if((pid = fork()) < 0) {
err_quit(“%s : can’t fork.”, cmd);
} elseif(pid != 0) {
/*parent */
exit(0);
}
第一次exit, fork 守护进程仍然可能重新获得终端, 第二次exit、fork保证进程不会成为会话首进程,确保一定不会获得终端
fd0 = open(“/dev/null”, O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
- 文件描述符 attach 到 /dev/null :守护进程不与终端输入输出交互。
出错记录
大多数守护进程使用syslog记录日志消息。syslogd守护进程处理日志消息。syslog不生产数据报,用户进程或守护进程需要按照日志消息格式发送到套接字。
产生日志消息的方法:
(1)大多数守护进程通过syslog(3)函数产生日志消息,消息会被发送到UNIX域数据报套接字 /dev/log 上。
(2)任何用户进程可以将日志消息发送到网络套接字udp端口514。
用法:
openlog(“process name”, LOG_PID, LOG_LPR);
syslog(LOG_ERR, “open error for %s: %m”, filename);
- 设置日志类型, 位屏蔽
- 记录日志消息
syslog 为方法一,与/dev/log套接字通信。
进程根目录为/ , 如果chroot(newroot)改变进程根目录,那么根路径从newroot开始
查看syslog,tail -f /var/log/syslog
单实例守护进程
如网络服务,需要任一时刻只有一个实例运行,避免发生资源冲突,如端口冲突。
实现方法:任一时刻只运行守护进程一个副本。 通过文件和记录锁机制来实现,每个守护进程有一个固定名字的文件,只允许这个文件有一个写锁,创建写锁失败,即表示不能不能创建守护进程。在守护进程终止时,会自动清理锁。
案例:创建守护进程时调用,试图创建一个文件,将pid写入。如果返回1,表明文件有锁,创建失败。
##defineLOCKFILE “/var/run/daemon.pid”
##defineLOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) |
externintlockfile(int);
intalready_running(void)
{
intfd;
charbuf[16];
fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE); |
if(fd < 0) {
syslog(LOG_ERR, “can’t open %s: %s”, LOCKFILE, strerror(errno));
exit(1);
}
if(lockfile(fd) < 0) {
if(errno == EACCES | errno == EAGAIN) { |
close(fd);
return1;
}
syslog(LOG_ERR, “can’t lock %s: %s”, LOCKFILE, strerror(errno));
exit(1);
}
ftruncate(fd, 0);
sprintf(buf, “%ld”, (long)getpid());
write(fd, buf, strlen(buf) + 1);
return0;
}
ftruncate(fd, 0); 将文件长度截断为0,清空文件内容。
errno == EACCES | errno == EAGAIN ;表示权限不足或者操作失败。 |
LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) : 文件所有者读写权限、所属组读权限、其他用户读权限。 |
守护进程的惯例
- 如果守护进程使用文件锁,文件格式为/var/run/守护进程name.pid, 如:/var/run/crond.pid 内容为pid
- 如果守护进程支持配置,文件格式为/etc/守护进程name.conf
- 守护进程可以用命令行启动。通常由 /etc/init.d/*系统初始化脚本初始启动,当守护进程终止时,应当自动重启,自动重启通过 /etc/inittab 包括respawn记录项,init就会自动重启守护进程。
- 重读配置文件方法:kill重启。另外方法,守护进程去捕获SIGHUP信号,因为没有控制终端,可以安全重复自己使用。
单线程守护进程:
获取当前文件名
// 获取当前文件名
char*cmd;
if((cmd = strchr(argv[0], ‘/’)) == NULL)
cmd = argv[0];
else
cmd++;
设置SIGHUP信号处理为默认,且不阻塞任何信号。
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if(sigaction(SIGHUP, &sa, NULL) < 0) {
err_quit(“%s : can’t restore SIGHUP default”);
}
主线程阻塞所有信号,专注运行, 创建一个线程来处理信号。
sigfillset(&mask);
if((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0) {
err_exit(err, “SIG BLOCK error”);
}
err = pthread_create(&tid, NULL, thr_fn, 0);
if(err != 0)
err_exit(err, “can’t create thread”);
处理信号的线程:
void*thr_fn(void*arg)
{
interr, signo;
for(;;) {
err = sigwait(&mask, &signo);
if(err != 0) {
syslog(LOG_ERR, "sigwait failed.");
exit(1);
}
switch(signo) {
caseSIGHUP:
syslog(LOG_INFO, "Re-reading configuration file");
reread();
break;
caseSIGTERM:
syslog(LOG_INFO, "got SIGTERM; exiting");
exit(0);
default:
syslog(LOG_INFO, "unexpected signal %d\n", signo);
}
}
return0;
}
sigwait:设置要等待的阻塞信号,与pthread_sigmask一致,表示等待所有被阻塞的信号。线程挂起,直到其中有被阻塞信号触发。
SIGTERM和SIGHUP默认动作都是终止线程,这里作为替代,不会终止线程。
常见守护进程
- /usr/sbin/cron:这是定时任务调度器 cron 的进程。
- /usr/sbin/rsyslogd:这是系统日志服务 rsyslogd 的进程,它负责收集、处理和转发系统日志。
- /usr/lib/snapd/snapd:这是 snapd 守护进程,它是 Snap 包管理器的后台服务。
- /usr/sbin/xinetd:这是 xinetd 守护进程,它是一个基于 TCP Wrappers 的超级服务器,用于管理和启动其他网络服务。
- /usr/libexec/packagekitd:这是 PackageKit 守护进程,它是一个软件包管理服务,用于管理软件包的安装、更新等操作。
- /usr/libexec/polkitd:这是 PolicyKit 守护进程,它是一个系统权限管理服务,用于控制用户对系统资源的访问权限。
场景
- 守护进程查看是否有登录名。A:没有。因为没有控制终端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <syslog.h>
extern void daemonize(constchar*cmd);
int main(void)
{
daemonize("login daemon");
char*file_name = "/var/log/mydaemon.log";
FILE *file = fopen(file_name, "w");
syslog(LOG_INFO, "pid : %ld", (long)getpid());
if(file == NULL) {
syslog(LOG_ERR, "file open failed.");
exit(1);
}
char*name = getlogin();
if(name == NULL) {
syslog(LOG_ERR, "getlogin name failed.");
exit(1);
}
fprintf(file, "%s\n", name);
fclose(file);
return0;
}
写时复制COW
在fork子进程时候,父子进程共享内存页,这些共享内存页标记只读,父进程或者子进程修改内存页,触发写时复制。
子进程尝试修改共享内存页,操作系统触发页故障,为子进程创建该页副本再修改,且只属于子进程,只对子进程可见。
同理,父进程一样。
比如:初始状态。
共享内存页: [A, B, C]
内容 : [“foo”, “bar”, “baz”]
子进程修改B页的bar为bor。
父亲内存页: [A, B, C]
子内存页:【A, B’ , C】
父内容:【A, B, C】
子内容 : [“foo”, “bor”, “baz”]
redis中案例,写时复制时候禁止内存修改
核心思想:
- 需要修改时进行实际数据拷贝
- 不修改时共享同内存区域
高级IO
IO多路转接
while((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
if(write(STDOUT_FILENO, buf, n) != n)
err_sys("wrtite error");
需求:从两个描述符读, 可能会阻塞在一个读IO上,导致另外的描述符也不能读。也不知道到底哪个输入会得到数据
IO多路转接:构造一个感兴趣的描述符表,调用一个函数,直到其中一个描述符已经准备好,该函数才返回。 poll , select, pselect , 返回后 进程会得知哪些描述符已经准备好。 然后就可以正确调用IO read , write
Select
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd, &rset);
FD_SET(STDIN_FILENO, &rset);
参数 | 说明 |
---|---|
nfds | 表示三个描述符集最大值加1. |
readfds, writefds | 表示感兴趣的 读写。 每个描述符为一位。 |
exceptfds | 监听异常事件(如 OOB 数据) |
timeout | 超时控制,决定 select 阻塞时间,NULL 表示无限等待 |
返回值 | -1表示出错, 0表示描述符都没有准备好(读集合中读不会阻塞,写集合写不会阻塞) 大于0为准备好的描述符个数 |
Fd_set操作
文件描述符集合,存储需要监听的文件描述符。
宏 | 作用 |
---|---|
FD_ZERO(&set) | 清空 fd_set 集合 |
FD_SET(fd, &set) | 将 fd 添加到 fd_set |
FD_CLR(fd, &set) | 从 fd_set 中移除 fd |
FD_ISSET(fd, &set) | 检查 fd 是否在 fd_set 中(即是否准备好) |
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
int main() {
fd_set read_fds; // 可读事件监听集合
struct timeval timeout;
int fd = STDIN_FILENO; // 监听标准输入(键盘输入)
FD_ZERO(&read_fds); // 清空集合
FD_SET(fd, &read_fds); // 将标准输入加入监听集合
timeout.tv_sec = 5; // 设置超时 5 秒
timeout.tv_usec = 0;
printf("等待输入,5 秒后超时...\n");
int ret = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select 出错");
return 1;
} else if (ret == 0) {
printf("超时,无输入\n");
} else {
if (FD_ISSET(fd, &read_fds)) {
char buffer[100];
read(fd, buffer, sizeof(buffer));
printf("输入内容:%s\n", buffer);
}
}
return 0;
}
要点
- select 会修改 fd_set,需要每次重新设置。(因为会清除未发生变化的描述符)
- timeout每次也要重新设置
- 最多能监听102个描述符
epoll
-
事件驱动:注册的fd,不会每次遍历所有fd。而是只关心发生变化的fd,epoll_wait也只返回有事件的fd,而不是select需要遍历
-
支持边缘触发ETL模式:减少重复通知
用法:
- 创建epoll实例:epoll_create1
#include <sys/epoll.h>
int epfd = epoll_create1(0); // 推荐使用 epoll_create1
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
- 添加、修改、删除 fd:epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd: epoll实例
- op:操作
- EPOLL_CTL_ADD → 添加 fd
- EPOLL_CTL_MOD → 修改 fd监听事件
- EPOLL_CTL_DEL → 删除fd
- fd:要监听的文件描述符
- event:要监听的epoll事件 epoll_event:表示一个fd上的事件 存储监听的事件类型和与之关联的fd struct epoll_event { uint32_t events; // 事件类型(EPOLLIN、EPOLLOUT、EPOLLERR、EPOLLET) epoll_data_t data; // 存储用户数据(通常是文件描述符) }; 事件类型:读、写、错误、ET模式 ET模式:只在fd发生状态改变时候通知触发。默认是LT模式
- LT:只要fd可读/可写,每次wait都会返回该fd
- ET:只在状态改变时候通知。 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
- ptr常指向一个数据结构体,当fd不够满足需求适合使用
- fd存储描述符 示例:给fd注册监听可读事件。 struct epoll_event event; event.events = EPOLLIN; // 监听可读事件 event.data.fd = socket_fd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event) == -1) { perror(“epoll_ctl: EPOLL_CTL_ADD”); exit(EXIT_FAILURE); }
- 等待事件 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数 | 作用 |
---|---|
epfd: | epoll 实例的文件描述符。 |
events: | 用于存储触发的事件。 |
maxevents: | 最多返回的事件数量。 |
timeout: | -1 → 阻塞等待 0 → 立即返回 >0 → 等待超时时间(毫秒) |
返回值 | > 0:触发的事件数量,events 数组里存有 n 个就绪的 fd; 0:超时,无事件发生; -1:错误,errno 设定错误类型。 |
示例:wait返回有事件的fd的event
比如监听了 fd 1,3,4,5 。 其中1,4有事件,那么nfds返回2. events[0].data.fd=1, events[1].data.fd=2。即返回有事件fd的event,填充进events数组
问题
accpet后 epoll_ctl常出现 no such file 问题,不能epool fd。
就是对方accept后瞬间有关闭了。瞬间连接-断开,更多是正常现象(健康检查/探测/测试),小概率是异常现象。服务器端必须容忍。
client connect()
成功只是TCP的三次握手完成。
真正连接是否健康,要通过后续的 read
/write
感知。 比如发送ping,
会一直阻塞在recv上,可以设置超时重试
如果对端正常关闭,epoll会短时内一直一直就绪。
记录锁
多个人同时编辑一个文件,结果如何?
一般的,会取决于写该文件的最后一个进程。但是对于数据库,只能一个写。
记录锁:一个进程读或者修改文件某个区域,阻止其他进程同时修改这一区域。
int fcntl(int fd, int cmd, struct flock *ptr);
- cmd : F_GETLK 测试是否能建立一把锁(很少用)。F_SETLK加锁。F_SETLKW阻塞式加锁,即如果文件已经加锁,该进程就会休眠阻塞。
- flock : 表明锁类型(共享读锁、独占写锁、解锁一个区域)。加解锁区域。相关进程PID。
加解锁时,会组合或分裂区域。
案例:请求和释放一把锁:
#define read_lock(fd, offset, whence, len) lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
#define readw_lock(fd, offset, whence, len) lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))
#define write_lock(fd, offset, whence, len) lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
#define writew_lock(fd, offset, whence, len) lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
#define un_lock(fd, offset, whence, len) lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))
int lock_reg(int fd, in tcmd, int type, off_t offset, int whence, off_t len)
{
struct flock lock;
lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK*/
lock.l_start = offset;
lock.l_whence = whence;
lock.l_len = len; /*##bytes (o means to EOF)*/
return fcntl(fd, cmd, &lock);
}
线程
使用
pthread_create
pthread_join
阻塞等待某个线程结束pthread_exit
线程主动退出pthread_self
获取线程IDpthread_detach
线程分离
主要概念
-
分离状态:主线程不需要线程的终止状态时候,即可将其分离。比如网络线程。
pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
-
优先级:默认是0
.线程控制
同步属性:
互斥量属性:
默认属性初始化:pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;, 或者pthread_mutex_init(&mutex, NULL)
非默认属性:pthread_mutexattr_init(attr) . pthread_mutex_init(&mutex, &attr) 先初始化设置互斥量属性,再用属性来初始化互斥量
- 类型属性 PTHREAD_MUTEX_RECURSIVE: 互斥量通过计数来加锁解锁。
内存管理
操作系统提供了mmap(分配内存)、sbrk(扩缩进程堆空间)等系统调用申请内存,但是应用层一般不会直接调用,原因有二:
- 系统调用提供的内存粒度太大,常常是一页4K.
- 频繁系统调用会增大开销。
现代应用层自己的内存分配器都是维持一片内存池,进行内存管理。
malloc的底层是glibc库的ptmalloc函数。
mmap:
将文件或内存区域映射到进程的虚拟地址空间,也可以用来直接分配内存。
含义:
- 虚拟地址空间:就是程序自己内存地址空间。
- 映射:可以将一个文件或者一块内存映射到自己的程序内存,程序就可以按照访问内存的方式访问,而不需要自己去读取。
例子:
有一个大数据库文件,程序即可通过mmap将该文件映射到自己的内存内,直接修改文件,而不需要每次还得打开读取。
网络编程
- 地址相关结构体
结构 | 场景 | 注意 |
---|---|---|
struct sockaddr { sa_family_t sa_family; // 地址族,如 AF_INET、AF_INET6 char sa_data[14]; // 地址数据 }; | 通用地址 | 不能直接用于bind,需要转为具体ipv4/6 |
struct sockaddr_in { sa_family_t sin_family; // AF_INET in_port_t sin_port; // 端口号(16 位),需要转换 struct in_addr sin_addr; // IPv4 地址(32位),需转换 }; | ipv4地址 | - 端口号和ip必须经过转换才能设置。htons和inet_pton - 只能用于IPV4 |
struct sockaddr_in6 { sa_family_t sin6_family; // AF_INET6 in_port_t sin6_port; // 端口号(16 位),需要转换 struct in6_addr sin6_addr; // IPv6 地址 }; | ipv6地址 | |
struct addrinfo { int ai_flags; // 选项,如 AI_PASSIVE(用于 bind()) int ai_family; // 地址族(AF_INET / AF_INET6 / AF_UNSPEC) int ai_socktype; // 套接字类型(SOCK_STREAM / SOCK_DGRAM) int ai_protocol; // 协议(通常为 0) size_t ai_addrlen; // 地址结构体大小 struct sockaddr *ai_addr; // 指向 sockaddr 结构体 char *ai_canonname; // 规范主机名(可选) struct addrinfo *ai_next; // 指向下一个 addrinfo }; | 解析域名返回地址表,包含IPV4,IPV6。通常,用这些地址建立socket连接。 | - 与sockaddr不同,可以直接进行兼容编程。 - 用于getaddrinfo()解析主机,获取ip |
struct sockaddr_storage | 给足够大的空间,IPV4/6都可以通过这个存储。通常,用这个存储accept等得到的地址 |
- 套接字函数
函数 | 含义 | 注意 |
---|---|---|
int socket(int domain, int type, int protocol); | ||
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); | ||
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); | ||
ssize_t send(int sockfd, const void *buf, size_t len, int flags); | ||
ssize_t recv(int sockfd, void *buf, size_t len, int flags); | ||
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); | UDP 发送数据 | |
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); | UDP 接收数据 | |
close(sockfd); |
- 其他辅助函数
函数 | 含义 | 注意 |
---|---|---|
inet_pton(AF_INET, “127.0.0.1”, &addr.sin_addr); | IP 地址转换:字符串 -> 二进制 | |
inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str)); | IP 地址转换:二进制 -> 字符串 | |
uint32_t htonl(uint32_t hostlong); | 字节序转换: 主机 -> 网络字节序(32 位) | |
uint16_t htons(uint16_t hostshort); | 字节序转换:主机 -> 网络字节序(16 位) | |
uint32_t ntohl(uint32_t netlong); | 字节序转换:网络 -> 主机字节序(32 位) | |
uint16_t ntohs(uint16_t netshort); | 字节序转换:网络 -> 主机字节序(16 位) |
未归
-
getaddrinfo():实现具体依赖于网络库,所以在netdb.h中只是extern,并没有具体实现,真正的实现在glibc中,需要编译连接
-
recv
返回值:- recv > 0 正常
- recv ==0 对端关闭,
- recv == -1 && errno == EAGAIN/EWOULDBLOCK 非错误,当前没数据
- recv == -1 其他errno 有错误需要重连
sockopt
get and set options on socket.
include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, void optval[], socklen_t optlen);
int getsockopt(int sockfd, int level, int optname, void optval[], socklen_t *optlen);
level: IPPROTO_TCP,SOL_SOCKET
ret: -1调用失败 errno查看:如fd无效。0调用成功
SOCKET级别的 optname:
SO_REUSEADDR
:快速在同一端口重启。
TCP连接关闭,会进入TIME_WAIT状态,等待确保所有数据能够处理。 在这个状态下,端口不能重新绑定,直到连接上没有数据包流动。
默认情况下,TIME_WAIT会持续2-4分钟。取决于操作系统配置。
并不是万能的!还是可能already in use。 lsof -i :<port>
查看手动关闭
-
SO_KEEPALIVE
:保活开关。自动断开死链。 -
SO_ERROR
:取出fd上最后一次错误。 -
SO_RCVTIMEO / SO_SNDTIMEO
:设置rec(), write()
超时时间。用于客户端超时重连。超时errno
是EAGAIN
或EWOULDBLOCK
。
TCP级的optname:
-
TCP_NODELAY
:Nagle算法会将小的数据包合成大的再发,等待缓冲区满了发。 启用不能保证实时性, -
TCP_KEEP**
:保活细节。会在连接空闲时候发生保活包检测是否连接有效,如果指定时间内没有响应就断开
TCP_KEEPIDLE
:连接空闲多长时间后开始发送保活探测包(单位:秒)。TCP_KEEPINTVL
:保活探测包之间的时间间隔(单位:秒)。TCP_KEEPCNT
:在认为连接失效之前,最多发送多少个保活探测包。-
问题
- 文件/套接字 用哪个?, write和send啥区别,
结论:就看需不需要flags。
函数 | 范围 | 区别 |
---|---|---|
write(fd, buf, len) | 适用于 文件 和 套接字, | 通用写入,不能传 flags |
send(fd, buf, len, flags) | 仅适用于 socket | 支持 flags(如 MSG_NOSIGNAL 避免 SIGPIPE) |
- FILE* 可以覆盖fd 操作吗,区别在哪?(redis io仅实现FILE)
结论:如果只操作普通文件,就FILE*, 涉及其他用fd
特性 | FILE* (stdio.h) | fd (unistd.h) |
---|---|---|
是否缓冲 | 有缓冲(fwrite() 先写入缓冲区) | 无缓冲(write() 直接写入内核) |
操作级别 | 高级 API,适合一般文件 I/O | 底层 API,适合文件、socket、管道 |
使用的系统调用 | fopen() / fwrite() / fread() / fseek() | open() / write() / read() / lseek() |
支持的对象 | 普通文件 | 文件、socket、管道、设备、非阻塞 I/O |
线程安全 | 部分操作是线程安全的(FILE* 结构体内部有锁) | 不保证线程安全(除非手动加锁) |
跨平台兼容性 | 跨平台兼容性更好 | POSIX 专属,不适用于 Windows |
- 文件到套接字流?
sendfile
共享内存
场景:有一个交易系统,一个终端界面显示实时信息,另一个要进行操作订单。他们两个需要共享内存 进行通信
mmap
: 把文件或设备映射成内存。 比如文件转换为结构体,但是结构体不能有指针,如果操作指针即跨越进程,不允许的。
所以对于这个场景是不太适用的,更适用文件共享读写。基于共享内存的操作都是内存页,与原来文件无关。如果要落入文件,需要通过msync
函数把内存页落盘。 即(内存-文件-磁盘),文件磁盘是系统内核同步的,不需要管
void *mmap(void* addr, size_t length, int prot, int flags,int fd, off_t offset);
- addr: 文件映射到内存空间的起始地址。一般为NULL
- length: 要多少字节数,从offset开始算起
- prot: 指定该内存访问权限。
PROT_READ, WRITE, EXEC, NORE
- flag : 解决内存重叠。
MAP_SHARED
表示写入的数据会复制同步到文件内。- fd: 表示映射的文件,一般是对应open的描述符。
- offset:从文件某个偏移量开始映射。必须是
PAGSIZE
倍数
UNIX DOMAIN SOCKET
:本地进程通信,还是服务端客户端结构。
- 不通过
ip:port
寻址,而是基于文件。 - 由于结构体含有指针,还是得序列化进行通信。
共享内存,但是不使用指针,即划定一片,然后通过offset定位所有指针。—> offset 型内存池
共享内存刷到文件:
int msync(void *addr, size_t length, int flags);
刷新addr ~ addr +len 到文件。
flags :
MS_SYNC
阻塞强制同步,MS_ASYNC
异步
释放共享内存 , 即解除当前进程对文件的内存映射。这不会影响其他进程。
munmap(base, size);
查看mmap
文件内容就是查看内存内容。
》