精华内容
下载资源
问答
  • OB组织块介绍

    2021-09-30 11:17:28
    1、自由循环组织块OB1 S7 CPU启动完成后,操作系统循环执行OB1,OB1执行完成后,操作系统再次启动OB1。在OB1中可以调用FB、SFB、FC、SFC等用户程序使其循环执行。除OB90以外,OB1优先级最低,可以被其他OB中断。OB1...

    1、自由循环组织块OB1
    S7 CPU启动完成后,操作系统循环执行OB1,OB1执行完成后,操作系统再次启动OB1。在OB1中可以调用FB、SFB、FC、SFC等用户程序使其循环执行。除OB90以外,OB1优先级最低,可以被其他OB中断。OB1默认扫描监控时间为150ms(可设置),扫描超时,CPU自动调用)B80报错,如果程序中没有建立OB80,CPU进入停止模式。

    2、日期中断组织块OB10~OB17

    在CPU属性中,可以设置日期中断组织块OB10~OB17触发的日期、执行模式(到达设定的触发日期后,OB只执行一次或按每分、每小时、每周、每月周期执行)等参数,当CPU的日期值大于设定的日期值时,触发相应的OB并按设定的模式执行。在用户程序中也可以通过调用SFC28系统函数设定CPU日期中断的参数,调用SFC30激活日期中断投入运行,与在CPU属性中的设置相比,通过用户程序,可以在CPU运行时灵活地修改设定的参数,两种方式可以任意选择,也可以同时对一个OB进行设置。

    3、时间延迟中断组织块OB20~OB23
    时间延迟中断组织块OB20OB23的优先级及更新过程映像区的参数需要在CPU属性中设置,通过调用系统函数SFC32触发执行,OB号及延迟时间在SFC32参数中设定,延迟时间为160000ms,大大优于定时器精度。

    4、循环中断组织块OB30~OB38

    循环中断组织块OB30OB38按设定的时间间隔循环执行,循环中断的间隔时间在CPU属性中设定,每一个OB默认的时间间隔不同,例如)B35默认的时间间隔为100ms,在OB35中的用程序将每隔100ms调用一次,时间间隔可以自由设定,最小时间间隔不能小于55ms。OB中的用户程序执行时间必须小于设定的时间间隔,如果间隔时间较短,由于循环中断OB没有完成程序扫描而被再次调用,从而造成CPU故障,触发OB80报错,如果程序中没有创建OB80,CPU进入停止模式。通过调用SFC39SFC42系统函数可以禁止、延迟、使能循环中断的调用。循环中断组织块通常处理需要固定扫描周期的用户程序,例如PID函数块通常需在循环中断中调用以处理积分时间的计算。

    5、硬件中断组织块OB40~OB47

    硬件中断也叫过程中断,由外部设备产生,例如功能模块FM、通信处理器CP及数字量输入、输出模块等。通常使用具有硬件中断的数字量输入模块触发中断响应,然后为每一个模块配置相应的中断OB(一个模块只能良一个中断OB,S7-300系列PLC CPU只能触发硬件中断OB40),在模块配置中可以选择输入点的上升沿、下降沿或全部作为触发中断OB的事件。配置中的中断事件出现,中断主程序,执行中断OB中的用户程序一个周期,然后跳回中断处继续执行主程序。使用中断与普通输入信号相比,没有主程序扫描和过程映像区更新时间,适合需要快速响应的应用。
    如果输入模块中的一个通道触发硬件中断,操作系统将识别模块的槽号及触发相应的OB,中断OB执行之后发送与通道相关的确认。在识别和确认过程中,该通道再次触发的中断事件将丢失;如果模块其他通道触发中断事件,中断不会丢失,在当前正在运行的中断确认之后触发;如果是不同的模块触发的中断事件,中断请求被记录,中断OB在空闲(没有模块其他通道的中断请求)时触发。通过调用SFC39~SFC42系统函数可以禁止、延迟、使能硬件中断的调用。

    6、DPV1中断组织块OB55~OB57

    CPU响应PROFIBUS-DP V1从站触发的中断信息。

    7、多处理器中断组织块OB60

    用于S7-400系列PLC多CPU(一个机架中最多插入4个CPU完成同一个复杂任务)处理功能,通过调用SFC35,可以触发OB60在多个CPU中同时执行。

    8、时钟同步中断组织块OB61~OB64

    用于处理PROFIBUS-DP V1等时钟同步,从采集各个从站的输入到逻辑结果输出,需要经过从站输入信号采样循环(信号转换)、从站背板总线循环(转换的信号从模块传递到从站接口)、PROFIBUS-DP总线循环(信号自从站传递到主站)、程序执行循环(信号的程序处理)、PROFIBUS-DP总线循环(信号从主站传递到从站)、从站背板总线循环(信号从从站接口传递到输出柜块)及模块输出循环(信号转换)7个循环,时钟同步中断将7个循环同步,优化数据的传递并保证PROFIBUS-DP各个从站数据处理的同步性。PROFIBUS时钟同步中断只能用于S7-400系列PLC CPU(具有DP V2 功能)。

    9、工艺同步处理中断组织块OB65

    用于T-CPU(具有运动控制功能的CPU)工艺块与开始程序的同步处理。

    10、冗余故障中断组织块OB70、OB72

    用于S7-400H冗余系统,当I/O冗余故障,例如冗余的PROFIBUS-DP从站故障时,触发OB70的调用,当CPU冗余故障,如CPU切换、同步故障时,触发OB72的调用。如果I/O冗余,或者CPU冗余故障而在CPU中没有创建OB70、OB72,CPU不会进入停止模式。

    11、异步故障中断组织块OB80~OB87
    异步故障中断用于处理各种故障事件。

    OB80:处理时间故障、CIR(Configuration In Run)后的重新运行等功能,例如OB1或OB35运行超时,CPU自动调用OB80报错,如果程序中没有创建OB80,CPU进入停止模式。

    OB81:处理与电源相关的各种信息(S7-400系列PLC CPU只有电池故障时调用),出现故障,CPU自动调用OB81报错,如果程序中没有创建OB81,CPU不会进入停止模式。

    OB82:诊断中断,如果使能一个具有诊断中断模块的诊断功能(例如断线、传感器电源丢失),出现故障时调用OB82,如果程序中没有创建OB82,CPU进入停止模式。诊断中断还对CPU所有内外部故障,包括模块前连接器拔出、硬件中断丢失等作出响应。

    OB83:用于模块插拔事件的中断处理,事件出现,CPU自动调用OB83报警,如果程序中没有创建OB83,CPU进入停止模式。

      OB84:用于处理存储器、冗余系统中两个CPU的冗余连接性能降低等事件。
    

    OB85:用于处理操作系统访问模块故障、更新过程映像区时I/O访问故障、事件触发但相应的OB没有下载到CPU等事件,事件出现,CPU自动调用OB85报错,如果程序中没创建OB85,CPU进入停止模式。

    OB86:用于处理扩展机架(不适用于S7-300系列)、PROFIBUS-DP主站、PROFIBUS-DP或PROFINET I/O分布I/O系统中站点故障等事件,事件出现,CPU自动调用OB86报错,如果程序中没有创建,CPU进入停止模式。

    OB87:用于处理MPI GD 通信及时钟同步故障,事件出现,CPU自动调用OB87报错,如果程序中没有创建,CPU不会进入停止模式。

    12、处理中断组织块OB88

    用于处理程序嵌套、区域数据分配故障,故障出现,CPU自动调用OB88报错,如果程序中没有创建,CPU进入停止模式。

    13、背景循环中断组织块OB90

    优先级最低,保证CPU最短的扫描时间,避免过程映像区更新过于频繁。程序的下载和CPU中程序的删除触发OB90的调用。只能用于S7-400系列PLC CPU。

    14、启动中断组织块OB100~OB102

    用于处理CPU启动事件,暖启动CPU调用OB100,热启动CPU调用OB101(不适合S7-300系列PLC和S7-400H),冷启动CPU调用OB102,温度越低,CPU启动时清除存储器中数据区的类型越多。

    15、同步错误中断组织块OB121、OB122

    OB121处理与编程故障有关的事件,例如调用的函数没有下载到CPU中、BCD码出错等,OB122处理与I/O地址访问故障有关的事件,例如访问一个I/O模块时,出现读故障等。如果上述故障出现,在程序中没有创建OB121、OB122,CP进入停止模式。

    注意:不是所有的OB都可以在S7 CPU中使用,例如S7-300系列PLC PU中只有暖启动OB100,操作系统不能调用OB101、OB102,CPU中可以使用的OB请参考CPU选型手册。

    S7-300系列PLC中组织块的优先级是固定的,不能修改,在S7-400系列PLC中下列组织块的优先级可以进行修改:

    OB10OB47:优先级修改范围223。

    OB70OB72:优先级修改范围238。

    OB81OB87:优先级修改范围226,优先级24~26确保异步故障中断不被其他的事件中断。

    几个组织块可以具有相同的优先级,当事件同时出现时,组织块按事件出现的先后顺序触发,如果超过12个相同优先级的OB同进触发,中断可能丢失。

    能够使S7-300/400 plc停机的原因有哪些
    能够使plc停机的原因有哪些
    使CPU进入stop的情况很多,比如地址调用错误,没有下载需要DB块,编程错误等等,如果你想避免错误时不使CPU进入停止状态,你可以在程序中加入特殊的OB块,则出现相应问题,调用相应的OB块,虽然里面没程序,PLC将对错误错误不作任何处理,继续运行。否则PLC将进入停机状态可,比如:

    OB73通讯冗余出错OB
    当容错S7连接中发生首次冗余丢失时,H CPU的操作系统将调用OB73(只有在S7通讯中才会有容错S7连接。更多信息,请参见“S7-400 H可编程控制器,容错系统。”)。如果其它容错S7连接发生了冗余丢失,则不会再有OB73启动。直到为具有容错功能的所有S7连接恢复冗余后,才会出现另一个OB73启动。如果发生了启动事件且OB73没有编程,CPU不会转为STOP模式。

    OB80时间出错组织块
    无论何时执行OB时出错,S7-300 CPU的操作系统将调用OB80。此类错误包括:
    超出周期时间、执行OB时出现确认错误、提前了时间而使OB的启动时间被跳过、在CiR后恢复RUN模式。例如,如果在上一次调用之后发生了某一周期性中断OB的启动事件,而同一OB此时仍在执行中,则操作系统将调用OB80。如果OB80尚未编程,则CPU将转为STOP模式。可以使用SFC39至42禁用或延迟和重新启用时间出错OB。

    OB81电源出错组织块
    只要发生由错误或故障所触发的事件,而此错误或故障又与电源(仅在S7-400上)或备用电池(当事件进入和离开时)有关,则S7-300 CPU的操作系统调用OB81。在S7-400中,如果已使用BATT.INDIC开关激活了电池测试功能,则只有在出现电池故障时才会调用OB81。如果OB81没有编程,则CPU不会转为STOP模式。可以使用SFC39至42禁用或延迟,并重新启用电源出错OB。

    OB82诊断中断组织块
    如果具有诊断功能的模块(已为其启用了诊断中断)检测到错误,则它会输出一个诊断中断的请求给CPU(当事件进入和离开时)。则操作系统调用OB82。OB82的局部变量包含逻辑基址和四字节的故障模块的诊断数据(请参见下表)。如果OB82尚未编程,则CPU转为STOP模式。可以使用SFC39至42禁用或延迟,并重新启用诊断中断OB。

    OB83插入/删除模块中断组织块
    在下列情况下,CPU操作系统会调用OB83:
    • 插入/删除已组态模块后
    • 在STEP 7下修改模块参数以及在运行期间将更改下载至CPU后
    可借助SFC39至42禁用/延迟/启用插入/删除中断OB。

    OB84 CPU硬件故障组织块
    在下列情况下,CPU中的OS将调用OB84:
    • 已检测到并更正了内存出错之后
    • 对于S7-400H:如果两个CPU之间的冗余链接的性能下降
    可以使用SFC39至42禁用或延迟CPU硬件出错OB,然后再次启用它。
    OB85优先级出错组织块
    只要发生下列事件之一,CPU的操作系统即调用OB85:
    • 尚未装载的OB(OB81除外)的启动事件。
    • 操作系统访问模块时出错。
    • 在系统更新过程映像期间出现I/O访问错误(如果由于组态原因,未禁止OB85的调用)。

    OB86机架故障组织块
    只要在分布式I/O (PROFIBUS DP或PROFInet IO)中检测到中央扩展机架(不带S7-300)、DP主站系统或站故障(进入事件与离开事件时),CPU的操作系统调用OB86。如果OB86尚未编程,当检测到此种类型的出错时,CPU将转为STOP模式。可使用SFC39至42禁用或延迟,并重新启用OB86。

    OB87通讯出错组织块
    只要发生由通讯出错导致的事件,CPU的操作系统就会调用OB87。如果OB87尚未编程,CPU不会转为STOP模式。可以使用SFC39至42禁用或延迟,并重新启用通讯出错OB。

    OB88处理中断OB
    程序块执行被中止后,CPU操作系统将调用OB88。导致此中断的原因可能是:
    • 同步出错的嵌套深度过大
    • 块调用(U堆栈)的嵌套深度过大
    • 分配本地数据时出错
    如果未对OB88编程且程序块执行被中止,则CPU进入STOP模式(事件ID W#16#4570)。如果在优先级28下中止了程序块执行,则CPU进入STOP模式。可借助于SFC39至42禁用、延迟和启用处理中断OB。

    OB121编程出错组织块
    只要发生同程序处理相关的错误所导致的事件,CPU的操作系统即调用OB121。例如,如果用户程序调用了尚未装载到CPU中的块,将会调用OB121。

    OB122I/O访问出错组织块
    只要在访问模块上的数据时出错,CPU的操作系统即调用OB122。例如,如果在访问I/O模块上的数据时,CPU检测到读取错误,操作系统将调用OB122。

    展开全文
  • 在python中,语句块是在条件为真(条件语句)时执行或者执行多次(循环语句)的一组语句。在代码前放置空格来缩进语句即可创建语句块,语句块中的每行必须是同样的缩进量。Python的基本语法——语句块语句块是在条件为真...

    在python中,语句块是在条件为真(条件语句)时执行或者执行多次(循环语句)的一组语句。在代码前放置空格来缩进语句即可创建语句块,语句块中的每行必须是同样的缩进量。

    b563a00e3e031ee49268aef0c46bf686.png

    Python的基本语法——语句块

    语句块是在条件为真(条件语句)时执行或者执行多次(循环语句)的一组语句。

    Python语言通过缩进来组织代码块,这是Python的强制要求。

    在代码前放置空格来缩进语句即可创建语句块,语句块中的每行必须是同样的缩进量。

    Python语言利用缩进表示语句块的开始和退出(Off-side规则),增加缩进表示语句块的开始,而减少缩进则表示语句块的退出。

    在Python编程中具有相同缩进的代码被自动视为一个代码块,无论进行了几个空格的缩进都是被允许的,只要缩进空格的数量统一。

    较为规范的情况下一般使用采用4个空格表示一个缩进。也可以使用一个制表符表示一个缩进(Tab键)。一般的IDE都拥有着把一个制表符视为4个空格的转换(记事本不具有此功能)。但一定要避免制表符和空格键的混用,以避免造成不必要的错误。

    在代码结束时,多敲一次换行。使得代码层次结构更加清晰

    推荐学习:Python视频教程

    本文由 @不成功的猿人[Vip] 发布于 职涯宝 ,未经作者许可,禁止转载,欢迎您分享文章

    展开全文
  • 看代码吧~test=# DO $$DECLARE i record;test$# BEGINtest$# FOR i IN 1..10test$# LOOPtest$# execute "select loop_insert(1)";...DOtest=#看匿名执行效果:test=# select count(*) from lineitem...

    看代码吧~

    test=# DO $$DECLARE i record;

    test$# BEGIN

    test$# FOR i IN 1..10

    test$# LOOP

    test$# execute "select loop_insert(1)";

    test$# END LOOP;

    test$# END$$;

    DO

    test=#

    看匿名块的执行效果:

    test=# select count(*) from lineitem;

    count

    -------

    7000

    (1 row)

    test=# select count(*) from lineitem;

    count

    -------

    17000 ------------->>>>匿名块插入了10000条记录.

    (1 row)

    test=#

    补充:PostgreSql 的PL/pgSQL 块结构 (在pgAdmin查询工具中如何执行语句块)

    PostgreSql 的PL/pgSQL 块结构

    本文我们学习PL/pgSQL结构块,包括如何写结构块和执行结构块。

    什么是结构块

    PL/pgSQL是结构块语言,因此,PL/pgSQL函数或过程是通过结构块进行组织。完整结构块的语法如下:

    [ <> ]

    [ DECLARE

    declarations ]

    BEGIN

    statements;

    ...

    END [ label ];

    详细说明如下:

    块有两部分组成:声明部分和主体部分。声明部分是可选的,而主体部分是必须的。块在end关键字后面使用分号(;)表示结束。

    块可以有个可选的标签在开始和结尾处。如果你想在块主体中使用exit语句或限定块中声明的变量名称时,需要使用块标签。

    主体部分是编写代码的地方,每条语句需要使用分号结束。

    PL/pgSQL 块结构示例

    下面示例描述一个简单块结构,一般称为匿名块:

    DO $$

    <>

    DECLARE

    counter integer := 0;

    BEGIN

    counter := counter + 1;

    RAISE NOTICE "The current value of counter is %", counter;

    END first_block $$;

    运行结果:

    NOTICE: The current value of counter is 1

    从pgAdmin中执行块,点击图示按钮:

    20ff8820d966d57e7b14c7935a318ac2.png

    注意DO语句不属于块结构。它用于执行匿名块。PostgreSQL 在9.0版本中引入DO语句。

    在声明部分定义变量counter并设置为0.

    在主体部分,是counter值加1,通过RAISE NOTICE语句输出其值。

    first_block 标签仅为了演示需要,本例中没有啥意义。

    ** 什么是双 ($$) 符号?**

    ($$) 符号 是单引号(")的替代符号。开发PL/pgSQL 时,无论是函数或过程,必须把主体部分放在一个字符串中。因此必须对主体部分的单引号进行转义表示:

    DO

    "<>

    DECLARE

    counter integer := 0;

    BEGIN

    counter := counter + 1;

    RAISE NOTICE ""The current value of counter is %"", counter;

    END first_block";

    使用($$) 符号可以避免引号问题。也可以在$之间使用标识,如之间使用标识,如之间使用标识,如function$ , procedureprocedureprocedure.

    PL/pgSQL 子结构块

    PL/pgSQL可以一个块在另一个块的主体中。一个块嵌入在另一个块中称为子块,包含子块的块称为外部块。

    d42e6bebb1b29b008918fbee6bfdf60e.png

    子块用于组织语句,这样大块能被分为更小和更多逻辑子块。子块的变量的名称可以与外部块变量名称同名,虽然这在实践中不建议。当在子块中声明一个与外部变量同名的变量,外部变量在子块中被隐藏。如果需要访问外部块的变量,可以使用块标签作为变量的限定符,如下面示例:

    DO $$

    <>

    DECLARE

    counter integer := 0;

    BEGIN

    counter := counter + 1;

    RAISE NOTICE "The current value of counter is %", counter;

    DECLARE

    counter integer := 0;

    BEGIN

    counter := counter + 10;

    RAISE NOTICE "The current value of counter in the subblock is %", counter;

    RAISE NOTICE "The current value of counter in the outer block is %", outer_block.counter;

    END;

    RAISE NOTICE "The current value of counter in the outer block is %", counter;

    执行结果如下:

    NOTICE: The current value of counter is 1

    NOTICE: The current value of counter in the subblock is 10

    NOTICE: The current value of counter in the outer block is 1

    NOTICE: The current value of counter in the outer block is 1

    首先,在外部块中声明变量counter。

    接着在子块中也声明了一个同名变量。

    在进入子块之前,变量的值为1。在子块中,我们给变量counter值加10,然后打印出来。注意,这个改变仅影响子块中counter变量。

    然后,我们通过标签限定符引用外部变量:outer_block.counter

    最后,我们打印外部块变量,其值保持不变。

    总结

    本文我们学习PL/pgSQL块结构,通过DO语句可以执行匿名块。

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持云海天教程。如有错误或未考虑完全的地方,望不吝赐教。

    展开全文
  • defer性能 go 1.13 正式版本的发布提升了 defer 的性能,号称针对 defer 场景提升了 30% 的性能。...2. 执行回调函数链过程:deferreturn go 1.13 带来的 deferprocStack 函数,这个函数就是这个 30% 性

    defer作用与使用场景

    在golang编程语言中,经常看到defer关键字搭配着函数一起出现,那么defer有哪些使用场景及这种使用方式背后又有什么原由呢?

    defer特性

    1. 延迟调用
    2. LIFO后进先出:后调用先返回
    3. 作用域
    4. 异常场景
    

    注意:defer使用时,需要和函数绑定。 两个理解:defer 只会和 defer 语句所在的特定函数绑定在一起,作用域也只在这个函数。从语法上来讲,defer 语句也一定要在函数内,否则会报告语法错误。

    package main
    
    func main() {
     func() {
      defer println("--- defer ---")
     }()
     println("--- ending ---")
    }
    

    如上,defer 处于一个匿名函数中,就 main 函数本身来讲,匿名函数 fun(){}() 先调用且返回,然后再调用 println("— ending —") ,所以程序输出自然是:

    --- defer ---
    --- ending ---
    

    defer作用

    defer 其实并不是 Golang 独创,是多种高级语言的共同选择;
    defer 最重要的一个特点就是无视异常,依然可使程序执行,这个是 Golang 在提供了 panic-recover 机制之后做的补偿;
    defer 的作用域存在于函数,defer 也只有和函数结合才有意义;
    defer 允许把配套的两个行为代码放在最近相邻的两行,比如创建&释放、加锁&放锁、前置&后置,以免引发加锁之后忘记之类的异常,使得代码更易读,编程体验良好。
    

    defer使用场景

    1. 并发同步
    2. 锁场景
    3. 资源释放
    4. panic-recover
    
    1. 并发同步
    var wg sync.WaitGroup
    
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // 程序逻辑
        }()
    }
    wg.Wait()
    
    1. 锁场景
    var mu sync.Mutex
     mu.Lock()
     defer mu.Unlock()
    

    但是请注意,lock 以下的代码都会在锁内。所以下面的代码要足够精简和快速才行,如果说下面的逻辑很复杂,那么可能就需要手动控制 unlock 防止的位置了。

    1. 资源释放
    file, err := os.Open("file.go") // For read access.
    if err != nil {
    	log.Fatal(err)
    }
    defer file.Close()
    
    1. panic-recover场景
     defer func() {
      if v := recover(); v != nil {
       _ = fmt.Errorf("PANIC=%v", v)
      }
     }()
    
    

    defer性能

    go 1.13 正式版本的发布提升了 defer 的性能,号称针对 defer 场景提升了 30% 的性能。

    This release improves performance of most uses of defer by 30%.
    

    go 1.13 之前只有 defer 语句只会被编译器翻译成两个过程:

    1. 回调注册函数过程:deferproc
    2. 执行回调函数链过程:deferreturn
    

    go 1.13 带来的 deferprocStack 函数,这个函数就是这个 30% 性能提升的核心手段。deferprocStack 和 deferproc 的目的都是注册回调函数,这个还是不变,但是不同的是 deferprocStatck 是在栈内存上分配 struct _defer 结构,而 deferproc 这个是需要去堆上分配结构内存的。而我们绝大部分的场景都是可以是在栈上分配的,所以自然整体性能就提升了。栈上分配内存自然是比对上要快太多了,只需要 rsp 寄存器操作下就分配出来了。

    _defer 数据结构

    提示:该数据结构为 go 1.13 版本,go 1.14 版本的比这个稍微复杂,加了一些开放编码优化需要的字段。

    type _defer struct {
    	 siz     int32 // 参数和返回值的内存大小
    	 started bool
    	 heap    bool    // 区分该结构是在栈上分配的,还是对上分配的
    	 sp        uintptr  // sp 计数器值,栈指针;
    	 pc        uintptr  // pc 计数器值,程序计数器;
    	 fn        *funcval // defer 传入的函数地址,也就是延后执行的函数;
    	 _panic    *_panic  // panic that is running defer
    	 link      *_defer   // 链表
    }
    

    ***每一次的 defer 调用都会对应到一个 _defer 结构体,一个函数内可以有多个 defer 调用,所以自然需要一个数据结构来组织这些 _defer 结构体。_defer 按照对齐规则占用 48 字节的内存。_defer 结构体中的 link 字段把所有的 _defer 串成一个链表,表头是挂在 Goroutine 的 _defer 字段。***效果如下:
    _defer链表

    还有一个重点,_defer 结构只是一个 header ,结构紧跟的是延迟函数的参数和返回值的空间,大小由 _defer.siz 指定。这块内存的值在 defer 关键字执行的时候被填充好,而延迟函数的参数是预计算的。
    _defer函数参数与返回值

    当 defer 外层出现显式(for)或者隐式(goto)的时候,将会导致 struct _defer 结构体分配在堆上,那么性能就比较差了,这个编程的时候要注意。

    在讲解defer内存分配及调用过程之前,需要先了解以下地址空间、栈帧划定、函数调用等基本概念。

    函数调用基本知识回顾

    如果要想深入地理解 defer ,避免踩坑,那么可以从理解下面的概念开始,这对理解 defer 在函数里的行为必不可少。

    地址空间

    下图是一个典型的操作系统的地址空间示意图:
    栈地址空间

    最重要的几点:

    1. 内核栈在高地址,用户栈在低地址。如果是 32 位操作系统,那么最经典的就是,用户栈区域为 [0, 3G],内核栈区域为 [3G, 4G];
    2. 栈空间分配是从高地址往下分配的(所以我们经常看到栈分配空间,是通过减 rsp 的值来实现就是这个道理)
    3. 堆空间分配是从低地址往上分配的。

    函数栈帧

    函数调用执行的时候,需要分配空间以存储数据,比如函数的参数,函数内局部变量,寄存器的值(用于上下文切换)。这些数据都需要保存在一个地方,这个地方就是在栈空间上。因为这些数据的声明周期是和函数一体的,函数执行的时候存在,函数执行完立马就可以销毁。因此,函数传递的参数、函数内的局部变量都是在栈上分配。
    和堆空间不同,堆上用来分配 声明周期由程序员控制的对象。
    栈的使用销毁由编译器控制,堆空间的使用则是由程序或者说是程序员(在有垃圾回收的语言里,堆空间的使用由语言层面支持)。

    当函数调用的时候,对应产生一个栈帧(stack frame),函数结束的时候,释放栈帧。栈帧主要用来保存

    1. 函数参数
    2. 局部变量
    3. 返回值
    4. 寄存器的值(上下文切换)

    函数在执行过程中使用一块栈内存来保存上述这些值。当发生函数调用时,因为 caller 还没执行完,caller 的栈帧中保存的数据还有用,所以 callee 函数执行的时候不能覆盖 caller 的栈帧,这种情况需要分配一个 callee 的栈帧。

    栈空间的使用方式由编译器管理,在编译期间就确定。栈的大小就会随函数调用层级的增加而向低地址增加,随函数的返回而缩小,调用层级越深,消耗的栈空间就越大。所以,在递归函数的场景,经常见到有些递归太深的函数会报错,被操作系统直接拒绝,就是因为考虑到这个栈空间使用的合理性,OS对栈的深度有限制。

    栈帧的划定

    在底层,有两个寄存器的值来划定一个函数栈帧:

    rsp :栈寄存器,指向当前栈顶位置;
    rbp :栈帧寄存器,指向函数栈帧的起始位置;
    

    所以,我们可以认为在一个函数执行的时候,rsp, rbp 这两个寄存器指向的区域就是当前函数的一个栈帧。在 golang 的一个函数的代码里,开头会先保存 rbp 寄存器的值,保存到栈上,函数执行完之后,需要返回 caller 函数之前,需要恢复 rbp 寄存器。

    defer内存分配

    注意:defer 放在循环嵌套的上下文中,可能导致性能问题。

    编译器就能决定_defer 结构体在栈上还是在堆上分配,对应函数分别是 deferprocStatck 和 deferproc 函数。deferprocStack和deferproc两个函数的目的一致:分配出 _defer 的内存结构,把回调函数初始化进去,挂到链表中。

    栈上分配 deferprocStack

    deferprocStack 函数做了哪些事情呢?

    // 进到这个函数之前,就已经在栈上分配好了内存结构(编译器分配的,rsp 往下伸展即可)
    func deferprocStack(d *_defer) {
    	gp := getg()
    	
    	// siz 和 fn 在进到该函数之前已经赋值
    	d.started = false
    	// 表明是栈的内存,true表明是在堆上分配的
    	d.heap = false
    	// 获取到 caller 函数的 rsp 寄存器值,并赋值到 _defer 结构 sp 字段中
    	d.sp = getcallersp()
    	// 获取到 caller 函数的 pc (rip) 寄存器值,并赋值到 _defer 结构 pc 字段中
    	// 回忆函数调用的原理,就知道 caller 压栈时的 pc 值就是 deferprocStack 的下一行指令;
    	d.pc = getcallerpc()
    	
    	// 把这个 _defer 结构作为一个节点,挂到 goroutine 的链表中;
    	*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
    	*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
    	*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
    	// 注意,特殊的返回,不会触发延迟调用的函数
    	return0()
    }
    

    deferprocStack作用:

    1. 由于是栈上分配内存的,所以其实在调用到 deferprocStack 之前,编译器就已经把  _defer 结构的函数准备好了;
    2. _defer.heap 字段用来标识(deferprocStack函数中的_defer)这个结构体是分配在栈上;
    3. 保存上下文,把 caller 函数的 rsp,pc(rip) 寄存器的值保存到 _defer 结构体;
    4. _defer 作为一个节点挂接到链表。注意:表头是 goroutine 结构的 _defer 字段,而在一个协程任务中大部分是有多次函数调用的,所以这个链表会挂接一个调用栈上的 _defer 结构,执行的时候按照 rsp 来过滤区分;
    

    _defer链表挂在goroutine上执行

    堆上分配 deferproc

    堆上分配的函数为 deferproc ,简化逻辑如下:

    func deferproc(siz int32, fn *funcval) { // fn表示defer 传入的函数地址
    	// 获取 caller 函数的 rsp 寄存器值
    	sp := getcallersp()
    	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    	// 获取 caller 函数的 pc(rip) 寄存器值
    	callerpc := getcallerpc()
    	
    	// 分配 _defer 内存结构
    	d := newdefer(siz)
    	if d._panic != nil {
    		throw("deferproc: d.panic != nil after newdefer")
    	}
    	// _defer 结构体初始化
    	d.fn = fn
    	d.pc = callerpc
    	d.sp = sp
    	switch siz {
    		case 0:
    		// Do nothing.
    		case sys.PtrSize:
    			*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    		default:
    			memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
     	}
    	 // 注意,特殊的返回,不会触发延迟调用的函数
    	 return0()
    }
    

    deferproc所做工作:

    1. 与栈上分配不同, _defer  结构是在deferproc函数里分配的(调用 newdefer 分配内存空间,newdefer 函数先去 pool 缓存池里看一眼,有就直接取用,没有就调用 mallocgc 从堆上分配内存);
    2. deferproc 函数接受参数 siz,fn ,这两个参数分别标识延迟函数的参数及返回值的内存大小、延迟函数地址;
    3. _defer.heap 字段用来标识这个结构体是分配在堆上;
    4. 保存上下文,把 caller 函数的 rsp,pc(rip) 寄存器的值保存到 _defer 结构体;
    5. _defer 作为一个节点挂接到链表;
    

    执行 defer 函数链( deferreturn )

    编译器遇到 defer 语句,会插入两类函数:

    分配函数:deferproc 或者 deferprocStack ;
    执行函数:deferreturn ;
    

    分配函数前面详细说过了,go1.13 之后大部分场景都会在栈上分配,函数调用的时机是在defer 关键字执行的时候。函数退出时,则由 deferreturn 负责执行所有的延迟调用链

    func deferreturn(arg0 uintptr) {
     gp := getg()
     // 获取到最前的 _defer 节点
     d := gp._defer
     // 函数递归终止条件(d 链表遍历完成)
     if d == nil {
      return
     }
     // 获取 caller 函数的 rsp 寄存器值
     sp := getcallersp()
     if d.sp != sp {
      // 如果 _defer.sp 和 caller 的 sp 值不一致,那么直接返回;
      // 因为,这种情况就说明这个 _defer 结构不是在该 caller 函数注册的  
      return
     }
    
     switch d.siz {
     case 0:
      // Do nothing.
     case sys.PtrSize:
      *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
     default:
      memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
     }
     // 获取到延迟回调函数地址
     fn := d.fn
     d.fn = nil
     // 把当前 _defer 节点从链表中摘除
     gp._defer = d.link
     // 释放 _defer 内存(主要是堆上才会需要处理,栈上的随着函数执行完,栈收缩就回收了)
     freedefer(d)
     // 执行延迟回调函数
     jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
    }
    

    deferreturn函数小结:

    1. 遍历 defer 链表,一个个执行,顺序链表从前往后执行,执行一个摘除一个,直到链表为空;
    2. jmpdefer 负责跳转到延迟回调函数执行指令,执行结束之后,跳转回 deferreturn 里执行;
    3. _defer.sp 的值可以用来判断哪些是当前 caller 函数注册的,这样就能保证只执行自己函数注册的延迟回调函数;
    4. 举个例子,a() -> b() -> c() ,a 调用 b,b 调用 c ,而 a,b,c 三个函数都有 defer 注册延迟函数,那么自然是 c()函数返回的时候,执行 c 的回调;
    

    defer 如何传递参数

    预计算参数

    在前面描述 _defer 数据结构的时候说到内存结构如下:
    _defer内存结构

    _defer 作为一个 header,延迟回调函数( defered )的参数和返回值紧接着 _defer 放置,而这个参数的值是在 defer 执行的时候就设置好了,也就是预计算参数,而非等到执行 defered 函数的时候才去获取。

    举个例子,执行 defer func(x, y) 的时候,x,y 这两个实参是计算出来的,Go 中的函数调用都是值传递。那么就会把 x,y 的值拷贝到 _defer 结构体中使用。再看个例子:

    package main
    
    func main() {
     var x = 1
     defer fmt.Println(x)
     x += 2
     return
    }
    

    这个程序标准输出是什么呢?是 1 ,还是 3 ?答案是:1 呢。
    defer 执行的函数是 Println ,Println 参数是 x ,实参x 传进去的值则是在 defer 语句执行的时候就确认了的

    defered 的参数准备

    defered 延迟函数执行的参数已经保存在和 _defer 一起的连续内存块了。那么执行 defered 函数的时候,参数是哪里来呢?当然不是直接去 _defer 的地址找。因为这里是走的标准的函数调用。

    在 Go 语言中,一个函数的参数由 caller 函数准备好,比如说,一个 main() -> A(7) -> B(a) 形成类似以下的栈帧:
    函数调用栈帧示例

    所以,***deferreturn 除了跳转到 defered 函数指令,还需要做一个事情:把 defered 延迟回调函数需要的参数准备好(空间和值)***。

    func deferreturn(arg0 uintptr) {
    
     switch d.siz {
     case 0:
      // Do nothing.
     case sys.PtrSize:
      *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
     default:
      memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
     }
    }
    

    arg0 就是 caller 用来放置 defered 参数和返回值的栈地址。这段代码的意思就是:把 _defer 预先准备好的参数,copy 到 caller 栈帧的某个地址(arg0)。

    一个函数多个 defer 语句的时候,会发生什么?

    _defer函数链
    _defer 是一个链表,表头是 goroutine._defer 结构。一个协程的函数注册的时候是挂在同一个链表,执行的时候按照 rsp 来区分函数。并且,这个链表是把新元素插在表头,而执行的时候是从前往后执行,所以这里导致了一个 LIFO 的特性,也就是先注册的 defered 函数后执行。

    图片

    defer 和 return 返回值运行顺序

    函数的调用过程

    要理解这些看似高深的原理,首先要回归最基础的知识:函数调用的过程。函数具体调用细节可以参看 深入剖析 defer 原理篇 —— 函数调用的原理?,复习一下知识点小结:

    1. go 的一行函数调用语句其实非原子操作,对应多行汇编指令,包括 1)参数设置,2) call 指令执行;
    2. 其中 call 汇编指令的内容也有两个:返回地址压栈(会导致 rsp 值往下增长,rsp-0x8),callee 函数地址加载到 pc 寄存器;
    3. go 的一行函数返回 return语句其实也非原子操作,对应多行汇编指令,包括 1)返回值设置 和 2)ret 指令执行;
    4. 其中 ret 汇编指令的内容是两个,指令pc 寄存器恢复为 rsp 栈顶保存的地址,rsp 往上缩减,rsp+0x8;
    5. 参数设置在 caller 函数里,返回值设置在 callee 函数里;
    6. rsp, rbp 两个寄存器是栈帧的最重要的两个寄存器,这两个值划定了栈帧;
    7. rbp 寄存器的常见的作用:作为栈基寄存器,以方便界定栈帧。但其实再深入了解下,rbp 在当今计算机体系里其实可以作为通用寄存器。而栈基寄存器最常见的作用还是为了调试,方便划定栈帧;
    

    最重要的一点:Go 的 return 的语句调用是个复合操作,可以对应一下两个操作序列

    1. 设置返回值
    2. ret 指令跳转到 caller 函数
    

    return 之后是先返回值还是先执行 defer 函数?

    Golang 官方文档是有明确说明的:

    That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.
    

    也就是说,defer 的函数链调用是在设置了 result parameters 之后,但在运行指令上下文返回到 caller 函数之前。
    所以在有 defer 调用的地方,执行 return 语句之后,对应执行三个操作序列:

    1. 设置返回值
    2. 执行 defered 链表
    3. ret 指令跳转到 caller 函数
    

    那么,根据这个原理我们来解析如下的行为:

    func f1 () (r int) {
     t := 1
     defer func() {
      t = t +5
     }()
     return t
    }
    
    func f2() (r int) {
     defer func(r int) { // defer在执行的时候,传入的参数r=0
      r = r + 5
     }(r)
     return 1
    }
    
    func f3() (r int) {
     defer func () {
      r = r + 5
     } ()
     return 1
    }
    

    这三个函数的返回值分别是多少?

    答案:f1() -> 1,f2() -> 1,f3() -> 6 。

    我逐个解释下:

    1. 函数 f1 执行 return t 语句之后:
    1.设置返回值:这里 r = t,这个时候局部变量 t 的值等于 1,所以 r = 1;
    2.执行 defered 函数,t = t+5 ,之后局部变量 t 的值为 6;
    3.执行汇编 ret 指令,跳转到 caller 函数;
    

    所以,f1() 的返回值是 1 。

    1. 函数 f2 执行 return 1 语句之后:
    1.设置返回值 r = 1 ;
    2.执行 defered 函数,defered 函数传入的参数是 r,r 在预计算参数的时候值为 0,Go 传参为值传递,0 赋值给了匿名函数的参数变量,所以 ,r = r+5 ,匿名函数的参数变量 r 的值为 5;
    3.执行汇编 ret 指令,跳转到 caller 函数;
    

    所以,f2() 的返回值还是 1

    1. 函数 f3 执行 return 1 语句之后:
    设置返回值 r = 1;
    执行 defered 函数,r= r+5 ,之后返回值变量 r 的值为 6(这是个闭包函数,注意和 f2 区分);
    执行汇编 ret 指令,跳转到 caller 函数;
    

    所以,f1() 的返回值是 6。

    在看,如下代码:

    func main() {
    	fmt.Println(test1())
    	fmt.Println(test2())
    
    }
    
    func test1() int {
    	var i int
    	defer func() {
    		i++
    		fmt.Println("test_1_defer1:", i)
    	}()
    
    	defer func() {
    		i++
    		fmt.Println("test_1_defer2:", i)
    	}()
    
    	return i
    }
    
    func test2() (i int) {
    	defer func() {
    		i++
    		fmt.Println("test_2_defer1:", i)
    	}()
    
    	defer func() {
    		i++
    		fmt.Println("test_2_defer2:", i)
    	}()
    
    	return i
    }
    
    

    运行结果如下:

    test_1_defer2: 1
    test_1_defer1: 2
    0
    test_2_defer2: 1
    test_2_defer1: 2
    2
    

    总结

    1. defer 关键字执行对应 _defer 数据结构,在 go1.1 - go1.12 期间一直是在堆上分配,在 go1.13 及之后优化成在栈上分配 _defer 结构,性能提升明显(go1.14之后,还有一个开放编码的优化,类似于内联);
    2. _defer 数据结构大部分场景是分配在栈上,但是遇到循环嵌套的场景会导致结构分配在堆上。所以,程序员在使用 defer 的时候要注意场景,否则可能出现性能问题; 
    3. _defer 对应一个注册的延迟回调函数(defered),defered 函数的参数和返回值紧跟 _defer,可以理解成 header,_defer 和函数参数,返回值所在内存是一块连续的空间,其中 _defer.siz 指明defer函数参数和返回值的所占空间大小;
    4. 在同一个协程里注册的多个defer,都挂在一个链表中,表头为 goroutine._defer;
    5. 新执行的defer(插入在待执行_defer链表的)在最前面,遍历执行的时候则是从前往后执行。所以 defer 注册函数具有 LIFO 后进先出的特性,也就是后注册的先执行;
    6. 不同的(defer)函数都在这个链表上,以 _defer.sp 区分;
    7. defered 的参数是预计算的,也就是在 defer 关键字执行的时候,参数就确认,赋值在 _defer 的内存块后面。执行的时候,copy 到栈帧对应的位置上;
    8. jmpdefer 修改了默认的函数调用行为(修改了压栈指令),实现了一个 defered 链表循环执行,直到执行完成;
    9. 在defer之后的return是 对应 3 个动作的复合操作:1)设置返回值;2)执行 defered 函数链表;3)ret 指令跳转;
    

    参考

    Go 的 defer 的特性还是有必要要了解下的!!!
    Go 使用 defer 计算函数耗时为什么总是很少?
    defer 的前世今生
    golang中的defer必须掌握的7个知识点
    深入剖析 defer 原理篇 —— 函数调用的原理?
    解密 defer 原理,究竟背着程序猿做了多少事情?

    展开全文
  • 控制流:条件分支语句(if)基本要素1:预设的判断条件2:达成条件后执行的语句扩展要素1:当条件不满足时执行的语句2:多条件时哪个满足执行哪个的条件条件语句Python中条件分支的实现if :……else:if和e...
  • 前文分享了Procmon软件基本用法及文件进程、注册表查看,这是一款微软推荐的系统监视...这篇文章将分享APT攻击检测溯源与常见APT组织的攻击案例,并介绍防御措施。希望文章对您有所帮助。基础性文章,希望对您有所帮助~
  • while 循环格式:while 条件 为 True:代码while True:rayn_age = 18age = input('请输入你的年龄:')age = int(age)if age == rayn_age:print('恭喜你答对了')elif age > rayn_age:print('猜大了')else:print('...
  • 这篇文章介绍在oracle数据文件中出现一个或多个数据...包含这个坏的数据文件的文件名可以标示为"FILENAME"(如果知道文件号但不知道文件名那么可以执行select name from v$datafile where file#=&AFN来得到文件...
  • C语言循环嵌套结构教学设计探究摘 要:C程序循环嵌套结构设计是C程序循环结构教学一章的重点与难点内容,是后续数据结构课程算法实现中使用频率最多的语句,是进行复杂程序设计的基础。该文将针对初学者实际情况,...
  • 前言我们从一个问题引入今天的主题。在日常业务开发中,我们可能经常...其实这涉及到 join 语句在 MYSQL 内部到底是怎么执行的。这就是我们今天要讲的内容。预备知识点STRAIGHT_JOINSTRAIGHT_JOIN与 JOIN 类似,不...
  • 通过将while循环同列表和字典结合起来使用,收集,存储并组织大量输入,方便查看。 在列表之间移动元素 假设有一个列表,其中包含新注册但未验证的网站用户;验证这些用户后,如何将他们移动到另一个已验证用户列表...
  • 原文链接:python的垃圾回收机制及循环引用 - libochou - 博客园 https://www.cnblogs.com/libochou/p/10150048.html[转]java垃圾回收之循环引用 - kkmm - 博客园 ...
  • PHP 循环列出目录内容的函数代码复制代码 代码如下:function list_files($dir){if(is_dir($dir)){if($handle = opendir($dir)){while(($file = readdir($handle)) !== false){if($file != "." && $file != ...
  • python以什么划分语句块?python是通过缩进格式来划分语句块的;具有相同缩进的代码被自动视为一个代码块,无论进行...语句块是在条件为真(条件语句)时执行或者执行多次(循环语句)的一组语句。另外很重要的一点,就...
  • 循环等待条件 多个线程互相等待对方释放资源 死锁预防,那么就是需要破坏这四个必要条件: 由于资源互斥是资源使用的固有特性,无法改变,我们不讨论 破坏不可剥夺条件 一个进程不能获得所需要的全部资源时便...
  • 组织结构图1、插入组织结构图:单击[插入]/[图片]/[组织结构图],弹出[组织结构图]工具栏,并可在每一个图中输入说明性文字。2、编辑组织结构图:◆添加与删除图:右击图在快捷菜单中选择添加同事、下属或删除...
  • 2 循环体:要重复执行的操作; 3 修正部分:修改循环变量的值,为循环的下一次重复做准备; 4 检查部分:测试循环条件,判断循环是否还要重复。 在一个循环结构中,以上四个部分缺一不可。循环程序有哪两种基本结构...
  • 一般用8个悬挂点为一个循环,其左右偏差为值为100-150mm。[多选] 设某对旅客列车每日开行一次,车底配属站和折返站之间运行距离为1890公里,列车直达速度为60公里每时,技术速度95公里每时,车底...
  • 顺序结构的程序语句只能被执行一次。...while 循环while是最基本的循环,它的结构为:while( 布尔表达式 ) {//循环内容}只要布尔表达式为 true,循环就会一直执行下去。创一个小群,供大家学习交流...
  • 实验3:循环结构

    2021-04-17 10:48:19
    一、实验目的掌握 range 函数的使用;了解整数除法和普通除法的...本题是最为简单的循环,每次执行循环体中的内容完全一样。提示:使用 range 函数水仙花数所谓”水仙花数”是指一个三位数,其各位数字立方和等于...
  • 换专业确实挺难的,逆向分析也是硬骨头,但我也试试,看看自己未来四年究竟能将它学到什么程度,漫漫长征路,偏向虎山行。享受过程,一起加油~ 前文带大家学习了网络安全攻防知识点,并以医疗数据安全为基础进行...
  • 主存字标记(7位) Cache字地址(9位) 字内地址(5位) 在四路组相联映射方式下,主存字标记为21-7-5=9位,主存地址格式为:主存字标记(9位) 组地址(7位) 字内地址(5位) 在全相联映射方式下,主存字标记为...
  • 本文给大家介绍用户输入以及while循环。首先给大家介绍函数input()的工作原理。 一、函数input()的工作原理   函数input()让程序暂停运行,等待用户输入一些文本。获取用品输入后,Python将其存储在一个变量中,以...
  • 一、设备与字符设备的区别 字符设备与设备IO操作的不同如下。 设备只能以为单位接收输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定大小进行操作。 ...
  • 而不是积累起来for循环和嵌套来处理列表和集合中的数据,您可以利用这些方法更好地将逻辑组织成功能的构建,然后将它们链接起来以创建更可读和更易于理解的实现。ES6还为我们提供了一些更好的数组方法,比如.find,...
  • Java 循环语句

    2021-08-05 20:22:44
    java的循环语句 for循环 含义:条件成立就重复执行 好处:减少了代码的冗余(减少重复性的代码) 语法结构: ... 2.1 true - 执行代码,并更新变量,重复第2个步骤 2.2 false- 跳出整个循环语句 for循
  • 在java中,可以用switch语句将动作组织起来,以一个较简单明了的方式来实现“多旋一”的选择。首先我们在学习一个Java的语句时,我们首先就需要学习这个语句的语法,和我们前面学习是一样的,switch语句的语法如下;...
  • linux面试题及答案

    2021-05-16 03:53:59
    2. Linux设备中字符设备与设备有什么主要的区别?请分别列举一些实际的设备说出它们是属于哪一类设备。字符设备:字符设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。字符...
  • 西门子1200PLC的OB用法讲解

    千次阅读 2020-12-20 12:26:16
    组织块是CPU系统和用户程序之间的接口,可以在CPU上电启动时调用,也可以循环调用,也可以在PLC发生错误时候调用。 DB:数据块,英文名Data block。从字面含义便知主要用于储存用户数据,比如模拟量转换数据,相当于...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 71,817
精华内容 28,726
关键字:

循环执行的组织块是