linux系统编程

常识

unistd.h

<unistd.h> UNIX standard header

主要就是UNIX系统级接口:进程,IO, 文件描述符,系统调用

  1. 类型:pid_t, uid_t, size_t, ssize_t, off_t, intptr_t, stdin-0,..
  2. 进程:fork, exec*, getpid, _exit, getppid
  3. 文件与IO:open, read,.. fsync, pipe
  4. 目录: rmdir, chdir, getcwd
  5. 用户:getuid..
  6. 系统控制: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);

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()

  1. 如果进程是进程组组长,出错。 一般先调用fork,然后使父进程终止。子进程即继承进程组ID,但不是进程组组长。
  2. 如果进程不是进程组组长,则创建新会话成功。具体地:
    • 该进程成为会话首进程。
    • 该进程成为一个新进程组的组长。
    • 该进程没有控制终端。

守护进程

守护进程

系统守护进程

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); 
}
  1. 创建一个会话:(a)新进程会直接成为会话的首进程。(b)成为一个新进程组的组长进程。(c)没有控制终端
  2. 信号处理 :确保后续守护进程不会持有终端,且不受控制终端影响。

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);

  1. 文件描述符 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);

  1. 设置日志类型, 位屏蔽
  2. 记录日志消息

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默认动作都是终止线程,这里作为替代,不会终止线程。

常见守护进程

  1. /usr/sbin/cron:这是定时任务调度器 cron 的进程。
  2. /usr/sbin/rsyslogd:这是系统日志服务 rsyslogd 的进程,它负责收集、处理和转发系统日志。
  3. /usr/lib/snapd/snapd:这是 snapd 守护进程,它是 Snap 包管理器的后台服务。
  4. /usr/sbin/xinetd:这是 xinetd 守护进程,它是一个基于 TCP Wrappers 的超级服务器,用于管理和启动其他网络服务。
  5. /usr/libexec/packagekitd:这是 PackageKit 守护进程,它是一个软件包管理服务,用于管理软件包的安装、更新等操作。
  6. /usr/libexec/polkitd:这是 PolicyKit 守护进程,它是一个系统权限管理服务,用于控制用户对系统资源的访问权限。

场景

  1. 守护进程查看是否有登录名。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;
}

要点

  1. select 会修改 fd_set,需要每次重新设置。(因为会清除未发生变化的描述符)
  2. timeout每次也要重新设置
  3. 最多能监听102个描述符

epoll

  1. 事件驱动:注册的fd,不会每次遍历所有fd。而是只关心发生变化的fd,epoll_wait也只返回有事件的fd,而不是select需要遍历

  2. 支持边缘触发ETL模式:减少重复通知

用法:

  1. 创建epoll实例:epoll_create1
#include <sys/epoll.h>
int epfd = epoll_create1(0);  // 推荐使用 epoll_create1
    if (epfd == -1) {
    perror("epoll_create1");
    exit(EXIT_FAILURE);
} 
  1. 添加、修改、删除 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); }
  2. 等待事件 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);

}

线程

使用

  1. pthread_create
  2. pthread_join阻塞等待某个线程结束
  3. pthread_exit线程主动退出
  4. pthread_self 获取线程ID
  5. pthread_detach线程分离

主要概念

  1. 分离状态:主线程不需要线程的终止状态时候,即可将其分离。比如网络线程。

        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
  2. 优先级:默认是0

.线程控制

同步属性:

互斥量属性:

默认属性初始化:pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;, 或者pthread_mutex_init(&mutex, NULL)

非默认属性:pthread_mutexattr_init(attr) . pthread_mutex_init(&mutex, &attr) 先初始化设置互斥量属性,再用属性来初始化互斥量

  1. 类型属性 PTHREAD_MUTEX_RECURSIVE: 互斥量通过计数来加锁解锁。

内存管理

操作系统提供了mmap(分配内存)、sbrk(扩缩进程堆空间)等系统调用申请内存,但是应用层一般不会直接调用,原因有二:

  1. 系统调用提供的内存粒度太大,常常是一页4K.
  2. 频繁系统调用会增大开销。

现代应用层自己的内存分配器都是维持一片内存池,进行内存管理。

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()超时时间。用于客户端超时重连。超时 errnoEAGAINEWOULDBLOCK

TCP级的optname:

  • TCP_NODELAY:Nagle算法会将小的数据包合成大的再发,等待缓冲区满了发。 启用不能保证实时性,

  • TCP_KEEP** :保活细节。

    会在连接空闲时候发生保活包检测是否连接有效,如果指定时间内没有响应就断开

    • TCP_KEEPIDLE:连接空闲多长时间后开始发送保活探测包(单位:秒)。
    • TCP_KEEPINTVL:保活探测包之间的时间间隔(单位:秒)。
    • TCP_KEEPCNT:在认为连接失效之前,最多发送多少个保活探测包。-

问题

  1. 文件/套接字 用哪个?, write和send啥区别,

结论:就看需不需要flags。

函数 范围 区别
write(fd, buf, len) 适用于 文件 和 套接字, 通用写入,不能传 flags
send(fd, buf, len, flags) 仅适用于 socket 支持 flags(如 MSG_NOSIGNAL 避免 SIGPIPE)
  1. 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
  1. 文件到套接字流?

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文件内容就是查看内存内容。

results matching ""

    No results matching ""