1. Си / Говнокод #27266

    +3

    1. 01
    2. 02
    3. 03
    4. 04
    5. 05
    6. 06
    7. 07
    8. 08
    9. 09
    10. 10
    11. 11
    12. 12
    13. 13
    14. 14
    15. 15
    16. 16
    17. 17
    18. 18
    19. 19
    20. 20
    21. 21
    22. 22
    23. 23
    24. 24
    25. 25
    26. 26
    // https://deadlockempire.github.io/
    // Игра, где надо играть за планировщик чтоб вызвать дедлок
    
    // https://deadlockempire.github.io/#2-flags
    
    // First Army
    
    while (true) {
      while (flag != false) {
        ;
      }
      flag = true;
      critical_section();
      flag = false;
    }
    
    // Second Army
    
    while (true) {
      while (flag != false) {
        ;
      }
      flag = true;
      critical_section();
      flag = false;
    }

    The day finally came. The Deadlock Empire opened its gates and from them surged massive amounts of soldiers, loyal servants of the evil Parallel Wizard. The Wizard has many strengths - his armies are fast, and he can do a lot of stuff that we can't. But now he set out to conquer the world, and we cannot have that.

    You are our best Scheduler, commander! We have fewer troops and simpler ones, so we will need your help. Already two armies of the Deadlock Empire are approaching our border keeps. They are poorly equipped and poorly trained, however. You might be able to desync them and break their morale.

    Запостил: j123123, 20 Февраля 2021

    Комментарии (60) RSS

    • flag=false

      армия:строка

      1:9
      2:20
      1:12
      2:23
      1:13
      2:24
      еблысь
      Ответить
      • ну кстати тут может быть еще смешнее

        если во круг флага нет заборов, то он по идее может остаться в буфере ЦПУ, и в теории армия1 вообще никогда не узнает, что его изменила армия2.
        Ответить
        • Тут может быть ещё смешнее. Конпелятор увидит, что внутри цикла флаг не меняют и выбросит весь код нахуй.

          Да и flag = true с flag = false может слипнуться ещё во время конпеляции.

          З.Ы. Угу, gcc от всей этой хуйни только внешний while (1) оставил.
          Ответить
          • >нахуй
            volatile нужно пхать, да?
            Ответить
            • Если гонка с железом или прерываниями, то volatile. Если гонка с соседним ядром, то atomic. Не перепутай!
              Ответить
              • как сказать компилятору "не выкидывай этот код, ты не знаешь, кто срет в эту переменную"?
                я думал, что это volatile.

                Срать может как и другое ядро, так и железо. просто в случае с другим ядром этого НЕДОСТАТОЧНО, потому что код-то он не выкинет, но гонка останется

                или нет?
                Ответить
                • volatile запрещает оптимизировать обращения, но барьеров не будет. Поэтому critical_section() может выплыть из-под "мутекса". Если заинлайнится, конечно.

                  Атомик втыкает барьеры, но не запрещает оптимизировать обращения. Они могут слипнуться если это никому не ломает happens before.
                  Ответить
                  • Спасибо, я это понимаю.

                    Я имел ввиду, что volatile хоть и не сделает код корректным, но гарантирует, что его хотя-бы не выкинут.

                    Потому что без волатайла тут написано ``while(true){}``

                    Разумеется, обращение к переменной из двух потоков нужно синхронизировать барьером.
                    Ответить
                  • Как всё сложно...
                    Ответить
              • > Если гонка с железом или прерываниями, то volatile.

                Допустим есть микроконтроллер с одним ядром, есть некая глобальная volatile переменная, которая может читаться/писаться в обработчике прерывания и в обычном коде. Допустим, переменная там 64-битная, а инструкции записи байтиков в память есть максимум на 32 бит, и вот в коде мы наполовину перезаписали volatile переменную новым значением, и тут хуяк - обработчик прерывания. А в переменной вообще хуйня какая-то непредусмотренная
                Ответить
                • :) ну по сути у тебя проблема многопоточности с одним ядром.

                  Выходит, что запись в такую переменную это критическая секция, и нужно маскировать прерыввания на время записи, не?
                  Ответить
                  • Да, но маскировка не отменяет volatile.
                    Ответить
                    • Потому что volatile это защита от выкидывания кода компилятором, а маскировка (или барьеры) это защита от железа. Верно?

                      У нас в жабке эти две концепции слиты воедино, нам проще
                      Ответить
                      • Ну в атомарных интринсиках обычно обе защиты есть: конпеляторный барьер и процессорный барьер.

                        Но, насколько я помню, для x.store(42); x.store(100500) конпелятор и проц имеют право выкинуть первую инструкцию. Порядок это не ломает, сторонний наблюдатель просто подумает, что он пиздец невезучий, раз никогда не видит там 42.
                        Ответить
              • Вообще вот есть: https://stackoverflow.com/questions/2484980

                > ... That is all we need for what volatile is intended for: manipulating I/O registers or memory-mapped hardware, but it doesn't help us in multithreaded code where the volatile object is often only used to synchronize access to non-volatile data. Those accesses can still be reordered relative to the volatile ones.

                volatile вообще нехуй так использовать, компилятор имеет право всякое говно между этими volatile переставлять

                https://godbolt.org/z/Pad3n5
                volatile int volatile_crap = 0;
                
                int regular_crap = 0;
                
                void test()
                {
                  volatile_crap++;
                  regular_crap++;
                  volatile_crap++;
                  regular_crap++;
                }

                - вот например оно два инкремента не-volatile глобальной переменной спихнуло в одну инструкцию после всей волатильной питушни
                test:                                   # @test
                        add     dword ptr [rip + volatile_crap], 1
                        add     dword ptr [rip + volatile_crap], 1
                        add     dword ptr [rip + regular_crap], 2
                        ret
                Ответить
                • А 64-битный атомик на 32-битном ядре повиснет нахуй если он через спинлок сэмулирован, лол. Тоже не лучшее решение для расшаривания данных с прерыванием.
                  Ответить
                  • так потмоу что regular_crap регулярный. Если бы он был volatile, то компилятор не имел бы право менять их местами

                    например
                    volatile_crap++
                    нельзя свернуть в volatile_crap += 2
                    а регулярный можно.
                    Ответить
                  • Можно придумать довольно-таки ебанутый способ расшарить данные с прерыванием. Допустим, есть 64-битная глобалка, которую надо читать/писать и из прерывания и обычным способом, а инструкции записи/чтения байтиков есть только 32-битные. Тогда можно в обработчике прерывания узнавать, из какого места сработало прерывание, и если оно сработало как раз тогда, когда из глобалки байтики читались и записывались в какие-то регистры (и байтики недопереписались), то тогда само прерывание должно там доделать, т.е. дозаписать байтики в регистры треда(т.е. сохраненный стейт треда), который был прерван прерыванием, и передвинуть instruction poiner на те инструкции, которые идут после инструкций записи байтиков из глобалки в регистры общего назначения.
                    И потом дальше делать как обычно.
                    Ответить
                    • Блядь, как всё сложно. Именно поэтому я за "PHP".
                      Ответить
                    • Можно ещё добавить флажок, как у мутекса. И если прерывание его видит, то синглстепать прерванный код пока флажок не уйдёт.
                      Ответить
                      • малость лапшекод будет
                        Ответить
                        • Да почему, можно в RAIIшненькую хуиту завернуть. Будет как обычное присваивание.

                          interrupt_safe_atomic<uint64_t> x;

                          x = 42;
                          Ответить
                • Ну а так да, я хуйню написал, раз все это читают как "volatile достаточно для синхронизации с ISR". Недостаточно, тот же инкремент не прокатит.
                  Ответить
                  • >инкремент
                    А если у CPU есть атомарная инструкция для этого?
                    Ответить
                    • Да он не будет её юзать для обычного инкремента. Дорого же. Посмотри на любой дизасм, у тебя там везде обычный add а не xadd.
                      Ответить
                      • Ну а если я явно попрошу?

                        В жабке есть AtomicInteger например
                        Ответить
                        • Если явно попросишь (через асм или какой-нибудь std::atomic) -- будет работать, конечно. Но там и volatile уже не нужен.
                          Ответить
                          • >. Но там и volatile уже не нужен.
                            это std::atomic дает такую гарантию?

                            Потому что если я просто использую атомарную иструацию цепеу, то мне это не поможет.



                            PetuhCountAtomicIncrement()
                            SendDataToIoPort()
                            PetuhCountAtomicIncrement()

                            если питух не волатилен, то что мешает компилятору сделать

                            PetuhCountAtomicIncrement()
                            PetuhCountAtomicIncrement()
                            SendDataToIoPort()

                            ?
                            Ответить
                            • Ну для relaxed'ов -- да, переставит. Но ты же не будешь юзать relaxed если тебе важен порядок.
                              Ответить
                      • Я не скажу за все архитектуры, но что касается Intel:
                        https://www.intel.ru/content/www/ru/ru/architecture-and-technology/64-ia-32-architectures-software-developer-system-programming-manual-325384.html


                        > 8.1.1 Guaranteed Atomic Operations
                        >
                        > The Intel486 processor (and newer processors since) guarantees that the following basic memory operations will always be carried out atomically:
                        >
                        > •Reading or writing a byte
                        > •Reading or writing a word aligned on a 16-bit boundary
                        > •Reading or writing a doubleword aligned on a 32-bit boundary
                        >
                        > The Pentium processor (and newer processors since) guarantees that the following additional memory operations will always be carried out atomically:
                        >
                        > •Reading or writing a quadword aligned on a 64-bit boundary
                        > •16-bit accesses to uncached memory locations that fit within a 32-bit data bus
                        >
                        > The P6 family processors (and newer processors since) guarantee that the following additional memory operation will always be carried out atomically:
                        >
                        > •Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line

                        т.е. никакого xadd не надо для атомарности инкремента, если адрес записи выровнен по нужной границе
                        Ответить
                        • Хотя не, это о записи/чтении памяти, а не об атомарной модификации.
                          Ответить
                          • Ну да, это скорее что он не прочитает половинку старых данных и половину новых. Как это может произойти на границе кешлайна для криво выравненной переменной.

                            Емнип, на практике не атомарный add ещё как плющит из нескольких потоков.
                            Ответить
                  • Тут еще такой момент: если два треда инкрементируют/декрементируют одну и ту же глобалку, тут понятно что нужен атомик, но вот если есть один обычный "тред" и прерывание (как в каком-нибудь контроллере, и нихуя нет планировщика процессов как такового), то атомарные инструкции достаточно использовать только для "треда", потому как прерывание никто на полпути не прервет (если нет каких-то других прерываний, которые имеют более высокий приоритет, и которые в ту же глобалку лезут) так что в прерывании использование атомарной модификации избыточно. В прерывании даже volatile не требуется
                    Ответить
                    • Ну если ты уверен, что тебя не вытеснит более важное прерывание, то ок.
                      Понятное дело, что обычный поток кода не может выполняться, пока выполняется прерывание в одноядерной системе) Это я еще со времен доса помню.

                      А как именно код и обработчик прерывания достукиваются до этой глобалки?
                      Если это просто какое-то место в памяти, то разве не нужно его поменчать как volatile? компилятор же выкинет иначе нафиг всё, нет?
                      Ответить
                      • > А как именно код и обработчик прерывания достукиваются до этой глобалки?
                        По адресу памяти, очевидно.

                        > Если это просто какое-то место в памяти, то разве не нужно его поменчать как volatile?
                        Нет, зачем?

                        > компилятор же выкинет иначе нафиг всё, нет?
                        С чего б ему выкидывать что-то?
                        Ответить
                        • p = 1234;
                          *p = 1;

                          он видит, что автматическая переменная p нигде не испльзуется, и выкидывает ее

                          или нет?
                          Ответить
                          • Мы ж про глобалки говорим, а не про какие-то переменные в скоупе. Глобалки должны поменяться, когда выходим из функции, которая какие-то глобалки поменяла.
                            Ответить
                          • Если у тебя поебень типа
                            void shit(void)
                            {
                              char *a = (char *)0xAABBCCDD;
                              *a = 51;
                            }

                            то запись байтика по адресу 0xAABBCCDD у тебя нихуя выкидываться не будет
                            Ответить
                            • хотя вот если так
                              int glob_var = 0;
                              
                              void shit(void)
                              {
                                glob_var = 1488;
                              }
                              
                              void shit2(void)
                              {
                                glob_var = 666;
                              }
                              
                              void shit3(void)
                              {
                                shit();
                                shit2();
                                glob_var = 1337;
                              }

                              то если в вызов shit3 заинлайнивается вся хуйня и компилятор это заоптимизирует, то запись в glob_var там будет только одна, ибо нехуй лишний раз ворошить память. И если shit3() это какой-то обработчик прерываний, то "применить" изменения глобалок надо тогда, когда весь этот обработчик завершится, так что по итогу там будет 1337 и никакие предыдущие записи в глобалку нахуй не нужны, раз обработчик никто на полпути не прервет. Но если glob_var это какая-то хуйня типа MMIO и там особая хуйня должна происходить при записи говна по таким-то адресам, то тогда вот можно volatile юзать
                              Ответить
                            • да, ты прав. Я запутался в понятии сайдэффекта, и мне нужно почитать стандарт.

                              автоматические переменные никто не видит, но так как память-то у нас не в стеке, то запись в нее нельзя выкидывать

                              выкинуть
                              *a = 51;
                              он может только если потом идет
                              *a = 52

                              и тут нужен волатил
                              Ответить
                          • Да не, выкидывается только код, который не имеет полезных сайд-эффектов.

                            Если насрать в глобалку и выйти -- конпелятор обязан сохранить такой эффект. Он даже отложить его на попозже его не может т.к. ISR никуда не инлайнится.
                            Ответить
                            • да, я тупанул, и запутался в трех соснах.

                              запись в глобалку нельзя отменить, можно только схлопнуть две записи
                              Ответить
                      • >Ну если ты уверен, что тебя не вытеснит более важное прерывание, то ок.
                        Оно вполне может вытеснять, важно чтоб оно не конфликтовало, т.е. не ворошило те же самые глобалки, с которыми то прерывание имеет дело.
                        Ответить
                    • > других прерываний

                      В том же "uefi" таймерное прерывание может само себя прервать 4-5 раз. Такие вот дела.
                      Ответить
        • Clang, кстати, интереснее сконпелял:
          // test
              cmp flag, 0
              jz L2
          L1:
              jmp L1
          L2:
              jmp L2
          Какой философский код )))
          Ответить
          • то есть так у нас бесконечный цикл, а так бесконечный цикл
            понятно
            Ответить
    • > This feels a bit too easy, and I needed absolutely no understanding of any of the underlying concepts.

      Congratulations, you have triggered deadlocks in same way normal CPUs do!
      Ответить
    • Иногда мне хочется хлопнуть дверью и уйти, после отправив отцу видео, где я кончаю. Как Сальвадор Дали.
      Ответить
    • Дикая годнота. Спасибо.
      Ответить

    Добавить комментарий