20150419 IMX257虚拟网卡vnet驱动程序
2015-04-19 Lover雪儿
一、一个简单的虚拟网卡驱动
1 static struct net_device *vnet_dev; //定义一个网络设备结构体 2 3 4 static const struct net_device_ops virt_netdev_ops = { 5 .ndo_start_xmit = NULL, 6 }; 7 8 9 static int virt_net_init(void){ 10 11 /* 1.分配一个net_device结构体 */ 12 vnet_dev = alloc_netdev(0,"vnet%d",ether_setup); //私有数据为0, 13 /* 2.设置 */ 14 vnet_dev->netdev_ops = &virt_netdev_ops; 15 /* 3.注册 */ 16 //register_netdevice(vnet_dev); //会报锁的错误 17 register_netdev(vnet_dev); //rtnl_lock(); 18 return 0; 19 } 20 21 static void virt_net_exit(void){ 22 if(vnet_dev){ 23 unregister_netdev(vnet_dev); 24 free_netdev(vnet_dev); 25 } 26 } 27 28 module_init(virt_net_init); 29 module_exit(virt_net_exit); 30 MODULE_AUTHOR("Lover雪儿"); 31 MODULE_LICENSE("GPL");
如上面程序所示,定义分配一个net_device结构体,注册结构体,这样就实现了一个简单的虚拟网卡驱动。
实现效果:
如图所示,在/sys/class/net/目录下多生成了一个vnet0的文件夹
附驱动程序virt_net1.c
1 #include <linux/errno.h> 2 #include <linux/netdevice.h> 3 #include <linux/etherdevice.h> 4 #include <linux/kernel.h> 5 #include <linux/types.h> 6 #include <linux/fcntl.h> 7 #include <linux/interrupt.h> 8 #include <linux/ioport.h> 9 #include <linux/in.h> 10 #include <linux/skbuff.h> 11 #include <linux/slab.h> 12 #include <linux/spinlock.h> 13 #include <linux/string.h> 14 #include <linux/init.h> 15 #include <linux/bitops.h> 16 #include <linux/delay.h> 17 18 #include <asm/system.h> 19 #include <asm/io.h> 20 #include <asm/irq.h> 21 22 static struct net_device *vnet_dev; //定义一个网络设备结构体 23 24 25 static const struct net_device_ops virt_netdev_ops = { 26 .ndo_start_xmit = NULL, 27 }; 28 29 30 static int virt_net_init(void){ 31 32 /* 1.分配一个net_device结构体 */ 33 vnet_dev = alloc_netdev(0,"vnet%d",ether_setup); //私有数据为0, 34 /* 2.设置 */ 35 vnet_dev->netdev_ops = &virt_netdev_ops; 36 /* 3.注册 */ 37 //register_netdevice(vnet_dev); //会报锁的错误 38 register_netdev(vnet_dev); //rtnl_lock(); 39 return 0; 40 } 41 42 static void virt_net_exit(void){ 43 if(vnet_dev){ 44 unregister_netdev(vnet_dev); 45 free_netdev(vnet_dev); 46 } 47 } 48 49 module_init(virt_net_init); 50 module_exit(virt_net_exit); 51 MODULE_AUTHOR("Lover雪儿"); 52 MODULE_LICENSE("GPL"); 53 54 55 /* 56 57 / 58 网卡驱动程序框架: 59 app: socket 60 -------------------------------------------------------------------- 61 ----------------- 62 ----------------- 若干层网络协议--纯软件 63 ----------------- 64 hard_start_xmit() 用于发送数据包 ↑↑↑ 65 ↓↓↓ sk_buff netif_rx()用于上报数据包 66 调用硬件相关的驱动程序(要提供har_start_xmit,有数据时用netif_rx上报) 67 -------------------------------------------------------------------- 68 硬件 69 // 70 71 网卡驱动编写: 72 1.分配一个net_device结构体 73 2.设置 74 2.1 发包函数:hard_start_xmit 75 2.2 收到到数据时,(在中断处理函数中)用netif_rx上报数据 76 2.3 其他设置 77 3.注册 register_netdevice 78 79 80 一、测试1th 81 insmod virt_net.ko 82 ifconfig vnet0 3.3.3.3 83 ping 3.3.3.3 84 ping 3.3.3.4 看效果 85 86 87 */
二、增加ping功能
下面是我画的一个网卡驱动程序框架。
所以此处我们增加一个发送数据包的函数 hard_start_xmit用于转发数据包。
1.在net_device结构体下定义hard_start_xmit。
vnet_dev->hard_start_xmit = vir_net_send_packet;
2.实现vir_net_send_packet函数
此处我们的代码只是增加一些打印语句,以便确定当我们ping的时候,是否会调用该函数。
1 //转发发送数据包 2 static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev){ 3 static int cnt = 0; 4 printk("vir_net_send_packet cnt = %d\n",++cnt); 5 return 0; 6 } 7 8 static const struct net_device_ops virt_netdev_ops = { 9 .ndo_start_xmit = virt_net_send_packet, 10 }; 11 //接着在入口函数中增加net_device_ops 12 13 vnet_dev->netdev_ops = &virt_netdev_ops;
实现效果:
ping 测试:
附驱动程序virt_net2.c
1 #include <linux/errno.h> 2 #include <linux/netdevice.h> 3 #include <linux/etherdevice.h> 4 #include <linux/kernel.h> 5 #include <linux/types.h> 6 #include <linux/fcntl.h> 7 #include <linux/interrupt.h> 8 #include <linux/ioport.h> 9 #include <linux/in.h> 10 #include <linux/skbuff.h> 11 #include <linux/slab.h> 12 #include <linux/spinlock.h> 13 #include <linux/string.h> 14 #include <linux/init.h> 15 #include <linux/bitops.h> 16 #include <linux/delay.h> 17 18 #include <asm/system.h> 19 #include <asm/io.h> 20 #include <asm/irq.h> 21 22 static struct net_device *vnet_dev; //定义一个网络设备结构体 23 24 //转发发送数据包 25 static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev){ 26 static int cnt = 0; 27 printk("vir_net_send_packet cnt = %d\n",++cnt); 28 return 0; 29 } 30 31 static const struct net_device_ops virt_netdev_ops = { 32 .ndo_start_xmit = virt_net_send_packet, 33 }; 34 35 36 static int virt_net_init(void){ 37 38 /* 1.分配一个net_device结构体 */ 39 vnet_dev = alloc_netdev(0,"vnet%d",ether_setup); //私有数据为0, 40 /* 2.设置 */ 41 vnet_dev->netdev_ops = &virt_netdev_ops; 42 /* 3.注册 */ 43 //register_netdevice(vnet_dev); //会报锁的错误 44 register_netdev(vnet_dev); //rtnl_lock(); 45 return 0; 46 } 47 48 static void virt_net_exit(void){ 49 if(vnet_dev){ 50 unregister_netdev(vnet_dev); 51 free_netdev(vnet_dev); 52 } 53 } 54 55 module_init(virt_net_init); 56 module_exit(virt_net_exit); 57 MODULE_AUTHOR("Lover雪儿"); 58 MODULE_LICENSE("GPL"); 59 60 61 /* 62 63 / 64 网卡驱动程序框架: 65 app: socket 66 -------------------------------------------------------------------- 67 ----------------- 68 ----------------- 若干层网络协议--纯软件 69 ----------------- 70 hard_start_xmit() 用于发送数据包 ↑↑↑ 71 ↓↓↓ sk_buff netif_rx()用于上报数据包 72 调用硬件相关的驱动程序(要提供har_start_xmit,有数据时用netif_rx上报) 73 -------------------------------------------------------------------- 74 硬件 75 // 76 77 网卡驱动编写: 78 1.分配一个net_device结构体 79 2.设置 80 2.1 发包函数:hard_start_xmit 81 2.2 收到到数据时,(在中断处理函数中)用netif_rx上报数据 82 2.3 其他设置 83 3.注册 register_netdevice 84 85 86 一、测试1th 87 insmod virt_net.ko 88 ifconfig vnet0 3.3.3.3 89 ping 3.3.3.3 90 ping 3.3.3.4 看效果 91 92 */
三、增加数据包的统计,设置MAC地址
有关于数据包的统计信息定义在net_device结构体的stats结构体下,主要是定义了收发数据包的个数,字节数,时间等。此处,我们就增加一个统计发送数据包的个数,以及字节数。
stats结构体如下:
发送数据包的统计 struct net_device_stats { unsigned long rx_packets; / * total packets received * / unsigned long tx_packets; / * total packets transmitted * / unsigned long rx_bytes; / * total bytes received * / unsigned long tx_bytes; / * total bytes transmitted * / unsigned long rx_errors; / * bad packets received * / unsigned long tx_errors; / * packet transmit probls * / unsigned long rx_dropped; / * no space in linux buffers * / unsigned long tx_dropped; / * no space available in linux * / unsigned long multicast; / * multicast packets received * / unsigned long collisions; / * detailed rx_errors: * / unsigned long rx_length_errors; unsigned long rx_over_errors; / * receiver ring buff overflow * / unsigned long rx_crc_errors; / * recved pkt with crc error * / unsigned long rx_frame_errors; / * recv'd frame alignment error * / unsigned long rx_fifo_errors; / * recv'r fifo overrun * / unsigned long rx_missed_errors; / * receiver missed packet * / / * detailed tx_errors * / unsigned long tx_aborted_errors; unsigned long tx_carrier_errors; unsigned long tx_fifo_errors; unsigned long tx_heartbeat_errors; unsigned long tx_window_errors; / * for cslip etc * / unsigned long rx_compressed; unsigned long tx_compressed; };
接下来就是在virt_net_send_packet实现数据包的自加。
1 static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev) 2 { 3 static int cnt = 0; 4 printk("virt_net_send_packet cnt = %d\n", ++cnt); 5 6 /* 更新统计信息 */ 7 dev->stats.tx_packets++; 8 dev->stats.tx_bytes += skb->len; 9 10 return 0; 11 }
设置mac地址
1 static int virt_net_init(void) 2 { 3 /* 1. 分配一个net_device结构体 */ 4 vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);; /* alloc_etherdev */ 5 6 /* 2. 设置 */ 7 vnet_dev->netdev_ops = &virt_netdev_ops; 8 9 /* 设置MAC地址 */ 10 vnet_dev->dev_addr[0] = 0x08; 11 vnet_dev->dev_addr[1] = 0x89; 12 vnet_dev->dev_addr[2] = 0x89; 13 vnet_dev->dev_addr[3] = 0x89; 14 vnet_dev->dev_addr[4] = 0x89; 15 vnet_dev->dev_addr[5] = 0x11; 16 17 /* 设置下面两项才能ping通 */ 18 //vnet_dev->flags |= IFF_NOARP; 19 //vnet_dev->features |= NETIF_F_NO_CSUM; 20 21 /* 3. 注册 */ 22 //register_netdevice(vnet_dev); 23 register_netdev(vnet_dev); 24 25 return 0; 26 }
实现效果:
附驱动程序virt_net3.c
1 #include <linux/module.h> 2 #include <linux/errno.h> 3 #include <linux/netdevice.h> 4 #include <linux/etherdevice.h> 5 #include <linux/kernel.h> 6 #include <linux/types.h> 7 #include <linux/fcntl.h> 8 #include <linux/interrupt.h> 9 #include <linux/ioport.h> 10 #include <linux/in.h> 11 #include <linux/skbuff.h> 12 #include <linux/slab.h> 13 #include <linux/spinlock.h> 14 #include <linux/string.h> 15 #include <linux/init.h> 16 #include <linux/bitops.h> 17 #include <linux/delay.h> 18 #include <linux/ip.h> 19 20 #include <asm/system.h> 21 #include <asm/io.h> 22 #include <asm/irq.h> 23 24 static struct net_device *vnet_dev; 25 26 27 static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev) 28 { 29 static int cnt = 0; 30 printk("virt_net_send_packet cnt = %d\n", ++cnt); 31 32 /* 更新统计信息 */ 33 dev->stats.tx_packets++; 34 dev->stats.tx_bytes += skb->len; 35 36 return 0; 37 } 38 39 static const struct net_device_ops virt_netdev_ops = { 40 .ndo_start_xmit = virt_net_send_packet, 41 }; 42 43 static int virt_net_init(void) 44 { 45 /* 1. 分配一个net_device结构体 */ 46 vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);; /* alloc_etherdev */ 47 48 /* 2. 设置 */ 49 vnet_dev->netdev_ops = &virt_netdev_ops; 50 51 /* 设置MAC地址 */ 52 vnet_dev->dev_addr[0] = 0x08; 53 vnet_dev->dev_addr[1] = 0x89; 54 vnet_dev->dev_addr[2] = 0x89; 55 vnet_dev->dev_addr[3] = 0x89; 56 vnet_dev->dev_addr[4] = 0x89; 57 vnet_dev->dev_addr[5] = 0x11; 58 59 /* 设置下面两项才能ping通 */ 60 //vnet_dev->flags |= IFF_NOARP; 61 //vnet_dev->features |= NETIF_F_NO_CSUM; 62 63 /* 3. 注册 */ 64 //register_netdevice(vnet_dev); 65 register_netdev(vnet_dev); 66 67 return 0; 68 } 69 70 static void virt_net_exit(void) 71 { 72 unregister_netdev(vnet_dev); 73 free_netdev(vnet_dev); 74 } 75 76 module_init(virt_net_init); 77 module_exit(virt_net_exit); 78 MODULE_AUTHOR("Lover雪儿"); 79 MODULE_LICENSE("GPL"); 80 81 82 /* 83 84 / 85 网卡驱动程序框架: 86 app: socket 87 -------------------------------------------------------------------- 88 ----------------- 89 ----------------- 若干层网络协议--纯软件 90 ----------------- 91 hard_start_xmit() 用于发送数据包 ↑↑↑ 92 ↓↓↓ sk_buff netif_rx()用于上报数据包 93 调用硬件相关的驱动程序(要提供har_start_xmit,有数据时用netif_rx上报) 94 -------------------------------------------------------------------- 95 硬件 96 // 97 98 网卡驱动编写: 99 1.分配一个net_device结构体 100 2.设置 101 2.1 发包函数:hard_start_xmit 102 2.2 收到到数据时,(在中断处理函数中)用netif_rx上报数据 103 2.3 其他设置 104 3.注册 register_netdevice 105 106 107 一、测试1th 108 insmod virt_net.ko 109 ifconfig vnet0 3.3.3.3 110 ping 3.3.3.3 111 ping 3.3.3.4 看效果 112 113 */
四、模拟回环网卡程序,实现数据包的收发过程
前面我们已经实现了转发数据包的功能,此处我们增加一个接受数据包的功能。
如程序中所示,主要就是交换数据包中的源目的MAC头信息,源目的IP信息等功能。
1 static void emulator_rx_packet(struct sk_buff *skb, struct net_device *dev) 2 { 3 /* 参考LDD3 */ 4 unsigned char *type; 5 struct iphdr *ih; 6 __be32 *saddr, *daddr, tmp; 7 unsigned char tmp_dev_addr[ETH_ALEN]; 8 struct ethhdr *ethhdr; 9 10 struct sk_buff *rx_skb; 11 12 // 从硬件读出/保存数据 13 /* 对调"源/目的"的mac地址 */ 14 ethhdr = (struct ethhdr *)skb->data; 15 memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN); 16 memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN); 17 memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN); 18 19 /* 对调"源/目的"的ip地址 */ 20 ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr)); 21 saddr = &ih->saddr; 22 daddr = &ih->daddr; 23 24 tmp = *saddr; 25 *saddr = *daddr; 26 *daddr = tmp; 27 28 //((u8 *)saddr)[2] ^= 1; /* change the third octet (class C) */ 29 //((u8 *)daddr)[2] ^= 1; 30 type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr); 31 //printk("tx package type = %02x\n", *type); 32 // 修改类型, 原来0x8表示ping 33 *type = 0; /* 0表示reply */ 34 35 ih->check = 0; /* and rebuild the checksum (ip needs it) */ 36 ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl); 37 38 // 构造一个sk_buff 39 rx_skb = dev_alloc_skb(skb->len + 2); 40 skb_reserve(rx_skb, 2); /* align IP on 16B boundary */ 41 memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len); 42 43 /* Write metadata, and then pass to the receive level */ 44 rx_skb->dev = dev; 45 rx_skb->protocol = eth_type_trans(rx_skb, dev); 46 rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */ 47 dev->stats.rx_packets++; 48 dev->stats.rx_bytes += skb->len; 49 50 // 提交sk_buff 51 netif_rx(rx_skb); 52 }
接着在发送数据包函数中:
1 static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev) 2 { 3 static int cnt = 0; 4 //printk("virt_net_send_packet cnt = %d\n", ++cnt); 5 6 /* 对于真实的网卡, 把skb里的数据通过网卡发送出去 */ 7 netif_stop_queue(dev); /* 停止该网卡的队列 */ 8 /* ...... */ /* 把skb的数据写入网卡 */ 9 10 /* 构造一个假的sk_buff,上报 */ 11 emulator_rx_packet(skb, dev); 12 13 dev_kfree_skb (skb); /* 释放skb */ 14 netif_wake_queue(dev); /* 数据全部发送出去后,唤醒网卡的队列 */ 15 16 /* 更新统计信息 */ 17 dev->stats.tx_packets++; 18 dev->stats.tx_bytes += skb->len; 19 20 return 0; 21 }
实现效果:
附驱动程序virt_net4.c
1 #include <linux/module.h> 2 #include <linux/errno.h> 3 #include <linux/netdevice.h> 4 #include <linux/etherdevice.h> 5 #include <linux/kernel.h> 6 #include <linux/types.h> 7 #include <linux/fcntl.h> 8 #include <linux/interrupt.h> 9 #include <linux/ioport.h> 10 #include <linux/in.h> 11 #include <linux/skbuff.h> 12 #include <linux/slab.h> 13 #include <linux/spinlock.h> 14 #include <linux/string.h> 15 #include <linux/init.h> 16 #include <linux/bitops.h> 17 #include <linux/delay.h> 18 #include <linux/ip.h> 19 20 #include <asm/system.h> 21 #include <asm/io.h> 22 #include <asm/irq.h> 23 24 static struct net_device *vnet_dev; 25 26 static void emulator_rx_packet(struct sk_buff *skb, struct net_device *dev) 27 { 28 /* 参考LDD3 */ 29 unsigned char *type; 30 struct iphdr *ih; 31 __be32 *saddr, *daddr, tmp; 32 unsigned char tmp_dev_addr[ETH_ALEN]; 33 struct ethhdr *ethhdr; 34 35 struct sk_buff *rx_skb; 36 37 // 从硬件读出/保存数据 38 /* 对调"源/目的"的mac地址 */ 39 ethhdr = (struct ethhdr *)skb->data; 40 memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN); 41 memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN); 42 memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN); 43 44 /* 对调"源/目的"的ip地址 */ 45 ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr)); 46 saddr = &ih->saddr; 47 daddr = &ih->daddr; 48 49 tmp = *saddr; 50 *saddr = *daddr; 51 *daddr = tmp; 52 53 //((u8 *)saddr)[2] ^= 1; /* change the third octet (class C) */ 54 //((u8 *)daddr)[2] ^= 1; 55 type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr); 56 //printk("tx package type = %02x\n", *type); 57 // 修改类型, 原来0x8表示ping 58 *type = 0; /* 0表示reply */ 59 60 ih->check = 0; /* and rebuild the checksum (ip needs it) */ 61 ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl); 62 63 // 构造一个sk_buff 64 rx_skb = dev_alloc_skb(skb->len + 2); 65 skb_reserve(rx_skb, 2); /* align IP on 16B boundary */ 66 memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len); 67 68 /* Write metadata, and then pass to the receive level */ 69 rx_skb->dev = dev; 70 rx_skb->protocol = eth_type_trans(rx_skb, dev); 71 rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */ 72 dev->stats.rx_packets++; 73 dev->stats.rx_bytes += skb->len; 74 75 // 提交sk_buff 76 netif_rx(rx_skb); 77 } 78 79 static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev) 80 { 81 static int cnt = 0; 82 //printk("virt_net_send_packet cnt = %d\n", ++cnt); 83 84 /* 对于真实的网卡, 把skb里的数据通过网卡发送出去 */ 85 netif_stop_queue(dev); /* 停止该网卡的队列 */ 86 /* ...... */ /* 把skb的数据写入网卡 */ 87 88 /* 构造一个假的sk_buff,上报 */ 89 emulator_rx_packet(skb, dev); 90 91 dev_kfree_skb (skb); /* 释放skb */ 92 netif_wake_queue(dev); /* 数据全部发送出去后,唤醒网卡的队列 */ 93 94 /* 更新统计信息 */ 95 dev->stats.tx_packets++; 96 dev->stats.tx_bytes += skb->len; 97 98 return 0; 99 } 100 101 static const struct net_device_ops virt_netdev_ops = { 102 .ndo_start_xmit = virt_net_send_packet, 103 }; 104 105 static int virt_net_init(void) 106 { 107 /* 1. 分配一个net_device结构体 */ 108 vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);; /* alloc_etherdev */ 109 110 /* 2. 设置 */ 111 vnet_dev->netdev_ops = &virt_netdev_ops; 112 113 /* 设置MAC地址 */ 114 vnet_dev->dev_addr[0] = 0x08; 115 vnet_dev->dev_addr[1] = 0x89; 116 vnet_dev->dev_addr[2] = 0x89; 117 vnet_dev->dev_addr[3] = 0x89; 118 vnet_dev->dev_addr[4] = 0x89; 119 vnet_dev->dev_addr[5] = 0x11; 120 121 /* 设置下面两项才能ping通 */ 122 vnet_dev->flags |= IFF_NOARP; 123 vnet_dev->features |= NETIF_F_NO_CSUM; 124 125 /* 3. 注册 */ 126 //register_netdevice(vnet_dev); 127 register_netdev(vnet_dev); 128 129 return 0; 130 } 131 132 static void virt_net_exit(void) 133 { 134 unregister_netdev(vnet_dev); 135 free_netdev(vnet_dev); 136 } 137 138 module_init(virt_net_init); 139 module_exit(virt_net_exit); 140 MODULE_AUTHOR("Lover雪儿"); 141 MODULE_LICENSE("GPL"); 142 143 144 /* 145 146 / 147 网卡驱动程序框架: 148 app: socket 149 -------------------------------------------------------------------- 150 ----------------- 151 ----------------- 若干层网络协议--纯软件 152 ----------------- 153 hard_start_xmit() 用于发送数据包 ↑↑↑ 154 ↓↓↓ sk_buff netif_rx()用于上报数据包 155 调用硬件相关的驱动程序(要提供har_start_xmit,有数据时用netif_rx上报) 156 -------------------------------------------------------------------- 157 硬件 158 // 159 160 网卡驱动编写: 161 1.分配一个net_device结构体 162 2.设置 163 2.1 发包函数:hard_start_xmit 164 2.2 收到到数据时,(在中断处理函数中)用netif_rx上报数据 165 2.3 其他设置 166 3.注册 register_netdevice 167 168 169 一、测试1th 170 insmod virt_net.ko 171 ifconfig vnet0 3.3.3.3 172 ping 3.3.3.3 173 ping 3.3.3.4 看效果 174 175 176 */