相关代码：DPDK_Learning/ICMP_echo_reply at main · mfdycs/DPDK_Learning (github.com) ICMP报文格式如图所示，每一个ICMP消息都将包含引发这条ICMP消息的数据包的完全IP包头，ICMP报文则作为IP数据包的数据部分封装在IP数据包内部 Type字段表示ICMP消息的类型； Code字段表示ICMP消息类型细分的子类型； Checksum字段表示ICMP报文的校验和。 对于ICMP响应报文，各字段的具体内容如下： 添加标识 #define ENABLE_ICMP 1 主函数判断 主函数中判断是否为 8 0 echo请求报文，是的话返回响应 #if ENABLE_ICMP if (iphdr->next_proto_id == IPPROTO_ICMP) { struct rte_icmp_hdr *icmphdr = (struct rte_icmp_hdr *)(iphdr + 1); struct in_addr addr; addr.s_addr = iphdr->src_addr; printf("icmp ---> src: %s ", inet_ntoa(addr)); // 如果是ICMP请求: 8 0 if (icmphdr->icmp_type == RTE_IP_ICMP_ECHO_REQUEST) { addr.s_addr = iphdr->dst_addr; printf("icmp ---> local: %s , type : %d\n", inet_ntoa(addr), icmphdr->icmp_type); struct rte_mbuf *txbuf = ng_send_icmp(mbuf_pool, ethr->s_addr.addr_bytes, iphdr->dst_addr, iphdr->src_addr, icmphdr->icmp_ident, icmphdr->icmp_seq_nb); rte_eth_tx_burst(DPDK_PortID, 0, &txbuf, 1); rte_pktmbuf_free(txbuf); rte_pktmbuf_free(mbufs[i]); } } #endif 发送函数 回发的时候需要发送IP, 目的IP, ICMP是网络层协议, 没有端口的概念 此外还需要ICMP协议首部相关值，根据ICMP官方文档，响应时identifier字段为发送时的值 static struct rte_mbuf *ng_send_icmp(struct rte_mempool *mbuf_pool, uint8_t *dst_mac, uint32_t sip, uint32_t dip, uint16_t id, uint16_t seqnb) { const unsigned total_length = sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_icmp_hdr); struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool); if (!mbuf) { rte_exit(EXIT_FAILURE, "rte_pktmbuf_alloc\n"); } mbuf->pkt_len = total_length; mbuf->data_len = total_length; uint8_t *pkt_data = rte_pktmbuf_mtod(mbuf, uint8_t *); ng_encode_icmp_pkt(pkt_data, dst_mac, sip, dip, id, seqnb); return mbuf; } 封装ICMP数据 static int ng_encode_icmp_pkt(uint8_t *msg, uint8_t *dst_mac, uint32_t sip, uint32_t dip, uint16_t id, uint16_t seqnb) { // 1. ethhdr struct rte_ether_hdr* eth = (struct rte_ether_hdr*)msg; rte_memcpy(eth->s_addr.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN); rte_memcpy(eth->d_addr.addr_bytes, gDstMac, RTE_ETHER_ADDR_LEN); eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // 2. iphdr struct rte_ipv4_hdr* ip = (struct rte_ipv4_hdr*)(msg + sizeof(struct rte_ether_hdr)); ip->version_ihl = 0x54; ip->type_of_service = 0; ip->total_length = htons(sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_icmp_hdr)); ip->packet_id = 0; ip->fragment_offset = 0; ip->time_to_live = 64; ip->next_proto_id = IPPROTO_ICMP; ip->src_addr = gSrcIp; ip->dst_addr = gDstIp; ip->hdr_checksum = 0; ip->hdr_checksum = rte_ipv4_cksum(ip); // 3. icmphdr struct rte_icmp_hdr* icmp = (struct rte_icmp_hdr*)(msg + sizeof(struct rte_ether_hdr) + sizeof(struct rte_icmp_hdr)); icmp->icmp_type = RTE_IP_ICMP_ECHO_REPLY; icmp->icmp_code = 0; icmp->icmp_ident = id; icmp->icmp_seq_nb = seqnb; // icmp->icmp_cksum = rte_ipv4_cksum(ip, icmp); // 错误 icmp->icmp_cksum = icmp_checksum((uint16_t*)icmp, sizeof(struct rte_icmp_hdr)); } 值得注意的是，ICMP首部中的Checksum字段不能使用dpdk中ipv4校验和的计算方法 // 错误计算 icmp->icmp_cksum = rte_ipv4_cksum(ip, icmp); 所以需要参考官方文档中的计算方法：RFC 1071: Computing the Internet checksum (rfc-editor.org) static uint16_t icmp_checksum(uint16_t *addr, int count) { register long sum = 0; while (count > 1) { sum += *(unsigned short*)addr++; count -= 2; } if (count > 0) { sum += *(unsigned char *)addr; } while (sum >> 16) { sum = (sum & 0xffff) + (sum >> 16); } return ~sum; } [note-ico type="flat" ico=""]对于各种协议，代码的处理逻辑都是类似的，那么是否可以抽象成一个方法出来 最终提供一个接口，供外部访问使用即可[/note-ico]
相关代码：DPDK_Learning/ARP_response at main · mfdycs/DPDK_Learning (github.com) 相关原理 ARP数据包 ARP Request ARP Response ARP的功能 1.响应其他机器的arp request（本章实现） 2.定时主动发送arp request，响应其他机器的arp reply，记录在arp table中 ARP是一个广播协议，不是一对一的通信 ARP操作 # 查看arp表 arp -a # 查看网卡对应的信息 netsh i i show in # 删除网卡的arp表 netsh i i delete neighbors 22 ## 此时再使用工具发送数据包，会发现找不到对应的arp映射 # 添加arp映射 netsh -c i i add neighbors 22 192.168.81.128 XX-XX-XX-XX-XX-XX # 此时发送数据正常 对于DPDK，响应ARP请求一共有三个步骤： 识别ARP请求 是否是自己的机器 发送ARP响应 添加标识 #define ENABLE_ARP 1 与UDP发送类似，构造ARP响应 #if ENABLE_ARP static int ng_encode_arp_pkt(uint8_t *msg, uint8_t *dst_mac, uint32_t sip, uint32_t dip) { // 1 ethhdr struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg; rte_memcpy(eth->s_addr.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN); rte_memcpy(eth->d_addr.addr_bytes, dst_mac, RTE_ETHER_ADDR_LEN); eth->ether_type = htons(RTE_ETHER_TYPE_ARP); // 2 arp struct rte_arp_hdr *arp = (struct rte_arp_hdr *)(eth + 1); // eth首部下一位 arp->arp_hardware = htons(1); // 硬件类型 arp->arp_protocol = htons(RTE_ETHER_TYPE_IPV4); // 协议类型 arp->arp_hlen = RTE_ETHER_ADDR_LEN; // 硬件地址长度 arp->arp_plen = sizeof(uint32_t); // 协议地址长度: len(ipv4) arp->arp_opcode = htons(2); // 操作码 rte_memcpy(arp->arp_data.arp_sha.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN); // 发送者硬件地址 rte_memcpy( arp->arp_data.arp_tha.addr_bytes, dst_mac, RTE_ETHER_ADDR_LEN); // 接收者硬件地址 arp->arp_data.arp_sip = sip; arp->arp_data.arp_tip = dip; return 0; } static struct rte_mbuf* ng_send_arp(struct rte_mempool *mbuf_pool, uint8_t *dst_mac, uint32_t sip, uint32_t dip) { // len(arp:42) = len(eth:14) + len(arp:28) const unsigned total_length = sizeof(struct rte_ether_hdr) + sizeof(struct rte_arp_hdr); struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool); if (!mbuf) { rte_exit(EXIT_FAILURE, "rte_pktmbuf_alloc\n"); } mbuf->pkt_len = total_length; mbuf->data_len = total_length; uint8_t *pkt_data = rte_pktmbuf_mtod(mbuf, uint8_t *); ng_encode_arp_pkt(pkt_data, dst_mac, sip, dip); return mbuf; } #endif 主函数解析数据并识别 #if ENABLE_ARP if (ethr->ether_type == rte_cpu_to_be_16(RTE_ETHER_TYPE_ARP)) { struct rte_arp_hdr *arp_hdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_arp_hdr*, sizeof(struct rte_ether_hdr)); struct in_addr addr; addr.s_addr = arp_hdr->arp_data.arp_tip; printf("arp ---> src: %s ", inet_ntoa(addr)); addr.s_addr = gLocalIp; printf("arp ---> local: %s \n", inet_ntoa(addr)); if (arp_hdr->arp_data.arp_tip == gLocalIp) { struct rte_mbuf *arpbuf = ng_send_arp(mbuf_pool, arp_hdr->arp_data.arp_sha.addr_bytes, arp_hdr->arp_data.arp_tip, arp_hdr->arp_data.arp_sip); rte_eth_tx_burst(DPDK_PortID, 0, &arpbuf, 1); rte_pktmbuf_free(arpbuf); rte_pktmbuf_free(mbufs[i]); } continue; } #endif 此时 ping 主机会显示Timeout，因为没有实现icmp协议，但是ARP响应流通（否则会显示不可到达）
相关代码：DPDK_Learning/UDP_transmit at main · mfdycs/DPDK_Learning (github.com) DPDK启动逻辑框架可以参考 接收UDP数据 定义网络信息 #define ENABLE_SEND 1 #define ENABLE_ARP 1 // 发送数据需要的网络信息, 先不考虑传参 #if ENABLE_SEND static uint32_t gSrcIp; static uint32_t gDstIp; static uint8_t gSrcMac[RTE_ETHER_ADDR_LEN]; static uint8_t gDstMac[RTE_ETHER_ADDR_LEN]; static uint16_t gSrcPort; static uint16_t gDstPort; #endif 启动发送队列 // 配置网卡信息 const int num_tx_queues = 1; // 发送队列网卡数量 struct rte_eth_conf port_conf = port_conf_default; #if ENABLE_SEND // 启动tx队列 struct rte_eth_txconf txq_conf = dev_info.default_txconf; // 修改tx对应负载：把发送大小设置为接收大小 txq_conf.offloads = port_conf.rxmode.offloads; // rte_eth_tx_queue_setup(port_id, tx_queue_id, nb_rx_desc, socket_id, tx_conf); ret = rte_eth_tx_queue_setup(DPDK_PortID, 0, 128, rte_eth_dev_socket_id(DPDK_PortID), &txq_conf); if (ret < 0) { rte_exit(EXIT_FAILURE, "Could not setup TX queue\n"); } #endif 发送数据 从 mempool 里面获取到 mbuf，mbuf是内存池中的最小单元 UDP首部长度为42字节：14(eth) + 20(ip) + 8(udp) static struct rte_mbuf* ng_send(struct rte_mempool *mbuf_pool, unsigned char* data, uint16_t length) { struct rte_mbuf* mbuf = rte_pktmbuf_alloc(mbuf_pool); if (!mbuf) { rte_exit(EXIT_FAILURE, "Error with EAL init\n"); } mbuf->pkt_len = total_len; mbuf->data_len = total_len; uint8_t *pktdata = rte_pktmbuf_mtod(mbuf, uint8_t*); ng_encode_udp_pkt(pktdata, data, total_len); return mbuf; } 封装UDP数据 UDP数据头部信息： 以太网协议头： IP协议头： // encode udp pkt static int ng_encode_udp_pkt(uint8_t *msg, unsigned char* data, uint16_t total_len) { // 1. ethhdr struct rte_ether_hdr* eth = (struct rte_ether_hdr*)msg; rte_memcpy(eth->s_addr.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN); rte_memcpy(eth->d_addr.addr_bytes, gDstMac, RTE_ETHER_ADDR_LEN); eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // 2. iphdr struct rte_ipv4_hdr* ip = (struct rte_ipv4_hdr*)(msg + sizeof(struct rte_ether_hdr)); ip->version_ihl = 0x54; // 版本+首部长度信息, 网络字节序 ip->type_of_service = 0; ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr)); ip->packet_id = 0; // 16位位标识 ip->fragment_offset = 0; // 3位标识+13位片位移 ip->time_to_live = 64; ip->next_proto_id = IPPROTO_UDP; ip->src_addr = gSrcIp; ip->dst_addr = gDstIp; ip->hdr_checksum = 0; // 先置0, 因为会参与计算 ip->hdr_checksum = rte_ipv4_cksum(ip); // 3. udphdr struct rte_udp_hdr* udp = (struct rte_udp_hdr*)(msg + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr)); udp->src_port = gSrcPort; udp->dst_port = gDstPort; uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr); udp->dgram_len = htons(udplen); rte_memcpy((uint8_t*)(udp + 1), data, udplen); udp->dgram_cksum = 0; udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp); // 4. print struct in_addr addr; addr.s_addr = gSrcIp; printf(" --> src: %s:%d, ", inet_ntoa(addr), ntohs(gSrcPort)); addr.s_addr = gDstIp; printf("dst: %s:%d\n", inet_ntoa(addr), ntohs(gDstPort)); return 0; } static struct rte_mbuf* ng_send(struct rte_mempool *mbuf_pool, unsigned char* data, uint16_t length) { const unsigned total_len = length + 42; struct rte_mbuf* mbuf = rte_pktmbuf_alloc(mbuf_pool); if (!mbuf) { rte_exit(EXIT_FAILURE, "Error with EAL init\n"); } mbuf->pkt_len = total_len; mbuf->data_len = total_len; uint8_t *pktdata = rte_pktmbuf_mtod(mbuf, uint8_t*); ng_encode_udp_pkt(pktdata, data, total_len); return mbuf; }
相关代码：DPDK_Learning/UDP_recv at main · mfdycs/DPDK_Learning (github.com) 思路步骤 1. 进程初始化 固定方法，用于初始化DPDK，检测巨页、CPU是否有设置 if (rte_eal_init(argc, argv) < 0) { rte_exit(EXIT_FAILURE, "Error with EAL init\n"); } 2. 初始化内存池 一个进程确定一个内存池, 用于接收数据 struct rte_mempool* rte_pktmbuf_pool_create(const char* name, unsigned n, unsigned cache_size, uint16_t priv_size, uint16_t data_room_size, int socket_id); 参数： name: The name of the mbuf pool. n: The number of elements in the mbuf pool. The optimum size (in terms of memory usage) for a mempool is when n is a power of two minus one: n = (2^q - 1). cache_size: Size of the per-core object cache. See rte_mempool_create() for details. priv_size: Size of application private are between the rte_mbuf structure and the data buffer. This value must be aligned to RTE_MBUF_PRIV_ALIGN. data_room_size: Size of data buffer in each mbuf, including RTE_PKTMBUF_HEADROOM. socket_id: The socket identifier where the memory should be allocated. The value can be SOCKET_ID_ANY if there is no NUMA constraint for the reserved zone. 为了提升性能，我们不设置整4k，而是 4k-1，这样大块可以放在另外的地方 #define NUM_MBUFS (4096-1) struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_BUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()); if (mbuf_pool == NULL) { rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n"); } 3. 初始化网卡 确定传送/接收数据的网卡 在安装DPDK的时候，我们会先绑定相应的网卡，可以选择相对应的功能查看绑定信息 43-45插入，选择49-50可以绑定，48查看信息 Option: 43 Option: 44 Option: 45 Option: 48 最前面的为PCI地址 选择49，输入 0000:03:00.0 绑定 VMXNET3 多队列网卡 此时可能会提示该设备被占用，在使用前需要先 down 掉 ens 为虚拟网卡, 需要先改为 ethx 初始化网卡 #define NUM_BUFS (4096-1) int DPDK_PortID = 0; static const struct rte_eth_conf port_conf_default = { .rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN, }, }; static void ng_init_port(struct rte_mempool *mbuf_pool) { uint16_t nb_sys_ports = rte_eth_dev_count_avail(); if (nb_sys_ports == 0) { rte_exit(EXIT_FAILURE, "No Supported eth found\n"); } #if 0 struct rte_eth_dev_info dev_info; rte_eth_dev_info_get(DPDK_PortID, &dev_info); # endif // 配置网卡信息 const int num_rx_queues = 1; const int num_tx_queues = 0; struct rte_eth_conf port_conf = port_conf_default; int ret = rte_eth_dev_configure(DPDK_PortID, num_rx_queues, num_tx_queues, &port_conf); if (ret < 0) { rte_exit(EXIT_FAILURE, ":: Cannot configure device: err = %d\n", ret); } ret = rte_eth_rx_queue_setup(DPDK_PortID, 0, 128, rte_eth_dev_socket_id(DPDK_PortID), NULL, mbuf_pool) < 0); if (ret < 0) { rte_exit(EXIT_FAILURE, "Could not setup RX queue\n"); } ret = rte_eth_dev_start(DPDK_PortID); if (ret < 0) { rte_exit(EXIT_FAILURE, "Could not start\n"); } } 其中，启动队列的函数为 int rte_eth_rx_queue_setup(port_id, rx_queue_id, nb_rx_desc, socket_id, rx_conf, mb_pool); nb_rx_desc 为当前指定队列的最长长度，为 rte_eth_rx_burst() 接收的包数量，超过且未设置处理的话会导致内存泄漏 4. 接收数据 接收数据，首先考虑以下几个问题： 从哪个端口接收 从哪个队列接收 接收完放到哪里 长度限制是多少 #define BURST_SIZE 128 struct rte_mbuf *mbufs[BURST_SIZE]; unsigned num_recvd = rte_eth_rx_burst(DPDK_PortID, 0, mbufs, BURST_SIZE); if (num_recvd > BURST_SIZE) { rte_exit(EXIT_FAILURE, "Error receiving from eth\n"); } 5. 处理数据 unsigned int i = 0; for (i = 0; i < num_recvd; i++) { // 处理 mbuf 中的数据 struct rte_ether_hdr *ethr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr*); if (ethr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) { continue; } struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr*, sizeof(struct rte_ether_hdr)); // UDP协议数据 if (iphdr->next_proto_id == IPPROTO_UDP) { struct rte_udp_hdr *udphdr = (struct rte_udp_hdr*)(iphdr + 1); uint16_t length = ntohs(udphdr->dgram_len); *(char*)(udphdr + length) = '\0'; struct in_addr addr; addr.s_addr = iphdr->src_addr; printf("src: %s:%d, ", inet_ntoa(addr), ntohs(udphdr->src_port)); addr.s_addr = iphdr->dst_addr; printf("dst: %s:%d, len: %d --> %s\n", inet_ntoa(addr), ntohs(udphdr->dst_port), length, (char*)(udphdr + 1)); rte_pktmbuf_free(mbufs[i]); } } 运行代码 初始化环境变量 export RTE_SDK=/home/king/share/dpdk/dpdk-stable-19.08.2/ export RTE_TARGET=x86_64-native-linux-gcc 将代码放到 ./example/recv 目录下，拷贝其他例子的 makefile，修改编译文件，编译 # binary name APP = dpdk_recv # all source are stored in SRCS-y SRCS-y := recv.c 从系统中解绑网卡，这里解绑 eth0： ifconfig eth0 down 绑定网卡 发送端添加ip和网卡绑定: arp -s ip mac 绑定对应网卡的arp: # 查看网卡 > netsh i i show in Idx Met MTU 状态 名称 --- ---------- ---------- ------------ --------------------------- 1 75 4294967295 connected Loopback Pseudo-Interface 1 6 25 1500 disconnected WLAN 23 25 1500 connected 以太网 26 25 1500 disconnected 本地连接* 1 12 25 1500 disconnected 本地连接* 2 9 65 1500 disconnected 蓝牙网络连接 2 17 55 1400 disconnected 以太网 2 4 35 1500 connected VMware Network Adapter VMnet1 13 35 1500 connected VMware Network Adapter VMnet8 52 5000 1500 connected vEthernet (WSL) > netsh -c i i add neighbors [Idx] [IP] [MAC] 运行 ./build/dpdk-recv
背景 最近本人尝试搭建几个局域网 Web 服务 考虑到wsl2有以下优势： 可以直接挂载本地硬盘：/mnt/c 可以根据程序需求动态变更所需资源 对外跟宿主机IP是一致的，本机可以通过 127.0.0.1 访问，外部设备可以通过宿主机IP访问 于是尝试采用WSL2搭建 Web 服务... 过程 在搭建好后，发现宿主机需要开启端口转发其他电脑才能访问到，网上教程如下： netsh interface portproxy add v4tov4 listenport=8001 listenaddress=0.0.0.0 connectport=8001 connectaddress=localhost 侦听 ipv4: 连接到 ipv4: 地址 端口 地址 端口 --------------- ---------- --------------- ---------- 0.0.0.0 8001 localhost 8001 此时一切正常，局域网其他主机可以顺利访问到 WSL2 中的 Web 服务，胜利仿佛在对我挥手 但在重启电脑后，出现以下bug： 其他设备无法访问到WSL2中的 Web 服务 宿主机 127.0.0.1:8001 无法访问 localhost:8001 可以访问 172.*.*.*:8001 可以访问 在其他设备和宿主机的 172.0.0.1 尝试连接时，宿主机CPU会跑满，根据相应端口和对应PID，查到此时iphlpsvc 进程会占满CPU，查看此进程为IP Helper 服务 尝试百度无解...GitHub issue区有相同情况WSL 2 Linux host cannot be accessed by a custom domain name or 127.0.0.1 · Issue #5728 · microsoft/WSL · GitHub 于是尝试debug： 查看宿主机Hosts，发现无异常 检查宿主机和 WSL2 的系统防火墙，发现无异常 重启 WSL2，无解 将Windows宿主机的网络重置：将所有网络适配器重置为出厂设置，无解 临时启动其他端口的 Web 服务，发现宿主机可以通过127.0.0.1:port进行连接，但之前设置过的端口依旧无法使用，此时考虑到是否是映射环境出现了问题 于是乎，在一位博主的帖子中(Troubleshooting WSL + localhost:3000 'connection refused' issue - DEV Community 👩‍💻👨‍💻)发现，关闭端口转发后，IP Helper 不会高占用，而且本机127.0.0.1:8001 可以正常访问web服务，但其他设备就无法访问到 WSL2 中的 Web 服务 思考：可能是相同端口转发出现了混乱？ 解决 自己尝试：将0.0.0.0:7001和 localhost:8001进行映射，重启服务，发现可用 netsh interface portproxy add v4tov4 listenport=7001 listenaddress=0.0.0.0 connectport=8001 connectaddress=localhost net stop iphlpsvc net start iphlpsvc 侦听 ipv4: 连接到 ipv4: 地址 端口 地址 端口 --------------- ---------- --------------- ---------- 0.0.0.0 7001 localhost 8001 或者：将宿主机的对外 IP:Port 和 localhost:8001进行映射，重启服务，发现可用（此处IP较为敏感，用 __.__.__.__ 代替） PS > netsh interface portproxy delete v4tov4 listenport=7001 listenaddress=0.0.0.0 PS > netsh interface portproxy add v4tov4 listenport=7001 listenaddress=__.__.__.__ connectport=8001 connectaddress=localhost PS > netsh interface portproxy show all 侦听 ipv4: 连接到 ipv4: 地址 端口 地址 端口 --------------- ---------- --------------- ---------- __.__.__.__ 7001 localhost 8001 PS > net stop iphlpsvc IP Helper 服务正在停止. IP Helper 服务已成功停止。 PS > net start iphlpsvc IP Helper 服务正在启动 . IP Helper 服务已经启动成功。
1. 进程号和父进程号 获取进程号： #include <unistd.h> pid_t getpid(void); // always successfully returns process ID of caller 可以通过 Linux系统特有的/proc/sys/kernel/pid_max 文件来进行查看或调整进程号上限（其值=最大进程号+1）。 一旦进程号达到上限，会将进程号计数器重置为 300，而不是 1。之所以如此，是因为低数值的进程号为系统进程和守护进程所长期占用，在此范围内搜索尚未使用的进程号只会是浪费时间。 父进程进程号： #include <unistd.h> pid_t getppid(void); // always successfully returns process ID of parent of caller 如果子进程的父进程终止，init 进程随即将收养该进程，子进程后续对 getppid () 的调用将返回进程号 1。 2. 进程内存分布 3. 虚拟内存管理 4. 栈和栈帧 5. 命令行参数 当执行程序时，命令行参数（command-line argument）（由 shell 逐一解析）通过两个入参提供给 main () 函数： 第一个参数 int argc，表示命令行参数的个数 第二个参数 char *argv[]，是一个指向命令行参数的指针数组，每一参数又都是以空字符（null）结尾的字符串。 第一个字符串，亦即 argv[0] 指向的，（通常）是该程序的名称 argv 中的指针列表以 NULL 指针结尾（即 argv[argc]为 NULL） 通过/proc/PID/cmdline文件可以读取任一进程的命令行参数；GNU C可使用program_invocation_name和program_invocation_short_name找到程序名称。 参数存储自己上限通过ARG_MAX限定，通过调用sysconf(__SC_ARG_MAC)确定上限值。 程序使用getopt () 库函数解析命令行选项。 6. 环境列表 可以使用全局变量 char **environ 访问环境列表，C运行时就定义并赋值了 #include "lib/tlpi_hdr.h" extern char **envirion; int main(int argc, char *argv[]) { char **ep; for (ep = envirion; *ep != NULL; ++ep) puts(*ep); exit(EXIT_SUCCESS); } 可以声明 main() 函数中的第三个参数来访问，但不推荐使用，该参数作用域仅在main () h函数内，而且该特性也不在规范中。 int main(int argc, char *argv[], char *envp[]) getenv () 函数能够从进程环境中检索单个值。 #include <stdlib.h> char *getenv(const char *name); // returns pointer to (vlaue) string, or NULL if no such variable 向 getenv () 函数提供环境变量名称，该函数将返回相应字符串指针。 如果指定 SHELL 为参数 name，那么将返回/bin/bash。 如果不存在指定名称的环境变量，那么 getenv () 函数将返回 NULL。 若需要改变一个环境变量的值，可以使用 setenv () 函数或 putenv () 函数 putenv () 函数向调用进程的环境中添加一个新变量，或者修改一个已经存在的变量值。 #include <stdlib.h> int putenv(char *string); // returns 0 on success, or nonzero on error 参数 string 是一指针，指向 name=value 形式的字符串。调用 putenv () 函数后，该字符串就成为环境的一部分，而不是复制副本，因此如果修改string变量，进程的环境也将被修改。 此外，string变量不应为自动变量，因为变量函数返回号，内存区域很有可能会被重写 setenv () 函数可以代替 putenv () 函数，向环境中添加一个变量。 #include <stdlib.h> int setenv(const char *name, const char *value, int overwrite); // returns 0 on success, or -1 on error setenv（）函数为形如name=value的字符串分配，并将所指向的字符串拷贝到缓冲区 unsetenv () 函数从环境中移除由name参数标识的变量 #include <stdlib.h> int unsetenv(const char *name); // return 0 on success, or -1 on error 同 setenv () 函数一样，参数 name 不应包含等号字符 #define _BSD_SOURCE #include <stdlib.h> int clearenv(void) // returns 0 on success, or a nonzero on error 示例：修改进程环境 #define _GNU_SOURCE #include <stdlib.h> #include "lib/tlpi_hdr.h" extern char **environ; int main(int argc, char *argv[]) { int j; char **ep; // erase entire environment clearenv(); for (j = 1; j < argc; ++j) if (putenv(argv[j]) != 0) errExit("putenv: %s", argv[j]); if (setenv("GREET", "Hello world", 0) == -1) errExit("setenv"); unsetenv("BYE"); for (ep = environ; *ep != NULL; ++ep) puts(*ep); exit(EXIT_SUCCESS); } 7. 执行非局部跳转：setjmp() 和 longjmp() 指跳转的目标为当前执行函数之外的某个位置 #include <setjmp.h> int setjmp(jmp_buf env); // returns 0 on initial call, nonzero on return via longjmp() void longjmp(jmp_buf env, int val); 通过查看 setjmp()返回的整数值，可以区分 setjmp 调用是初始返回还是第二次“返回”。初始调用返回值为 0，后续“伪”返回的返回值为 longjmp()调用中 val 参数所指定的任意值。通过对 val 参数使用不同值，能够区分出程序中跳转至同一目标的不同起跳位置。 调用 setjmp () 时，env 除了存储当前进程的其他信息外，还保存了程序计数寄存器（指向当前正在执行的机器语言指令）和栈指针寄存器（标记栈顶）的副本。 longjmp() 调用不能跳转到一个已经返回的函数中。 #include <setjmp.h> #include "tlpi_hdr.h" static jmp_buf env; static void f2(void) { longjmp(env, 2); } static void f1(int argc) { if (argc == 1) longjmp(env, 1); f2(); } int main(int argc, int *argv[]) { switch (setjmp(env)) { case 0: // the return after the initial setjmp() printf("Calling f1() after initial setjmp()\n"); f1(argc); break; case 1: printf("We jumped back from f1()\n"); break; case 2: printf("We jumped back from f2()\n"); break; } exit(EXIT_SUCCESS); } 执行结果： $ ./longjmp Calling f1() after initial setjmp() We jumped back from f1() $ ./longjmp x Calling f1() after initial setjmp() We jumped back from f2() setjmp() 使用限制 SUSv3 和 C99 规定，对 setjmp()的调用只能在如下语境中使用。 构成选择或迭代语句中（if、switch、while 等）的整个控制表达式。 作为一元操作符!（not）的操作对象，其最终表达式构成了选择或迭代语句的整个控制表达式。 作为比较操作（==、!=、< 等）的一部分，另一操作对象必须是一个整数常量表达式，且其最终表达式构成选择或迭代语句的整个控制表达式。 作为独立的函数调用，且没有嵌入到更大的表达式之中。 注意：C 语言赋值语句不在上述列表之列。以下形式的语句是不符合标准的： s = setjmp(env); // wrong! 之所以规定这些限制，是因为作为常规函数的 setjmp() 实现无法保证拥有足够信息来保存所有寄存器值和封闭表达式中用到的临时栈位置，以便于在 longjmp() 调用后此类信息能得以正确恢复。 优化编译器的问题 优化编译器会重组程序的指令执行顺序，并在 CPU 寄存器中而非 RAM 中存储某些变量。 此外，某些应用程序二进制接口（ABI）实现的语义要求 longjmp()函数恢复先前 setjmp()调用所保存的 CPU 寄存器副本。这意味着 longjmp()操作会致使经过优化的变量被赋以错误值。 例如： #include <stdio.h> #include <stdlib.h> #include <setjmp.h> static jmp_buf env; static void doJump(int nvar, int rvar, int vvar) { printf("Inside doJump(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar); longjmp(env, 1); } int main(int argc, char **argv) { int nvar; register int rvar; // 存储于寄存器中 volatile int vvar; // 不优化，该变量要实时更新读取 nvar = 111; rvar = 222; vvar = 333; if (setjmp(env) == 0) { // after setjmp() nvar = 777; rvar = 888; vvar = 999; doJump(nvar, rvar, vvar); } else { // after longjmp() printf("After longjmp(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar); } exit(EXIT_SUCCESS); } 执行结果如下： $ cc -o setjmp_vars setjmp_vars.c $ ./setjmp_vars Inside doJump(): nvar=777 rvar=888 vvar=999 After longjmp(): nvar=777 rvar=888 vvar=999 $ cc -O -o setjmp_vars setjmp_vars.c $ ./setjmp_vars Inside doJump(): nvar=777 rvar=888 vvar=999 After longjmp (): nvar=111 rvar=222 vvar=999 此处，在 longjmp()调用后，nvar 和 rvar 参数被重置为 setjmp()初次调用时的值。起因是优化器对代码的重组受到 longjmp()调用的干扰。作为候选优化对象的任一局部变量可能都难免会遇到这类问题，一般包含指针变量和 char、int、float、long 等任何简单类型的变量。 将变量声明为 volatile，是告诉优化器不要对其进行优化，从而避免了代码重组。在上面的程序输出中，无论编译优化与否，声明为 volatile 的变量 vvar 都得到了正确处理。因为不同的优化器有着不同的优化方法，具备良好移植性的程序应在调用 setjmp()的函数中，将上述类型的所有局部变量都声明为 volatile。 若在 GNU C 语言编译器中加入–Wextra（产生额外的警告信息）选项，setjmp_vars.c 程序的编译结果将显示有帮助的警告信息如下： $ cc -Wall -Wextra -O -o setjmp_vars setjmp_vars.c setjmp_vars.c: In function 'main': setjmp_vars.c:17: warning: variable 'nvar' might be clobbered by 'longjmp' or 'vfork' setjmp_vars.c:18: warning: variable 'rvar' might be clobbered by 'longjmp' or 'vfork' 尽可能避免使用 setjmp() 函数和 longjmp() 函数
1. 文件控制操作：fcntl() #include <fcntl.h> int fcntl(int fd, int cmd, ...); // return on success depends on cmd, or -1 on error 打开文件的状态标志 fcntl()的用途之一是针对一个打开的文件，获取或修改其访问模式和状态标志（这些值是通过指定 open()调用的 flag 参数来设置的）。要获取这些设置，应将 fcntl()的 cmd 参数设置为F_GETFL。 fcntl() 的 F_SETFL 命令来修改打开文件的某些状态标志。允许更改的标志有 O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC 和 O_DIRECT。 使用 fcntl() 修改文件状态标志，尤其适用于如下场景: 文件不是由调用程序打开的，所以程序也无法使用 open() 调用来控制文件的状态标志 文件描述符的获取是通过 open() 之外的系统调用 比如 pipe() 调用，该调用创建一个管道，并返回两个文件描述符分别对应管道的两端 比如 socket() 调用，该调用创建一个套接字并返回指向该套接字的文件描述符 为了修改打开文件的状态标志，可以使用 fcntl () 的 F_GETFL 命令来获取当前标志的副本，然后修改需要变更的比特位，最后再次调用 fcntl () 函数的 F_SETFL 命令来更新此状态标志。因此，为了添加 O_APPEND 标志，可以编写如下代码： int flags; flags = fcntl(fd, F_GETFL); if (flags == -1) errExit("fcntl"); flags |= O_APPEND; if (fcntl(fd, F_SETFL, flags) == -1) errExit("fcntl"); 2. 文件描述符与文件 文件描述符和打开的文件并不是一一对应的关系，多个文件描述符可以指向同一个打开文件，这些文件描述符可以在相同或不同的进程中打开。 进程级的文件描述符表。每个进程有自己的打开文件的描述符表。其中每个条目都记录了单个文件描述符的相关信息，比如： 控制文件描述符操作的标志。这是文件描述符自己的属性。 对打开文件句柄的引用。 系统级的打开文件表。针对所有打开的文件，内核有一个系统级的描述表，其中每个条目称为打开文件句柄，存储了与一个打开文件相关的全部信息，比如： 当前文件偏移量（调用read和write时更新，或者使用lseek直接修改）。 打开文件时所使用的状态标志（open的flags参数）。 文件访问模式（如调用 open () 时所设置的只读模式、只写模式或读写模式）。 与信号驱动 I/O 相关的设置。 对该文件inode对象的引用。 inode表。文件系统，每个文件系统都会为其中所有文件建立一个inode表。其中条目为文件的inode信息，比如： 文件类型（例如，常规文件、套接字）和访问权限。 文件的各种属性，包括文件大小以及与不同类型操作相关的时间戳。 在进程 A 中，文件描述符 1 和 20 都指向同一个打开的文件句柄（标号为 23）。这可能是通过调用 dup ()、dup2 () 或 fcntl () 而形成的。 进程A的文件描述符2和进程B的文件描述符2都指向同一个打开的文件句柄（标号为73）。这种情形可能在调用 fork () 后出现（即，进程 A 与进程 B 之间是父子关系），或者当某进程通过UNIX 域套接字将一个打开的文件描述符传递给另一进程时，也会发生。 此外，进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件句柄，但这些句柄均指向 i-node 表中的相同条目（1976），换言之，指向同一文件。发生这种情况是因为每个进程各自对同一文件发起了 open () 调用。同一个进程两次打开同一文件，也会发生类似情况。 总结如下： 两个不同的文件描述符，若指向同一打开文件句柄，将共享同一文件偏移量。因此，如果通过其中一个文件描述符来修改文件偏移量（由调用 read ()、write () 或 lseek () 所致），那么从另一文件描述符中也会观察到这一变化。无论这两个文件描述符分属于不同进程，还是同属于一个进程，情况都是如此。 要获取和修改打开的文件标志（例如，O_APPEND、O_NONBLOCK 和 O_ASYNC），可执行 fcntl () 的 F_GETFL 和 F_SETFL 操作，其对作用域的约束与上一条颇为类似。 相形之下，文件描述符标志（亦即，close-on-exec 标志）为进程和文件描述符所私有。对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符。 3. 复制文件描述符 shell指令，把myscript执行产生的标准输出和错误输出全部输出到results. log $ ./myscript > results.log 2>&1 是重定向到标准输出没错，但此时标准输出已经定向到文件了，所以错误输出也到文件了 2>&1 ：将标准错误输出转变化标准输出，可以将错误信息也输出到日志文件中 shell 通过复制文件描述符 21 实现了标准错误的重定向操作，因此文件描述符 2 与文件描述符 1 指向同一个打开文件句柄（类似于上节图中进程 A 的描述符 1 和 20 指向同一打开文件句柄的情况）。 请注意，要满足 shell 的这一要求，仅仅简单地打开 results.log 文件两次是远远不够的（第一次在描述符 1 上打开，第二次在描述符 2 上打开） 首先两个文件描述符不能共享相同的文件偏移量指针，因此有可能导致相互覆盖彼此的输出。 再者打开的文件不一定就是磁盘文件。 dup() #include <unistd.h> int dup(int oldfd); // return (new) file descriptor on success, or -1 on error newfd = dup(1); 在正常情况下，shell 已经代表程序打开了文件描述符 0、1 和 2，且没有其他描述符在用，dup () 调用会创建文件描述符 1 的副本，返回的文件描述符编号值为 3。 如果希望返回文件描述符 2，可以使用如下技术： close(2); newfd = dup(1); dup2() 如果想进一步简化上述代码，同时总是能获得所期望的文件描述符，可以调用 dup2 ()。 #include <unistd.h> int dup2(int oldfd, int newfd); // returns (new) file descriptor on success, or -1 on error 如果由 newfd 参数所指定编号的文件描述符之前已经打开，那么 dup2 () 会首先将其关闭。 dup2()调用会默然忽略 newfd 关闭期间出现的任何错误。故此，编码时更为安全的做法是：在调用 dup2 () 之前，若 newfd 已经打开，则应显式调用 close () 将其关闭。 上述代码可以简化为： dup2(1, 2); 若调用 dup2 () 成功，则将返回副本的文件描述符编号（即 newfd 参数指定的值）。 如果 oldfd 并非有效的文件描述符，那么 dup2 () 调用将失败并返回错误 EBADF，且不关闭 newfd。 如果 oldfd 有效，且与 newfd 值相等，那么 dup2 () 将什么也不做，不关闭 newfd，并将其作为调用结果返回。 fcntl() fcntl () 的 F_DUPFD 操作是复制文件描述符的另一接口，更具灵活性 newfd = fcntl(oldfd, F_DUPFD, startfd); 该调用为 oldfd 创建一个副本，且将使用大于等于 startfd 的最小未用值作为描述符编号。该调用还能保证新描述符（newfd）编号落在特定的区间范围内。 dup2 () 和 fcntl () 二者返回的 errno 错误码存在一些差别 dup3() 文件描述符的正、副本之间共享同一打开文件句柄所含的文件偏移量和状态标志。然而，新文件描述符有其自己的一套文件描述符标志，且其 close-on-exec 标志（FD_CLOEXEC）总是处于关闭状态。 dup3()系统调用完成的工作与 dup2()相同，只是新增了一个附加参数 flag，这是一个可以修改系统调用行为的位掩码。 #define _GNU_SOURCE #include <unistd.h> int dup3(int oldfd, int newfd, int flags) // returns (new) file descriptor on success, or -1 on error 目前，dup3 () 只支持一个标志 O_CLOEXEC，这将促使内核为新文件描述符设置 close-on-exec标志（FD_CLOEXEC）。 Linux 从 2.6.24 开始支持 fcntl () 用于复制文件描述符的附加命令：F_DUPFD_CLOEXEC。该标志不仅实现了与 F_DUPFD 相同的功能，还为新文件描述符设置 close-on-exec 标志。 4. 在文件特定偏移量处的I/O：pread() 和 pwrite() 系统调用 pread () 和 pwrite () 完成与 read () 和 write () 相类似的工作，只是前两者会在 offset 参数所指定的位置进行文件 I/O 操作，而非始于文件的当前偏移量处，且它们不会改变文件的当前偏移量。 #include <unistd.h> ssize_t pread(int fd, void *buf, size_t count, off_t offset); // returns number of bytes read, 0 on EOF, or -1 on error ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset); // returns number of bytes written, or -1 on error pread () 调用等同于将如下调用纳入同一原子操作： off_t orig; orig = lseek(fd, 0, SEEK_CUR); // Save current offset lseek(fd, offset, SEEK_SET); s = read(fd, buf, len); lseek(fd, orig, SEEK_SET); // Restore original file offset 对 pread () 和 pwrite () 而言，fd 所指代的文件必须是可定位的（即允许对文件描述符执行lseek () 调用）。 如果需要反复执行 lseek ()，并伴之以文件 I/O，那么 pread () 和 pwrite () 系统调用在某些情况下是具有性能优势的。 5. 分散输入和集中输出：readv() 和 writev() #include <sys/uio.h> ssize_t readv(int fd, const struct iovec *iov, int iovcnt); // returns number of bytes read, 0 on EOF, or -1 on error ssize_t writev(int fd, const struct iovec *iov, int iovcnt); // returns number of bytes written, or -1 on error 这些系统调用并非只对单个缓冲区进行读写操作，而是一次即可传输多个缓冲区的数据。数组 iov 定义了一组用来传输数据的缓冲区。整型数 iovcnt 则指定了 iov 的成员个数。iov 中的每个成员都是如下形式的数据结构。 struct iovec { void *iov_base; // start address of buffer size_t iov_len; // number of bytes to transfer to/from buffer }; 分散输入 readv () 系统调用实现了分散输入的功能：从文件描述符 fd 所指代的文件中读取一片连续的字节，然后将其散置（“分散放置”）于 iov 指定的缓冲区中。 原子性是 readv()的重要属性。从调用进程的角度来看，当调用 readv()时，内核在 fd 所指代的文件与用户内存之间一次性地完成了数据转移。 #include <sys/stat.h> #include <sys/uio.h> #include <fcntl.h> #include "lib/tlpi_hdr.h" #define STR_SIZE 100 int main(int argc, char *argv[]) { int fd; struct iovec iov[3]; struct stat myStruct; // First buffer int x; // Second buffer char str[STR_SIZE]; // Third buffer ssize_t numRead, totRequired; if (argc != 2 || strcmp(argv[1], "--help") == 0) usageErr("%s file\n", argv[0]); fd = open(argv[1], O_RDONLY); if (fd == -1) errExit("open"); iov[0].iov_base = &myStruct; iov[0].iov_len = sizeof(struct stat); totRequired += iov[0].iov_len; iov[1].iov_base = &x; iov[1].iov_len = sizeof(x); totRequired += iov[1].iov_len; iov[2].iov_base = str; iov[2].iov_len = STR_SIZE; totRequired += iov[2].iov_len; numRead = readv(fd, iov, 3); if (numRead == -1) errExit("readv"); if (numRead < totRequired) printf("Read fewer bytes than requested\n"); printf("total bytes requested: %ld; bytes read: %ld\n", (long) totRequired, (long) numRead); exit(EXIT_SUCCESS); } 集中输出 writev()系统调用实现了集中输出：将 iov 所指定的所有缓冲区中的数据拼接（“集中”）起来，然后以连续的字节序列写入文件描述符 fd 指代的文件中。 像 readv()调用一样，writev()调用也属于原子操作，即所有数据将一次性地从用户内存传输到 fd 指代的文件中。 如同 write () 调用，writev () 调用也可能存在部分写的问题。因此，必须检查 writev () 调用的返回值，以确定写入的字节数是否与要求相符。 readv () 调用和 writev () 调用的主要优势在于便捷。 在指定的文件偏移量处执行分散输入/集中输出 #define _BSD_SOURCE #include <sys/uio.h> ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset); // Returns number of bytes read, 0 on EOF, or –1 on error ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset); // Returns number of bytes written, or –1 on error 6. 截断文件：truncate() 和 ftruncate() 系统调用 truncate() 和 ftruncate() 系统调用将文件大小设置为 length 参数指定的值 #include <unistd.h> int truncate(const char *pathname, off_t length); int ftruncate(int fd, off_t length); // Both return 0 on success, or –1 on error 若文件当前长度大于参数 length，调用将丢弃超出部分，若小于参数 length，调用将在文件尾部添加一系列空字节或是一个文件空洞。 两个系统调用之间的差别在于如何指定操作文件： truncate () 以路径名字符串来指定文件，并要求可访问该文件（可执行 x），且对文件拥有写权限。若文件名为符号链接，那么调用将对其进行解用。 调用 ftruncate () 之前，需以可写方式打开操作文件，获取其文件描述符以指代该文件，该系统调用不会修改文件偏移量。 若 ftruncate () 的 length 参数值超出文件的当前大小，SUSv3 允许两种行为：要么扩展该文件（如 Linux），要么返回错误。而符合 XSI 标准的系统则必须采取前一种行为。 truncate () 无需先以 open ()（或是一些其他方法）来获取文件描述符，却可修改文件内容，在系统调用中可谓独树一帜。 7. 非阻塞I/O 在打开文件时指定 O_NONBLOCK 标志，目的有二： 若 open () 调用未能立即打开文件，则返回错误，而非陷入阻塞。有一种情况属于例外，调用 open () 操作 FIFO 可能会陷入阻塞。 调用 open()成功后，后续的 I/O 操作也是非阻塞的。若 I/O 系统调用未能立即完成，则可能会只传输部分数据，或者系统调用失败，并返回 EAGAIN 或 EWOULDBLOCK 错误。具体返回何种错误将依赖于系统调用。Linux 系统与许多 UNIX 实现一样，将两个错误常量视为同义。 管道、FIFO、套接字、设备（比如终端、伪终端）都支持非阻塞模式。 因为无法通过 open () 来获取管道和套接字的文件描述符，所以要启用非阻塞标志，就必须使用 5.3 节所述 fcntl () 的F_SETFL 命令。 由于内核缓冲区保证了普通文件 I/O 不会陷入阻塞，故而打开普通文件时一般会忽略 O_NONBLOCK 标志。然而，当使用强制文件锁时，O_NONBLOCK标志对普通文件也是起作用的。 8. 大文件I/O 通常将存放文件偏移量的数据类型 off_t 实现为一个有符号的长整型。 应用程序可使用如下两种方式之一以获得 LFS 功能： 使用支持大文件操作的备选 API。 在编译应用程序时，将宏_FILE_OFFSET_BITS 的值定义为 64。 要获取 LFS 功能，推荐的作法是：在编译程序时，将宏_FILE_OFFSET_BITS 的值定义为64 做法之一是利用 C 语言编译器的命令行选项： -cc -D_FILE_OFFFSET_BITS=64 prog.c 另外一种方法，是在 C 语言的源文件中，在包含所有头文件之前添加如下宏定义，所有相关的 32 位函数和数据类型将自动转换为 64 位版本。 #define _FILE_OFFFSET_BITS 64 若试图使用 32 位函数访问大文件（即在编译程序时，未将宏_FILE_OFFSET_BITS 的值设置为 64），调用可能会返回 EOVERFLOW 错误。例如，为获取大小超过 2G 文件的信息，若使用 stat 的 32 位版本时就会遇到这一错误。 9. 创建临时文件 mkstemp() 函数生成一个唯一文件名并打开该文件，返回一个可用于 I/O 调用的文件描述符 #include <stdlib.h> int mkstemp(char *template); // return file descriptor on success, or -1 on error 模板参数采用路径名形式，其中最后 6 个字符必须为 XXXXXX。这 6 个字符将被替换，以保证文件名的唯一性，且修改后的字符串将通过 template 参数传回。因为会对传入的 template参数进行修改，所以必须将其指定为字符数组，而非字符串常量。 文件拥有者对 mkstemp() 函数建立的文件拥有读写权限（其他用户则没有任何操作权限），且打开文件时使用了 O_EXCL 标志，以保证调用者以独占方式访问文件。 int fd; char tmp[] = "/tmp/somestringXXXXXX"; fd = mkstemp(tmp); if (fd == -1) errExit("mkstemp"); printf("filename is: %s\n", tmp); unlink(tmp); // name disappears immediately // but the file is removed only afer close() // I/O code if (close(fd) == -1) errExit("close"); 使用 tmpnam ()、tempnam () 和 mktemp () 函数也能生成唯一的文件名。然而，由于这会导致应用程序出现安全漏洞，应当避免使用这些函数。 tmpfile() 函数会创建一个名称唯一的临时文件，并以读写方式将其打开。 打开该文件时使用了 O_EXCL 标志，以防一个可能性极小的冲突，即另一个进程已经创建了一个同名文件。 #include <stdio.h> FILE *tmpfile(void); tmpfile () 函数执行成功，将返回一个文件流供 stdio 库函数使用。文件流关闭后将自动删除临时文件。为达到这一目的，tmpfile () 函数会在打开文件后，从内部立即调用 unlink () 来删除该文件名
本章重点介绍用于文件输入/输出的系统调用。本章开篇会讨论文件描述符的概念，随后会逐一讲解构成通用 I/O 模型的系统调用，其中包括：打开文件、关闭文件、从文件中读数据和向文件中写数据。 所有执行 I/O 操作的系统调用都以文件描述符，一个非负整数（通常是小整数），来指代打开的文件。 文件描述符用以表示所有类型的已打开文件，包括管道（pipe）、FIFO、socket、终端、设备和普通文件。 文件描述符 用途 POSIX 名称 stdio 流 0 标准输入 STDIN_FILENO stdin 1 标准输出 STDOUT_FILENO stdout 2 标准错误 STDERR_FILENO stderr 在程序开始运行之前，shell 代表程序打开这 3 个文件描述符。更确切地说，程序继承了 shell 文件描述符的副本—在 shell 的日常操作中，这 3 个文件描述符始终是打开的。 针对 stdout 调用 freopen () 函数后，无法保证 stdout 变量值仍然为 1 执行文件I/O操作的4个主要系统调用： fd = open(pathname, flags, mode) 打开 pathname 所标识的文件，并返回文件描述符，用以在后续函数调用中指代打开的文件 如果文件不存在，open () 函数可以创建之，这取决于对位掩码参数 flags 的设置 flags 参数可指定文件的打开方式：只读、只写亦或是读写方式 mode 参数则指定了由 open () 调用创建文件的访问权限 numread = read(fd, buffer, count) 调用从 fd 所指代的打开文件中读取至多 count 字节的数据，并存储到 buffer 中 read () 调用的返回值为实际读取到的字节数，无字节可读返回0 numwritten = write(fd, buffer, count) 调用从 buffer 中读取多达 count 字节的数据写入由fd 所指代的已打开文件中 status = close(fd) 释放文件描述符 fd 以及与之相关的内核资源 例子：实现拷贝文件操作 ./copy oldfile newfile #include<sys/stat.h> #include<fcntl.h> #include"lib/tlpi_hdr.h" #ifndef BUF_SIZE // allow "cc -D" to override #define BUF_SIEZ 1024 #endif int main(int argc, char *argv[]) { int inputFd, outputFd, openFlags; mode_t filePerms; ssize_t numRead; char buf[BUF_SIEZ]; if (argc != 3 || strcmp(argv[1], "--help") == 0) usageErr("%s old-file new-file\n", argv[0]); // open input and output files inputFd = open(argv[1], O_RDONLY); if (inputFd == -1) errExit("opening file %s", argv[1]); openFlags = O_CREAT | O_WRONLY | O_TRUNC; filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; // rw-rw-rw outputFd = open(argv[2], openFlags, filePerms); if (outputFd == -1) errExit("opening file %s", argv[2]); // transfer data while ((numRead = (read(inputFd, buf, BUF_SIEZ)) > 0)) { if (write(outputFd, buf, numRead) != numRead) fatal("couldn't write whole buffer"); } if (numRead == -1) errExit("read"); if (close(inputFd) == -1) errExit("close input"); if (close(outputFd) == -1) errExit("close output"); exit(EXIT_SUCCESS); } 1. 打开文件：open () #include<sys/stat.h> #include<fcntl.h> int open(const char *pathname, int flags, ...); // return file descripter on success, or -1 on error 访问模式 描述 O_RDONLY 以只读方式打开文件 O_WRONLY 以只写方式打开文件 O_RDWR 以读写方式打开文件 早期的 UNIX 实现中使用数字 0、1、2，而非表中所列的常量名称。 O_RDWR 并不等同于 O_RDONLY | O_WRONLY，后者（或组合）属于逻辑错误。 当调用 open() 创建新文件时，位掩码参数 mode 指定了文件的访问权限。 新建文件的访问权限不仅仅依赖于参数 mode，而且受到进程的 umask 值和（可能存在的）父目录的默认访问控制列表影响。 open() 调用中的flags参数 常量分为以下几组： 文件访问模式标志：先前描述的 O_RDONLY、O_WRONLY 和 O_RDWR 标志均在此列，调用 open () 时，上述三者在 flags 参数中不能同时使用，只能指定其中一种。调用 fcntl () 的 F_GETFL 操作能够检索文件的访问模式。 文件创建标志：这些标志在表中位于第二部分，其控制范围不拘于 open() 调用行为的方方面面，还涉及后续 I/O 操作的各个选项。这些标志不能检索，也无法修改。 已打开文件的状态标志：这些标志是表中的第三部分，使用 fcntl () 的 F_GETFL 和F_SETFL 操作可以分别检索和修改此类标志。有时干脆将其称之为文件状态标志。 open () 报错信息 EACCES：文件权限不允许调用进程以 flags 参数指定的方式打开文件。 EISDIR：所指定的文件属于目录，而调用者企图打开该文件进行写操作。 EMFILE：进程已打开的文件描述符数量达到了进程资源限制所设定的上限。 ENFILE：文件打开数量已经达到系统允许的上限。 ENOENT：要么文件不存在且未指定 O_CREAT 标志，要么指定了 O_CREAT 标志，但 pathname 参数所指定路径的目录之一不存在，或者 pathname 参数为符号链接，而该链接指向的文件不存在（空链接）。 EROFS：所指定的文件隶属于只读文件系统，而调用者企图以写方式打开文件。 ETXTBSY：所指定的文件为可执行文件（程序），且正在运行。 早期UNIX实现中，open () 只有两个参数无法创建新文件，使用create () xi系统调用来创建并打开一个新文件，但由于 open()的 flags 参数能对文件打开方式提供更多控制，对 creat()的使用现在已不多见 2. 读取文件：read() #include<unistd.h> ssize_t read(int fd, void *buffer, size_t count); // returns number of bytes read, 0, on EOF, or -1 on error count 参数指定最多能读取的字节数。（size_t 数据类型属于无符号整数类型。）buffer 参数提供用来存放输入数据的内存缓冲区地址。缓冲区至少应有 count 个字节。 read ()系统调用不会分配内存缓冲区用以返回信息给调用者。所以，必须预先分配大小合适的缓冲区并将缓冲区指针传递给系统调用。 当 read () 应用于其他文件类型时，比如管道、FIFO、socket 或者终端，在不同环境下会出现 read () 调用读取的字节数小于请求字节数的情况。例如，默认情况下从终端读取字符，一遇到换行符（\n），read () 调用就会结束。 当使用read () 从终端读取一串字符时，需要注意在最后增加结束符，例如以下两个代码： #define MAX_READ 20 char buffer[MAX_READ]; if (read(STDIN_FILENO, buffer, MAX_READ) == -1) errExit("read"); printf("The input data was: %s\n", buffer); char buffer[MAX_READ + 1]; ssize_t numRead; numRead = read(STDIN_FILENO, buffer, MAX_READ); if (numRead == -1) errExit("read"); buffer[numRead] = '\0'; printf("The input data was: %s\n", buffer); 3. 数据写入：write() #include<unistd.h> ssize_t write(int fd, void *buffer, size_t count); // returns number of bytes written, or -1 on error 对磁盘文件执行 I/O 操作时，write () 调用成功并不能保证数据已经写入磁盘。 4. 关闭文件：close() #include<unistd.h> int close(int fd); // returns 0 on success, or -1 on error 像其他所有系统调用一样，应对 close () 的调用进行错误检查，如下所示： if (close(fd) == -1) errExit("close"); 上述代码能够捕获的错误有： 企图关闭一个未打开的文件描述符 或者两次关闭同一文件描述符 捕获特定文件系统在关闭操作中诊断出的错误条件 5. 改变文件偏移量：lseek() #include<unistd.h> off_t lseek(int fd, off_t offset, int whence); // returns new file offset if successful, or -1 on error whence 参数则表明应参照哪个基点来解释 offset 参数，应为下列其中之一： SEEK_SET ：将文件偏移量设置为从文件头部起始点开始的 offset 个字节。 SEEK_CUR ：相对于当前文件偏移量，将文件偏移量调整 offset 个字节。 SEEK_END ：将文件偏移量设置为起始于文件尾部的 offset 个字节。 如果 whence 参数值为 SEEK_CUR 或 SEEK_END，offset 参数可以为正数也可以为负数； 如果 whence 参数值为 SEEK_SET，offset 参数值必须为非负数。 lseek()并不适用于所有类型的文件。不允许将 lseek()应用于管道、FIFO、socket 或者终端。一旦如此，调用将会失败，并将 errno 置为 ESPIPE。 从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看，文件空洞中是存在字节的，读取空洞将返回以 0（空字节）填充的缓冲区。 6. 通用 I/O 模型以外的操作：ioctl() #include <sys/ioctl.h> int ioctl(int fd, int request, ...); // return on success depends on request, or -1 on error 7. 综合示例 该程序的第一个命令行参数为将要打开的文件名称，余下的参数则指定了在文件上执行的输入/输出操作。每个表示操作的参数都以一个字母开头，紧跟以相关值（中间无空格分隔）。 soffset：从文件开始检索到 offset 字节位置。 rlength：在当前文件偏移量处，从文件中读取 length 字节数据，并以文本形式显示。 Rlength：在当前文件偏移量处，从文件中读取 length 字节数据，并以十六进制形式显示。 wstr：在当前文件偏移量处，向文件写入由 str 指定的字符串。 #include "lib/tlpi_hdr.h" #include <ctype.h> #include <fcntl.h> #include <sys/stat.h> int main(int argc, char *argv[]) { size_t len; off_t offset; int fd, ap, j; unsigned char *buf; ssize_t numRead, numWritten; // command wrong or help if (argc < 3 || strcmp(argv[1], "--help") == 0) usageErr("%s file {r<length>|R<length>|w<string>|s<offset>}...\n", argv[0]); int openFlags = O_CREAT | O_RDWR; mode_t filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; // rw-rw-rw fd = open(argv[1], openFlags, filePerms); if (fd == -1) errExit("open"); for (ap = 2; ap < argc; ++ap) { switch (argv[ap][0]) { case 'r': len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]); buf = malloc(len); if (buf == NULL) errExit("malloc"); numRead = read(fd, buf, len); if (numRead == -1) errExit("read"); else if (numRead == 0) printf("%s: end-of-file\n", argv[ap]); else { printf("%s: ", argv[ap]); for (j = 0; j < len; ++j) printf("%c", isprint(buf[j]) ? buf[j] : '?'); } printf("\n"); free(buf); break; case 'R': len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]); buf = malloc(len); if (buf == NULL) errExit("malloc"); numRead = read(fd, buf, len); if (numRead == -1) errExit("read"); else if (numRead == 0) printf("%s: end-of-file\n", argv[ap]); else { printf("%s: ", argv[ap]); for (j = 0; j < len; ++j) printf("%02x ", buf[j]); } printf("\n"); free(buf); break; case 'w': numWritten = write(fd, &argv[ap][1], strlen(&argv[ap][1])); if (numWritten == -1) errExit("write"); printf("%s: wrote %ld bytes\n", argv[ap], (long) numWritten); break; case 's': offset = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]); if (lseek(fd, offset, SEEK_SET) == -1) errExit("lseek"); printf("%s: seek succeeded\n", argv[ap]); break; default: cmdLineErr("Argument must start with [rRws]: %s\n", argv[ap]); break; } } if (close(fd) == -1) errExit("close"); exit(EXIT_SUCCESS); }
1. 系统调用 以x86平台为例，分析系统调用： 应用程序通过调用 C 语言函数库中的外壳（wrapper）函数，来发起系统调用 对系统调用中断处理例程来说，外壳函数必须保证所有的系统调用参数可用。通过堆栈，这些参数传入外壳函数，但内核却希望将这些参数置入特定寄存器。因此，外壳函数会将上述参数复制到寄存器。 由于所有系统调用进入内核的方式相同，内核需要设法区分每个系统调用。为此，外壳函数会将系统调用编号复制到一个特殊的 CPU 寄存器（%eax）中。 外壳函数执行一条中断机器指令（int 0x80），引发处理器从用户态切换到核心态，并执行系统中断 0x80 (十进制数 128) 的中断矢量所指向的代码。 较新的 x86-32 硬件平台实现了 sysenter 指令，较之传统的 int 0x80 中断指令，sysenter指令进入内核的速度更快。2.6 内核及 glibc 2.3.2 以后的版本都支持 sysenter 指令。 为响应中断 0x80，内核会调用 system_call () 例程（位于汇编文件 arch/i386/entry.S 中）来处理这次中断，具体如下： 在内核栈中保存寄存器值。 审核系统调用编号的有效性。 以系统调用编号对存放所有调用服务例程的列表（内核变量 sys_call_table）进行索引，发现并调用相应的系统调用服务例程。若系统调用服务例程带有参数，那么将首先检查参数的有效性。例如，会检查地址指向用户空间的内存位置是否有效。随后，该服务例程会执行必要的任务，这可能涉及对特定参数中指定地址处的值进行修改，以及在用户内存和内核内存间传递数据（比如，在 I/O 操作中）。最后，该服务例程会将结果状态返回给 system_call () 例程。 从内核栈中恢复各寄存器值，并将系统调用返回值置于栈中。 返回至外壳函数，同时将处理器切换回用户态。 若系统调用服务例程的返回值表明调用有误，外壳函数会使用该值来设置全局变量 errno。然后，外壳函数会返回到调用程序，并同时返回一个整型值，以表明系统调用是否成功。 在 Linux 上，系统调用服务例程遵循的惯例是调用成功则返回非负值。发生错误时，例程会对相应 errno 常量取反，返回一负值。C 语言函数库的外壳函数随即对其再次取反（负负得正），将结果拷贝至 errno，同时以-1 作为外壳函数的返回值返回，向调用程序表明有错误发生。 上述惯例所依赖的前提条件是系统调用服务例程，若调用成功则不会返回负值。可是，对于少数例程来说，这一前提并不成立。一般情况下，这也不会有问题，因为取反的 errno值范围不会与调用成功返回负值的范围有交集。不过，有一种情况，沿用这个惯例确实会出问题：系统调用 fcntl () 的 F_GETOWN 操作。 2. 处理来自系统调用和库函数的错误 少数几个系统函数在调用时从不失败。例如，getpid() 总能成功返回进程的 ID，而 _exit() 总能终止进程。无需对此类系统调用的返回值进行检查。 处理系统调用错误 每个系统调用的手册页记录有调用可能的返回值，并指出了哪些值表示错误。通常，返回值为-1 表示出错。因此，可使用下列代码对系统调用进行检查： // system call to open a file fd = open(pathname, flags, mode); if (fd == -1) { // Code to handle the error } // ... if (close(fd) == -1) { // Code to handle the error } 系统调用失败时，会将全局整形变量 errno 设置为一个正值，以标识具体的错误。如果调用系统调用和库函数成功，errno 绝不会被重置为 0，故此，该变量值不为 0，可能是之前调用失败造成的。因此，在进行错误检查时，必须坚持首先检查函数的返回值是否表明调用出错，然后再检查 errno 确定错误原因。 少数系统调用（比如，getpriority ()）在调用成功后，也会返回−1。 系统调用失败后，常见的做法之一是根据 errno 值打印错误消息。提供库函数 perror() 和 strerror ()，就是出于这一目的。 函数 perror() 会打印出其 msg 参数所指向的字符串，紧跟一条与当前 errno 值相对应的消息。 函数 strerror() 会针对其 errnum 参数中所给定的错误号，返回相应的错误字符串。 #include<stdio.h> void perror(const char *msg); #include<string.h> char *strerror(int errnum); 例如： fd = open(pathname, flags, mode); if (fd == -1) { perror("open"); exit(EXIT_FAILURE); } 3. 错误诊断函数 具体代码见项目，以下为部分函数及相关解释： #include "tlpi_hdr.h" void errMsg(const char *format, ...); void errExit(const char *format, ...); void err_exit(const char *format, ...); void errExitEN(int errnum, const char *format, ...); errMsg() ：能够在标准错误设备上打印信息。会打印出与当前errno值相对应的错误文本，其中包含了错误名以及由 strerror() 返回的错误描述，外加由参数列表指定的格式化输出。 errExit() ：操作方式与 errMsg() 相似，只是还会以如下两种方式之一来终止程序。 其一，调用 exit () 退出。 其二，若将环境变量 EF_DUMPCORE 定义为非空字符串，则调用 abort () 退出，同时生成核心转储（core dump）文件，供调试器调试之用。 err_exit() ：类似于 errExit()，但存在两方面的差异 打印错误消息之前，err_exit () 不会刷新标准输出。 err_exit () 终止进程使用的是_exit ()，而非 exit ()。略去了对 stdio 缓冲区的刷新以及对退出处理程序（exit handler）的调用。 errExitEN() ：与 errExit() 大体相同，区别仅仅在于：与 errExit() 打印与当前 errno 值相对应的错误文本不同，errExitEN() 只会打印与 errnum 参数中给定的错误号 #include"tlpi_hdr.h" void fatal(const char *format, ...); void usageErr(const char *format, ...); void cmdLineErr(const char *format, ...); fatal() 用来诊断一般性错误，其中包括未设置 errno 的库函数错误。除了将一个终止换行符自动追加到输出字符串尾部以外，fatal () 的参数列表与 printf () 基本相同。该函数会在标准错误上打印格式化输出，然后，像 errExit () 那样终止程序。 usageErr() 用来诊断命令行参数使用方面的错误。其参数列表风格与 printf () 相同，并在标准错误上打印字符串“Usage：”，随之以格式化输出，然后调用 exit () 终止程序。 cmdLineErr() 酷似 usageErr ()，但其错误诊断是针对于特定程序的命令行参数。 3.1 tlpi_hdr.h #ifndef TLPI_HDR_H #define TLPI_HDR_H #include<sys/types.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> // Prototypes for many system calls #include<errno.h> #include<string.h> #include"get_num.h" #include"error_functions.h" typedef enum { false, true } boolean; #define min(m, n) ((m) < (n) ? (m) : (n)) #define max(m, n) ((m) > (n) ? (m) : (n)) #endif 3.2 error_functions.h #ifndef ERROR_FUNCTIONS_H #define ERROR_FUNCTIONS_H // errMsg() 在标准错误设备上打印信息 // 会打印出与当前errno值相对应的错误文本 void errMsg(const char *format, ...); #ifdef __GNUC__ // 检查是否是gcc环境 #define NORETURN __attribute__ ((__noreturn__)) // 改变函数的属性，代表这个自己编写的函数在进行GCC编译的时候声明没有返回值 #else #define NORETURN #endif void errExit(const char *format, ...) NORETURN; void err_exit(const char *format, ...) NORETURN; void errExitEN(int errnum, const char *format, ...) NORETURN; void fatal(const char *format, ...) NORETURN; void usageErr(const char *format, ...) NORETURN; void cmdLineErr(const char *format, ...) NORETURN; #endif 3.3 error_functions.c #include "error_functions.h" #include "ename.c.inc" // defines ename and MAX_ENAME #include "tlpi_hdr.h" #include <stdarg.h> #ifdef __GUNC__ __attribute__((__noreturn)) #endif static void terminate(boolean useExit3) { char *s; // if EF_DUMPCORE environment variable // is defined and is nonempty string s = getenv("EF_DUMPCORE"); if (s != NULL && *s != '\0') abort(); // Dump core else if (useExit3) exit(EXIT_FAILURE); // exit(3) else _exit(EXIT_FAILURE); // _exit(2) // _exit(2) would cause exit handlers to be invoked } // Diagnoes 'errno' error: // output the error name and the corresponding error message from strerror() // output the caller-supplied error message specified in 'format' and 'ap' static void outputError(boolean useErr, int err, boolean flushStdout, const char *format, va_list ap) { #define BUF_SIZE 500 char buf[BUF_SIZE], userMsg[BUF_SIZE], errText[BUF_SIZE]; vsnprintf(userMsg, BUF_SIZE, format, ap); if (useErr) snprintf(errText, BUF_SIZE, " [%s %s]", (err > 0 && err <= MAX_ENAME) ? ename[err] : "?UNKNOWN?", strerror(err)); else snprintf(errText, BUF_SIZE, ":"); #if __GNUC__ >= 7 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-truncation" #endif snprintf(buf, BUF_SIZE, "ERROR%s %s\n", errText, userMsg); #if __GNUC__ >= 7 #pragma GCC diagnostic pop #endif if (flushStdout) fflush(stdout); // flush any pending stdout fputs(buf, stderr); fflush(stderr); // In case stderr is not line-buffered } // display error massage including 'errno', and return to caller void errMsg(const char *format, ...) { va_list argList; int savedErrno; savedErrno = errno; va_start(argList, format); outputError(true, errno, true, format, argList); va_end(argList); errno = savedErrno; } // display error massage including 'errno', and terminate the process void errExit(const char *format, ...) { va_list argList; va_start(argList, format); outputError(true, errno, true, format, argList); va_end(argList); terminate(true); } // display error massage including 'errno', and _exit(2) /* * These differences make this function especially useful in a library * function that creates a child process that must then terminate * because of an error: the child must terminate without flushing * stdio buffers that were partially filled by the caller and without * invoking exit handlers that were established by the caller. */ void err_exit(const char *format, ...) { va_list argList; va_start(argList, format); outputError(true, errno, false, format, argList); va_end(argList); terminate(false); } // expects the error number in 'errnum', other same as errExit() void errExitEN(int errnum, const char *format, ...) { va_list argList; va_start(argList, format); outputError(true, errnum, true, format, argList); va_end(argList); terminate(false); } // print an error message (without an 'errno' diagnostic) void fatal(const char *format, ...) { va_list argList; va_start(argList, format); outputError(false, 0, true, format, argList); va_end(argList); terminate(true); } // print a command usage error message and terminate the process void usageErr(const char *format, ...) { va_list argList; fflush(stdout); // Flush any pending stdout fprintf(stderr, "Usage: "); va_start(argList, format); vfprintf(stderr, format, argList); va_end(argList); fflush(stderr); // In case stderr is not line-buffered exit(EXIT_FAILURE); } // Diagnose an error in command-line arguments and terminate the process void cmdLineErr(const char *format, ...) { va_list argList; fflush(stdout); // Flush any pending stdout fprintf(stderr, "Command-line usage error: "); va_start(argList, format); vfprintf(stderr, format, argList); va_end(argList); fflush(stderr); // In case stderr is not line-buffered exit(EXIT_FAILURE); } 3.4 get_num.h #ifndef GET_NUM_H #define GET_NUM_H #define GN_NONNEG 01 /* Value must be >= 0 */ #define GN_GT_0 02 /* Value must be > 0 */ /* By default, integers are decimal */ #define GN_ANY_BASE 0100 /* Can use any base - like strtol(3) */ #define GN_BASE_8 0200 /* Value is expressed in octal */ #define GN_BASE_16 0400 /* Value is expressed in hexadecimal */ long getLong(const char *arg, int flags, const char *name); int getInt(const char *arg, int flags, const char *name); #endif 3.5 get_num.c #include "get_num.h" #include <errno.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> /* Print a diagnostic message that contains a function name ('fname'), the value of a command-line argument ('arg'), the name of that command-line argument ('name'), and a diagnostic error message ('msg'). */ static void gnFail(const char *fname, const char *msg, const char *arg, const char *name) { fprintf(stderr, "%s error", fname); if (name != NULL) fprintf(stderr, " (in %s)", name); fprintf(stderr, ": %s\n", msg); if (arg != NULL && *arg != '\0') fprintf(stderr, " offending text: %s\n", arg); exit(EXIT_FAILURE); } /* Convert a numeric command-line argument ('arg') into a long integer, returned as the function result. 'flags' is a bit mask of flags controlling how the conversion is done and what diagnostic checks are performed on the numeric result; see get_num.h for details. 'fname' is the name of our caller, and 'name' is the name associated with the command-line argument 'arg'. 'fname' and 'name' are used to print a diagnostic message in case an error is detected when processing 'arg'. */ static long getNum(const char *fname, const char *arg, int flags, const char *name) { long res; char *endptr; int base; if (arg == NULL || *arg == '\0') gnFail(fname, "null or empty string", arg, name); base = (flags & GN_ANY_BASE) ? 0 : (flags & GN_BASE_8) ? 8 : (flags & GN_BASE_16) ? 16 : 10; errno = 0; res = strtol(arg, &endptr, base); if (errno != 0) gnFail(fname, "strtol() failed", arg, name); if (*endptr != '\0') gnFail(fname, "nonnumeric characters", arg, name); if ((flags & GN_NONNEG) && res < 0) gnFail(fname, "negative value not allowed", arg, name); if ((flags & GN_GT_0) && res <= 0) gnFail(fname, "value must be > 0", arg, name); return res; } /* Convert a numeric command-line argument string to a long integer. See the comments for getNum() for a description of the arguments to this function. */ long getLong(const char *arg, int flags, const char *name) { return getNum("getLong", arg, flags, name); } /* Convert a numeric command-line argument string to an integer. See the comments for getNum() for a description of the arguments to this function. */ int getInt(const char *arg, int flags, const char *name) { long res; res = getNum("getInt", arg, flags, name); if (res > INT_MAX || res < INT_MIN) gnFail("getInt", "integer out of range", arg, name); return res; }
出现原因：CNN不能解决图像放大缩小和旋转的问题（即数据增强问题） Spatial Transformer Layer是在CNN前又叠了一个Neuron Network 它不仅可以transform input layer，也可以被放在CNN里面，transform feature map 那么如何寻找weight呢？ 放大缩小： 旋转： 如果只旋转、平移、缩放的话，只需要六个参数 例子如下： 但我们不能直接设为最近的点，因为无法微分，gradient为0，所以我们使用双线性插值，这样就构成连续函数