内核代码中,ip_rcv是ip层收包的主入口函数,该函数由软中断调用。存放数据包的sk_buff结构包含有目的地ip和端口信息,此时ip层进行检查,如果目的地ip不是本机,且没有开启转发的话,则将包丢弃,如果配置了netfilter,则按照配置规则对包进行转发。

tcp_v4_rcv是tcp层收包的接收入口,其调用__inet_lookup_skb函数查到数据包需要往哪个socket传送,之后将数据包放入tcp层收包队列中,如果应用层有read之类的函数调用,队列中的包将被取出。

最近遇到一个问题,就是libpcap的收包,比tcpdump的收包,要慢。

然后修改测试代码如下:

  1. #include <pcap.h> /*libpcap*/
  2.  
  3. static pcap_t * pcap_http_in;
  4.  
  5. int initPcapIn_http()
  6. {
  7. int snaplen = ;//以太网数据包,最大长度为1518bytes
  8. int promisc = ;//混杂模式
  9. int timeout = ;
  10. char errbuf[PCAP_ERRBUF_SIZE];//内核缓冲区大小
  11. /*这个设备号需要根据测试服务器更改*/
  12. char * _pcap_in = "enp8s0f1";
  13. char * _bpf_filter = "tcp[13]=24";
  14.  
  15. /*打开输入设备或者文件*/
  16. if((pcap_http_in = pcap_open_live(_pcap_in, snaplen, promisc, timeout, errbuf)) == NULL)
  17. {
  18. printf("pcap_open_live(%s) error, %s\n", _pcap_in, errbuf);
  19. pcap_http_in = pcap_open_offline(_pcap_in, errbuf);
  20. if(pcap_http_in == NULL) {
  21. printf("pcap_open_offline(%s): %s\n", _pcap_in, errbuf);
  22. } else
  23. printf("Reading packets from pcap file %s...\n", _pcap_in);
  24. }
  25. else
  26. {
  27. printf("Capturing live traffic from device %s...\n", _pcap_in);
  28.  
  29. /*设置bpf过滤参数。*/
  30. if(_bpf_filter!= NULL)
  31. {
  32. struct bpf_program fcode;
  33.  
  34. if(pcap_compile(pcap_http_in, &fcode, _bpf_filter, , 0xFFFFFF00) < )
  35. {
  36. printf("pcap_compile error: '%s'\n", pcap_geterr(pcap_http_in));
  37. }
  38. else
  39. {
  40. if(pcap_setfilter(pcap_http_in, &fcode) < )
  41. {
  42. printf("pcap_setfilter error: '%s'\n", pcap_geterr(pcap_http_in));
  43. }
  44. else
  45. printf("Succesfully set BPF filter to '%s'\n", _bpf_filter);
  46. }
  47. }
  48.  
  49. /*设置一些参数*/
  50. if(pcap_setdirection(pcap_http_in, PCAP_D_IN)<) /*只抓入向包*/
  51. {
  52. printf("pcap_setdirection error: '%s'\n", pcap_geterr(pcap_http_in));
  53. }
  54. else
  55. printf("Succesfully set direction to '%s'\n", "PCAP_D_IN");
  56. }
  57.  
  58. return ;
  59. }
  60.  
  61. static inline unsigned long long rp_get_us(void)
  62. {
  63. struct timeval tv = {};
  64. gettimeofday(&tv, NULL);
  65. return (unsigned long long)(tv.tv_sec*1000000L + tv.tv_usec);
  66. }
  67.  
  68. int main(int argc, char *argv[])
  69. {
  70. initPcapIn_http();
  71.  
  72. unsigned char * pkt_data = NULL;
  73. struct pcap_pkthdr pcap_hdr;
  74. struct pcap_pkthdr * pkt_hdr = &pcap_hdr;
  75. while()
  76. {
  77. while( (pkt_data = (unsigned char * )pcap_next( pcap_http_in, &pcap_hdr))!=NULL)
  78. {
  79. if(pkt_hdr->caplen == )
  80. {
  81. unsigned long long time1 = rp_get_us();
  82. printf("---BEGIN: %ld us\n",time1);
  83. }
  84. }
  85. }
  86.  
  87. if (pcap_http_in)
  88. {
  89. pcap_close(pcap_http_in);
  90. }
  91.  
  92. return ;
  93. }

一开始静态编译,gcc静态编译报错,/usr/bin/ld: cannot find -lc

Makefile中肯定有-static选项。这其实是静态链接时没有找到libc.a。

其实需要安装glibc-static.xxx.rpm,如glibc-static-2.12-1.107.el6_4.2.i686.rpm,或是yum install glibc-static,我最终下载的是:glibc-static-2.17-157.el7.x86_64.rpm.

结果测试发现,我们打印的---BEGIN的时间,比tcpdump对应的时间晚1ms左右,也就是1000us左右。

然后我们根据tcpdump的调用方式,发现

  1. ldd /sbin/tcpdump |grep -i pcap
  2. libpcap.so. => /usr/local/lib/libpcap.so. (0x00007fd1903f1000)

ls -alrt /usr/local/lib/libpcap.so.1
lrwxrwxrwx. 1 root root 16 7月 25 19:36 /usr/local/lib/libpcap.so.1 -> libpcap.so.1.5.3

然后我将我的编译方式改成动态链接方式,即

  1. gcc -lpcap -g -o pcap.o pcap.c

发现效果很好,跟tcpdump差不多,也就是说,动态链接的lpcap的性能比静态链接的lpcap的性能要好。颠覆了我的认知,因为我一直认为静态链接快一点是有可能的。

我下载的源码是http://www.tcpdump.org/release/官网的,系统自带的版本和我的版本号一致。

发现不管是gcc -O2还是O3都是如此,我的静态链接的库就是慢,然后将tcpdump官网的libpcap库改成动态链接,还是慢。对比如下:

  1. 自己tcpdump官网下载的1..3libpcap如下
  2. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.103021>
  3. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.095322>
  4. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.101384>
  5. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.100031>
  6.  
  7. 对应的系统自带的1..3版本如下:
  8. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.000139>
  9. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.000061>
  10. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.000231>
  11. poll([{fd=, events=POLLIN}], , ) = ([{fd=, revents=POLLIN}]) <0.000062>

从参数看,是一模一样的,但是调用的消耗看,前者明显慢,直觉告诉我,应该看fd的属性,所以针对属性又单独跟踪了一次:

  1. 系统自带的1..3版本:
  2. [root@localhost libpcap-1.5.]# strace -e setsockopt ./pcaptest
  3. setsockopt(, SOL_PACKET, PACKET_ADD_MEMBERSHIP, "\3\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0", ) =
  4. setsockopt(, SOL_PACKET, PACKET_AUXDATA, [], ) =
  5. setsockopt(, SOL_PACKET, PACKET_VERSION, [], ) =
  6. setsockopt(, SOL_PACKET, PACKET_RESERVE, [], ) =
  7. setsockopt(, SOL_PACKET, PACKET_RX_RING, {block_size=, block_nr=, frame_size=, frame_nr=}, ) =
  8. Capturing live traffic from device enp5s0...
  9. setsockopt(, SOL_SOCKET, SO_ATTACH_FILTER, "\1\0\0\0\0\0\0\0\224\246c\0\0\0\0\0", ) =
  10. setsockopt(, SOL_SOCKET, SO_ATTACH_FILTER, "\v\0\0\0\0\0\0\0\260\276\357\1\0\0\0\0", ) =
  11. 我在tcpdump官网下载的libpcap版本:
  12. [root@localhost libpcap-1.5.]# strace -e setsockopt ./pcaptest
  13. setsockopt(, SOL_PACKET, PACKET_ADD_MEMBERSHIP, "\3\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0", ) =
  14. setsockopt(, SOL_PACKET, PACKET_AUXDATA, [], ) =
  15. setsockopt(, SOL_PACKET, PACKET_VERSION, [], ) =
  16. setsockopt(, SOL_PACKET, PACKET_RESERVE, [], ) =
  17. setsockopt(, SOL_PACKET, PACKET_RX_RING, "\0\0\2\0\20\0\0\0\0\0\2\0\20\0\0\0\350\3\0\0\0\0\0\0\0\0\0\0", ) =
  18. Capturing live traffic from device enp5s0...
  19. setsockopt(, SOL_SOCKET, SO_ATTACH_FILTER, "\1\0\0\0\0\0\0\0\224\246c\0\0\0\0\0", ) =
  20. setsockopt(, SOL_SOCKET, SO_ATTACH_FILTER, "\v\0\0\0\0\0\0\0P\6\343\1\0\0\0\0", ) =
  21. Succesfully set BPF filter to 'tcp[13]=24'
  22. Succesfully set direction to 'PCAP_D_IN'

果然参数不一样,设置的PACKET_VERSION,快的是2,慢的是3.

同样的版本号,难道代码有区别,走查代码流程,再加上gdb和strace,确定是PACKET_VERSION的问题。

  1. (gdb) p *(struct pcap_linux*)handle->priv
  2. $2 = {packets_read = 26, proc_dropped = 76591622, stat = {ps_recv = 0, ps_drop = 0, ps_ifdrop = 0}, device = 0x81c460 "br0",
  3. filter_in_userland = 0, blocks_to_filter_in_userland = 0, must_do_on_close = 0, timeout = 1000, sock_packet = 0, cooked = 0, ifindex = 8,
  4. lo_ifindex = 1, oldmode = 0, mondevice = 0x0, mmapbuf = 0x7ffff513d000 "\002", mmapbuflen = 2097152, vlan_offset = 12, tp_version = 2,
  5. tp_hdrlen = 36, oneshot_buffer = 0x81c940 "", current_packet = 0x0, packets_left = 0}

PACKET_VERSION就是一个宏决定的,后来发现,系统自带的版本,没有定义define HAVE_TPACKET3,所以不会走V3的版本。

那么,下一步就需要排查,怎么会慢,慢在哪里。

用户态没有问题,直接调用gettimeofday打印下时间。内核态要用stap跟踪了。

由于是抓包,那么在哪里下桩呢?

我们先来看pacap的版本,理清楚调用。

在 pcap_activate_linux 函数中,会先设置handle的默认的一些回调。

  1. handle->inject_op = pcap_inject_linux;
  2. handle->setfilter_op = pcap_setfilter_linux;
  3. handle->setdirection_op = pcap_setdirection_linux;
  4. handle->set_datalink_op = pcap_set_datalink_linux;
  5. handle->getnonblock_op = pcap_getnonblock_fd;
  6. handle->setnonblock_op = pcap_setnonblock_fd;
  7. handle->cleanup_op = pcap_cleanup_linux;
  8. handle->read_op = pcap_read_linux;
  9. handle->stats_op = pcap_stats_linux;

在此之后,会先尝试activate_new方法,如果失败的话,则会回退到activate_old方法,其实这种命名不太好,因为后续内核发展了,总不能叫activate_new_new.

在activate_new中,

  1. sock_fd = is_any_device ?
  2. socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))
  3. socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));:后面要用到,协议类型和socket类型
  1.  

这个申请socket单独拿出来说,是因为根据是否抓包带-any 参数,后面下钩子的地方不一样。

如果activate_new调用成功,则会继续判断activate_mmap ,这个函数其实就是测试内核是否支持mmap的方式来抓取报文,如果支持的话,就使用mmap的方式来获取报文,否则,就会回退到

之前的方法使用。针对mmap收包的情况,tp_version也有三个version,分别是v1,v2,v3,代码中会尝试先设置v3,如果不行则设置v2,以此类推。最终会调用 init_tpacket 来设置socket属性。先通过 getsockopt(handle->fd, SOL_PACKET, PACKET_HDRLEN, &val, &len) 来获取能力,然后使用

setsockopt(handle->fd, SOL_PACKET, PACKET_VERSION, &val,sizeof(val)) 来设置。PACKET_MMAP非常高效,它提供一个映射到用户空间的大小可配置的环形缓冲区。这种方式,读取报文只需要等待报文就可以了,大部分情况下不需要系统调用(其实poll也是一次系统调用)。通过内核空间和用户空间共享的缓冲区还可以起到减少数据拷贝的作用。
当然为了提高捕获的性能,不仅仅只是PACKET_MMAP。如果你在捕获一个高速网络中的数据,你应该检查NIC是否支持一些中断负载缓和机制或者是NAPI,确定开启这些措施。
PACKET_MMAP减少了系统调用,不用recvmsg就可以读取到捕获的报文,相比原始套接字+recvfrom的方式,减少了一次拷贝和一次系统调用,但是低版本的libpcap是不支持PACKET_MMAP,比如libpcap 0.8.1以及之前的版本都不支持,具体的资料,大家可以参考Document/networking/packet_mmap.txt,而本文最终的问题,刚好出在PACKET_MMAP上面,所谓成也萧何,败也萧何,下面继续分析代码:

activate_mmap -->|prepare_tpacket_socket-->init_tpacket

-->|create_ring(关于ring的一些设置),很关键地调用了if (setsockopt(handle->fd, SOL_PACKET, PACKET_RX_RING,(void *) &req, sizeof(req)))

-->|pcap_read_linux_mmap_v3(1,2),以及handle的其他一些回调设置。

继续libpcap的代码:

  1. switch (handlep->tp_version) {
  2. case TPACKET_V1:
  3. handle->read_op = pcap_read_linux_mmap_v1;
  4. break;
  5. #ifdef HAVE_TPACKET2
  6. case TPACKET_V2:
  7. handle->read_op = pcap_read_linux_mmap_v2;
  8. break;
  9. #endif
  10. #ifdef HAVE_TPACKET3
  11. case TPACKET_V3:
  12. handle->read_op = pcap_read_linux_mmap_v3;
  13. break;
  14. #endif
  15. }
  16. handle->cleanup_op = pcap_cleanup_linux_mmap;
  17. handle->setfilter_op = pcap_setfilter_linux_mmap;
  18. handle->setnonblock_op = pcap_setnonblock_mmap;
  19. handle->getnonblock_op = pcap_getnonblock_mmap;
  20. handle->oneshot_callback = pcap_oneshot_mmap;
  21. handle->selectable_fd = handle->fd;

如果activate_new失败,则会尝试activate_old,那么创建socket就会使用:

  1. handle->fd = socket(PF_INET, SOCK_PACKET, htons(ETH_P_ALL));

这个注意和上面activate_new使用的socket方法相区别。这种模式不支持-any参数。

因为我目前使用的内核是3.10.0+,所以不会走active_old流程。

在libpcap的库中,pcap_open_live-->|pcap_create--->pcap_create_interface,设置handle->activate_op = pcap_activate_linux;

|pcap_activate-->pcap_activate_linux

在tcpdump的代码中,直接就是main函数调用pcap_create和pcap_activate。然后在pcap_loop中循环收包,调用链分析结束。

下一步,需要了解,在内核态收包和在用户态收包的钩子下在哪里比较合适。因为使用的socket是SOCK_RAW,family是PF_PACKET,而且我们使用的是mmap收包,所以有必要看一下

PF_PACKET针对mmap的收包函数。所以就针对SOCK_RAW的收包去下钩子。

从创建socket的地方看起:

  1. static const struct net_proto_family packet_family_ops = {
  2. .family = PF_PACKET,
  3. .create = packet_create,
  4. .owner = THIS_MODULE,
  5. };
  6.  
  7. static int packet_create(struct net *net, struct socket *sock, int protocol,
  8. int kern)
  9. {
  10. 。。。
  11. spin_lock_init(&po->bind_lock);
  12. mutex_init(&po->pg_vec_lock);
  13. po->prot_hook.func = packet_rcv;
  14. 。。。
  15. }

那么看起来要在packet_rcv 下钩子,但是因为libpcap里面针对create_ring的函数调用if (setsockopt(handle->fd, SOL_PACKET, PACKET_RX_RING,(void *) &req, sizeof(req)))

使得packet_setsockopt函数中针对po->prot_hook.func 修改为了 tpacket_rcv ,所以我们应该在这个函数下钩子。tpacket_rcv是PACKET_MMAP的实现,packet_rcv是普通AF_PACKET的实现。

  1. static int
  2. packet_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen)
  3. {
  4. 。。。。。
  5. case PACKET_RX_RING:
  6. case PACKET_TX_RING:
  7. {
  8. union tpacket_req_u req_u;
  9. int len;
  10.  
  11. switch (po->tp_version) {
  12. case TPACKET_V1:
  13. case TPACKET_V2:
  14. len = sizeof(req_u.req);
  15. break;
  16. case TPACKET_V3:
  17. default:
  18. len = sizeof(req_u.req3);
  19. break;
  20. }
  21. if (optlen < len)
  22. return -EINVAL;
  23. if (pkt_sk(sk)->has_vnet_hdr)
  24. return -EINVAL;
  25. if (copy_from_user(&req_u.req, optval, len))
  26. return -EFAULT;
  27. return packet_set_ring(sk, &req_u, 0,
  28. optname == PACKET_TX_RING);
  29. 。。。。。。
  30. }
  31.  
  32. static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
  33. int closing, int tx_ring)
  34. {
  35. 。。。。
  36. po->prot_hook.func = (po->rx_ring.pg_vec) ?
  37. tpacket_rcv : packet_rcv;-------------决定了下桩的函数,我们是tpacket_rcv
  1. 。。。。。

这个偷懒一下,借用同事涂强的脚本,

  1. probe kernel.function("tpacket_rcv") {
  2. iphdr = __get_skb_iphdr($skb)
  3. saddr = format_ipaddr(__ip_skb_saddr(iphdr), %{ /* pure */ AF_INET %})
  4. daddr = format_ipaddr(__ip_skb_daddr(iphdr), %{ /* pure */ AF_INET %})
  5.  
  6. tcphdr = __get_skb_tcphdr($skb)
  7. dport = __tcp_skb_dport(tcphdr)
  8. sport = __tcp_skb_sport(tcphdr)
  9. psh = __tcp_skb_psh(tcphdr)
  10. ack = __tcp_skb_ack(tcphdr)
  11.  
  12. if(dport == && psh == && ack == ) {
  13. printf("%-25s %-10d, ts:%ld %s %s %d %d\n", execname(), pid(), gettimeofday_us(), saddr, daddr, dport, sport)
  14. }
  15. }

取样数据如下:

  1. swapper/ , ts: 10.74.44.16 10.75.9.158 //内核时间戳
  2. swapper/ , ts: 10.74.44.16 10.75.9.158
  3.  
  4. ---BEGIN: us //tcpdump官网下载的libpcap版本
  5.  
  6. ---BEGIN: us //centos官网下载的系统自带libpcap版本

可以看出,tcpdump官网下载的版本,也就是TPACKET_V3使能的,在内核收到包的时候,还是和TPACKET_V2的差10us,但是到用户态poll收包,则慢了1000us左右。说明redhat系列内核对

TPACKET_V3支持得肯定有问题。

rpm -qpi libpcap --changelog 查询变更记录,找到了相关信息:

* Tue Dec 02 2014 Michal Sekletar <msekleta@redhat.com> - 14:1.5.3-4
- disable TPACKET_V3 memory mapped packet capture on AF_PACKET socket, use TPACKET_V2 instead (#1085096)

在https://git.centos.org/blobdiff/rpms!libpcap.git/ed18a5631cc5de8fa95805d0cfd29a0678ea1458/SPECS!libpcap.spec中,找到了对应的patch。

Patch5:  0001-pcap-linux-don-t-use-TPACKETV3-for-memory-mmapped-ca.patch

uname -a
Linux centos7 3.10.0+

综上所述,如果是redhat系列,需要关闭TPACKETV3,不能直接使用tcpdump官网的libpcap包,suse的不存在这个问题。

linux libpcap的性能问题,请大家注意绕行。的更多相关文章

  1. 【转载】Linux系统与性能监控

    原文地址:http://kerrigan.sinaapp.com/post-7.html Linux System and Performance Monitoring http://www.hous ...

  2. Linux系统与性能监控

    原文地址:http://kerrigan.sinaapp.com/post-7.html Linux System and Performance Monitoring http://www.hous ...

  3. 1.linux服务器的性能分析与优化

    [教程主题]:1.linux服务器的性能分析与优化 [课程录制]: 创E [主要内容] [1]影响Linux服务器性能的因素 操作系统级 CPU 目前大部分CPU在同一时间只能运行一个线程,超线程的处 ...

  4. [转]提高 Linux 上 socket 性能,加速网络应用程序的 4 种方法

    原文链接:http://www.ibm.com/developerworks/cn/linux/l-hisock.html 使用 Sockets API,我们可以开发客户机和服务器应用程序,它们可以在 ...

  5. 高性能Linux服务器 第10章 基于Linux服务器的性能分析与优化

    高性能Linux服务器 第10章    基于Linux服务器的性能分析与优化 作为一名Linux系统管理员,最主要的工作是优化系统配置,使应用在系统上以最优的状态运行.但硬件问题.软件问题.网络环境等 ...

  6. linux上NFS性能参数

    linux nfs客户端对于同时发起的NFS请求数量进行了控制,若该参数配置较小会导致IO性能较差,查看该参数: cat /proc/sys/sunrpc/tcp_slot_table_entries ...

  7. linux服务器的性能分析与优化(十三)

    [教程主题]:1.linux服务器的性能分析与优化 [主要内容] [1]影响Linux服务器性能的因素 操作系统级 Ø CPU 目前大部分CPU在同一时间只能运行一个线程,超线程的处理器可以在同一时间 ...

  8. 通过/proc/sys/net/ipv4/优化Linux下网络性能

    通过/proc/sys/net/ipv4/优化Linux下网络性能 /proc/sys/net/ipv4/优化1)      /proc/sys/net/ipv4/ip_forward该文件表示是否打 ...

  9. GNU Linux高并发性能优化方案

    /*********************************************************** * Author : Samson * Date : 07/14/2015 * ...

随机推荐

  1. python 模块:xlrd && xlwt

    主要来自:http://www.jb51.net/article/60510.htm python读excel--xlrd 这个过程有几个比较麻烦的问题,比如读取日期.读合并单元格内容.下面先看看基本 ...

  2. golang 队列

    You have to perform NN operations on the queue. The operations are of following type: E xE x : Enque ...

  3. Emmet for Dreamweaver 整理分享

    我是一名技术不是很到位的前端,每次做项目总要写大量的HTML和CSS,耳边经常听到的是快.快点.再快点!我真想说快你妹!但是,我不得不承认的是:我只有两只手... 后来,在群里看到有人分享了一个连接大 ...

  4. flex弹性布局语法介绍及使用

    一.语法介绍 Flex布局(弹性布局) ,一种新的布局解决方案 可简单.快速的实现网页布局 目前市面浏览器已全部支持1.指定容器为flex布局 display: flex; Webkit内核的浏览器, ...

  5. MicroPython最全资料集锦丨TPYBoard全系列教程之文档+例程源码

    MicroPython成功将Python引入到嵌入式领域,近几年MicroPython的发展和普及也证明,Python无疑将在未来几年内快速抢占和蚕食C/C++的份额.包括现在比较火爆的机器人.无人机 ...

  6. input 光标在 chrome下不兼容 解决方案

    input 光标在 chrome下不兼容 解决方案 height: 52px; line-height: normal; line-height:52px\9 .list li input[type= ...

  7. Java学习笔记16(面向对象九:补充内容)

    总是看到四种权限,这里做一个介绍: 最大权限是public,后面依次是protected,default,private private修饰的只在本类可以使用 public是最大权限,可以跨包使用,不 ...

  8. java equals == contentEquals

    equals与== 经常用于比较,用法如下:字符串比较相同用equals,普通数值(基本数据类型)比较用==, contentEquals下面讲 理论准备: java的基本类型如int.float,d ...

  9. uboot各种目录下的文件作用

    uboot下载地址:http://ftp.denx.de/pub/u-boot/ 1.目录分布 2.目录结构变化: u-boot-2010.03及以前版本├── api                ...

  10. RNN的简单的推导演算公式(BPTT)

    附上y=2x-b拟合的简单的代码. import numpy as np x = np.asarray([2,1,3,5,6]); y = np.zeros((1,5)); learning_rate ...