---
# System prepended metadata

title: Lista 3
tags: [SO2022]

---

# Lista 3
###### tags: `SO2022`

![](https://i.imgur.com/YBT6ZSX.png)

## Deklaracja
:::spoiler

| Zadanie   | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|-----------|---|---|---|---|---|---|---|---|
|Deklaracja|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
:::

## Zadanie 1
![](https://i.imgur.com/EPkPKRw.png)

```
terminal1:
    xterm -e 'bash -i' &
    ps -eo user,pid,sid,ppid,pgid,cmd
    
xterm:
    sleep 1000 &

terminal2:
    kill -9 BASH_PID lub XTERM_PID
    ps -eo user,pid,sid,ppid,pgid,cmd
```

* ### signal do Bash'a
    `sleep` zmienia parent pid na init (lub systemd user init)
    :::spoiler Przykład
    ![](https://i.imgur.com/3BS4ypi.png)
    
    ![](https://i.imgur.com/mBm5QvZ.png)


    **bash -i** (lider sesji bo pid == sid):
    pid 55115
    sid 55115

    **sleep**
    sid 55115
    ppid
    * przed sigkillem: 55115 (bash -i)
    * po: 1 (init)

    sleep
    ![](https://i.imgur.com/Xbr3FTJ.png)
    bash -i
    ![](https://i.imgur.com/EvKYb7S.png)
    :::

* ### signal do emulatora
    Po zabiciu sigkillem xterma do wszystkich procesow dzieci (czyli u nas `bash -i`) zostają wysłane SIGHUP.
    Bash rozsyła otrzymany SIGHUP dalej:
    
    The shell exits by default upon receipt of a 'SIGHUP'.  Before
    exiting, an interactive shell resends the 'SIGHUP' to all jobs, running
    or stopped.  Stopped jobs are sent 'SIGCONT' to ensure that they receive
    the 'SIGHUP'.  To prevent the shell from sending the 'SIGHUP' signal to
    a particular job, it should be removed from the jobs table with the
    'disown' builtin (*note Job Control Builtins::) or marked to not receive
    'SIGHUP' using 'disown -h'.

    :::spoiler Przykład
    ![](https://i.imgur.com/vajrzp7.png)

    sleep i xterm:
    
    ![](https://i.imgur.com/9hKHwRG.png)
    
    ![](https://i.imgur.com/Rs5JEmE.png)
    :::


## Zadanie 2
![](https://i.imgur.com/GjkPyzK.png)

* ### Jak zachowuje się sterownik terminala działającego w trybie kanonicznym (czytanie linijkami)?

    Input terminala jest przetwarzany linijkami, których końce są wyznaczone przez:
    * znaki nowej linii `\n`
    * lub end-of-file (EOF)
    * lub end-of-line (EOL)
    
    Limit wielkości linii wyznaczony jest przez `MAX_CANON`.

    Przetwarzane są również specjalne znaki `ERASE` (usuń znak) (oraz podobne typu `WERASE`, jeżeli flaga `IEXTEN` jest ustawiona) oraz `KILL` (usuń wszystkie dotychczasowe znaki z linii).

    W większości systemów UNIXowych jest moduł nazywany `terminal line discipline`, który jest pośrednikiem między funkcjami `read` oraz `write` kernela, aby wszystkie urządzenia terminala mogły przetwarzać płynnie tryb kanoniczny.

    ![](https://i.imgur.com/y5iGQ5S.png)


* ### W jaki sposób przetwarza on znaki (w tym kody sterujące) wchodzące do kolejki wejściowej/wyjściowej?

    ![](https://i.imgur.com/2d1ROei.png)

    * #### Tryb kanoniczny 
        jak wyżej
    * #### Tryb niekakoniczny
        W tym trybie input jest dostępny natychmiastowo (zależnie od ustawień zmiennych odpowiadających za czas i minimalną liczbę wczytanych znaków w strukturze `termios`).
        Nie da się go edytować - bufor jedynie przyjmuje 4095 znaków (z konieczności zapewnienia miejsca na znak końca w trybie kanonicznym, gdybyśy zdecydowali się na niego przełączyć).

        Również rozmiar **output queue** jest ograniczony, jednak nie ma dostępu do stałych określających ten rozmiar.
        Kiedy output queue się zapełnia, kernel usypia proces pisania, aż zwolni się miejsce.
        Można to wykonać również funkcjami `tcflush` lub ustawieniami `tcsetattr`, które czyszczą input lub output queue.

    Jeżeli opcja **echo** jest włączona, to znaki z inputu pojawiają się również na outpucie.

    Dostępne są również **specjalne znaki** (np. `ctrl+c`), które w różny sposób są przetwarzane (np. wysyłanie sygnałów), opisane w tablicy `c_cc` w strukturze `termios` (część z nich można edytować, lub wyłączać specjalnymi flagami)

* ### Jak konfigurację terminala powinien zmienić program na czas wpisywania hasła przez użytkownika?

    Nie powinien wyświetlać wpisywanych znaków. 
    Wyłączamy opcję echo
    
    :::spoiler Obrazowanie różnic
    `stty -a < /dev/pts/[nr]`

    ![](https://i.imgur.com/vQbtrSC.png)


    **stty -a normalnie**
    ```
    speed 38400 baud; rows 23; columns 80; line = 0;
    intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
    eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;
    werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
    -parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
    -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
    -iuclc -ixany -imaxbel iutf8
    opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
    isig icanon iexten **echo** echoe echok -echonl -noflsh -xcase -tostop -echoprt
    echoctl echoke -flusho -extproc
    ```
    **podczas wpisywania hasła**
    ```
    speed 38400 baud; rows 23; columns 80; line = 0;
    intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
    eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;
    werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
    -parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
    -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
    -iuclc -ixany -imaxbel iutf8
    opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
    isig icanon iexten **-echo** echoe echok -echonl -noflsh -xcase -tostop -echoprt
    echoctl echoke -flusho -extproc
    ```
    :::

* ### Czemu edytory takie jak `vi` konfigurują sterownik terminala do pracy w trybie niekanonicznym?

    Ponieważ nie wszystkie instrukcje sterujące programu są zakończone znakami kończącymi wczytanie linii (to by było bardzo niewygodne!), np. `dd` usuwające linie, czy `hjkl` i wiele (chyba większość) innych poleceń w edytorach

## Zadanie 3
![](https://i.imgur.com/51fwKmU.png)
```
terminal1:
    stty -a 
```
* ### Sygnały związane z **zarządzaniem zadaniami**:
    - `intr` wysyła sygnał przerwania
    - `quit` wysyła sygnał zamknięcia
    - `swtch` włącza inną warstwę powłoki
    - `start`/`stop` wznawia/wstrzymuje wyświetlanie
    - `susp` wysyła sygnał stop

* ### Sygnały związane z **edycją wiersza** (do zaprezentowania z `cat`):
    - `erase` kasuje ostatni wprowadzony znak,
    - `kill` kasuje całą linię
    - `eof` wysyła znak końca pliku (końca wejścia)
    - `eol`/`eol2` wysyła znak końca wiersza
    - `werase` kasuje ostatnie wprowadzone słowo
    - `rprnt` powtarza bieżący wiersz
    - `lnext` wprowadza kolejny znak w cudzysłowie

* :::spoiler Przykłady
    ![](https://i.imgur.com/b4Izi3D.png)
    ![](https://i.imgur.com/1mBSXTE.png)
    :::
* ### Zmiana rozmiaru okna
    Program zostaje poinformowany o zmianie rozmiaru okna przy odebraniu sygnału `SIGWINCH`
    ```
    terminal1:
       trace -e 'trace=!all' -p PID_NANO
       stty -F WYNIK_TTY columns 20
    terminal2:
        tty
        nano plik    Zminiamy rozmiar okna i obserwujemy sygnały
    ```

## Zadanie 4
![](https://i.imgur.com/CqHeObV.png)

```
terminal1:
    echo -e '\e[10A';read
    echo -e '\e[1mBold\e[3mItalic\e[0mReset'
    cat         strzałki wypisują ^[[A ^[[B ^[[C ^[[D
    type echo   wbudowane 
    type cat    program
```
* [Kody CSI](https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797)
* [Kody CSI Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences)
* :::spoiler Przykłady
    ![](https://i.imgur.com/zs8Ap93.png)

    ![](https://i.imgur.com/HJUH27D.png)

    ![](https://i.imgur.com/4HnN6hN.png)
    :::


## Zadanie 5
![](https://i.imgur.com/PV3SuYI.png)

1. ### Wstrzymaj zadanie pierwszoplanowe `sleep 1000` i przy pomocy wbudowanego polecenia powłoki `bg` przenieś to zadanie do wykonania w tle. Jaki sygnał został użyty do wstrzymania zadania?
    ```
    terminal1:
        strace -e 'trace=!all' -p PID_SLEEP
    
    terminal2:
        sleep 1000
        ctrl+z
        ps - pozmany pid
        bg %1
    ```
    * Zatrymanie `ctrl+z` `SIGSTP`
    * Wznowienie `bg` `SIGCONT`

2. ### Uruchom `find /`. W trakcie jego działania naciśnij na przemian kilkukrotnie kombinację klawiszy `CTRL+S` oraz `CTRL+Q`. Czemu program zatrzymuje się i wznawia swoją pracę, skoro sterownik terminala nie wysyłał do niego żadnych sygnałów?
    
    
    ```
    terminal1:
        strace -e 'trace=!all' -p PID_FIND
    
    terminal2:
        find /
        ctrl+z
        ps - pozmany pid
        fg
        ctrl+s
        ctr+q
    ```
    
Są to sekwencje sterowania terminalem
Domyślnie w naszym terminalu: `start = ^Q; stop = ^S;`
[software_flow_control](https://en.wikipedia.org/wiki/Software_flow_control)
[tty_ioctl](https://man7.org/linux/man-pages/man4/tty_ioctl.4.html)
* `stop` - zatrzymuje output (sterownik terminala deklaruje że nie jest w stanie przyjąć więcej danych i wysyła `XOFF`)
* `start` - ponawia zatrzymany output (wysłaniem `XON`)


3. ### Uruchom w powłoce `bash` polecenie `cat - &`. Czemu zadanie zostało od razu wstrzymane? Jaki sygnał otrzymało? Zakończ to zdanie wbudowanym poleceniem powłoki `kill`.
    ```
    terminal1:
        strace -e 'trace=!all' -p PID_CAT
    
    terminal2:
        cat - &
        fg
        ctrl+z
        bg
        kill -9 PID_CAT
    ```
    
Zostało wstrzymane sygnałem `SIGTTIN`
    
* `SIGTTIN`  Zatrzymuje proces gdy próbuje czytać z Terminala, domyślnie włączone
* `SIGTTOU`  Zatrzymuje proces gdy próbuje pisać na Terminala, domyślnie wyłączone


4. ### Porównaj działanie polecenia `cat /etc/shells &` przed i po zmianie konfiguracji terminala poleceniem `stty tostop`. Jaki efekt ma włączenie flagi `tostop` na zachowanie sterownika terminala?
    ```
    terminal1:
        stty -a             pokaże konfigurację bierzącego terminala
        cat /etc/shells &
        stty tostop         SIGTTOU zaczyna być wysyłany 
        cat /etc/shells &
        stty -a             widać zmianę tostop
    ```
    
    Wynik: po ustawieniu `tostop` zadanie po przeniesieniu na drugi plan, przy próbie zapisu danych na standardowe wyjście otrzymuje `SIGTTOU`
    
    :::spoiler Wynik z Terminala
    ![](https://i.imgur.com/Lj12BIt.png)
    :::


5. ### Wykonaj polecenie `stty -echoctl`. Wyjaśnij co zmieniło się w konfiguracji terminala i zaprezentuj na przykładzie programu `cat` pokaż jak zmieniło się przetwarzanie znaków sterujących.
    ```
    terminal1:
        stty -echoctl    wyłącza notację z daszkiem np. ^Z, znaki są przetwarzane
        cat -            możemy chodzić strzałkami po całym terminalu
    ```




## Zadanie 6
![](https://i.imgur.com/qjw4UfE.png)
```x86asm=
_JB_RBX = 0
_JB_RBP = 1
_JB_R12 = 2
_JB_R13 = 3
_JB_R14 = 4
_JB_R15 = 5
_JB_RSP = 6
_JB_RIP = 7

        .text

        .globl Setjmp
        .type Setjmp,@function
Setjmp:
        ; Zapisujemy rejestry o których wartość 
        ; powinna dbać funkcja wywoływana 
        ; oraz wskaźnik na stos i następną instrukcję
    movq    (%rsp),%r11
    movq    %rbx,(_JB_RBX * 8)(%rdi)
    movq    %rbp,(_JB_RBP * 8)(%rdi)
    movq    %r12,(_JB_R12 * 8)(%rdi)
    movq    %r13,(_JB_R13 * 8)(%rdi)
    movq    %r14,(_JB_R14 * 8)(%rdi)
    movq    %r15,(_JB_R15 * 8)(%rdi)
    movq    %rsp,(_JB_RSP * 8)(%rdi)
    movq    %r11,(_JB_RIP * 8)(%rdi)
        ; Zracamy 0
    xorl	%eax,%eax
    ret
        .size Setjmp, . - Setjmp

        .globl Longjmp
        .type Longjmp,@function
Longjmp:
        ; Przywracamy zrejestry zapisane w setjump
    movq    (_JB_RBX * 8)(%rdi),%rbx
    movq    (_JB_RBP * 8)(%rdi),%rbp
    movq    (_JB_R12 * 8)(%rdi),%r12
    movq    (_JB_R13 * 8)(%rdi),%r13
    movq    (_JB_R14 * 8)(%rdi),%r14
    movq    (_JB_R15 * 8)(%rdi),%r15
    movq    (_JB_RSP * 8)(%rdi),%rsp
    movq    (_JB_RIP * 8)(%rdi),%r11
        ; Przenosimy wartość przekazaną 
        ; do funkcji do rejestru zwracanego przez funkcję
    movl	%esi,%eax
        ; Zabezpieczamy się przed zwróceniem 0,
        ; które przeznaczone jest tylko jako powrót z setjmp 
    testl	%eax,%eax
    jnz	1f
    incl	%eax
        Ustalamy adres powrotu
1:	movq	%r11,(%rsp)
    ret
        .size Longjmp, . - Longjmp

```

```c=
typedef struct {
  long rbx;
  long rbp;
  long r12;
  long r13;
  long r14;
  long r15;
  void *rsp;
  void *rip;
} Jmpbuf[1];

int Setjmp(Jmpbuf env);
noreturn void Longjmp(Jmpbuf env, int val);
```
* ### Dlaczego `Jmpbuf` nie przechowuje wszystkich rejestrów procesora?
    Część rejestrów jest zapisywana przez funkcję wywołującą, a zęść przez funkcję wywolywaną, i te rejestry zapisujemy
    Zapisujemy też wskaźnik na stos i następną instrukcję.
* ### Czemu `Longjmp` zapisuje na stos wartość przed wykonaniem instrukcji `ret`?
    Ustalamy miejsce w kodzie do którego chcemy wrócić


PRZYKLAD Z PODRECZNIKA
![](https://i.imgur.com/2f4RGbb.png)


## Zadanie 7
![](https://i.imgur.com/WwM1oY3.png)

![](https://i.imgur.com/nbi7ceE.png)

![](https://i.imgur.com/UqBJQWZ.png)


```c=
#include "csapp.h"
#include "terminal.h"

#undef MAXLINE
#define MAXLINE 120

static sigjmp_buf env;

static void signal_handler(int signo) {
  /* TODO: Something is missing here! */
  // Skaczemu do readnum, któ©y następnie się kończy
  siglongjmp(env,signo);
  // longjmp(env,signo);
  /* TODO: END*/
}

/* If interrupted by signal, returns signal number. Otherwise converts user
 * provided string to number and saves it under num_p and returns zero. */
static int readnum(int *num_p) {
  char line[MAXLINE];
  int n;

  /* TODO: Something is missing here! Use Read() to get line from user. */
  /*
  Ustawiamy miejsce skoku funkcji tak, aby zapamiętała maskę sygnałów,
  Ponieważ wchodzimy do funkcji prosto z handlera, 
  Zatem nigdy się on nie zakończy czyli nie wyczyści po sobie maski
  */
  n=sigsetjmp(env,1); 
  // n=setjmp(env); 
  // Zgodnie z treścią po otrzymaniu sygnału natyczmiast przerywamy działanie 
  if(n)
  	return n;
  // Ustalamy aby sygnał SIGALRM został wysłany po sekundzie
  alarm(1);
  // Wczytujemy odpowiedź
  Read(STDIN_FILENO,line,MAXLINE);
  /* TODO: END*/
  *num_p = atoi(line);
  return 0;
}

static void game(void) {
  int tty = tty_open();

  int timeout = 0, num1 = 0, num2 = 0, sum;
  int last_sig = 0;
  int lives = 3;
  int score = 0;

  while (lives > 0) {
    switch (last_sig) {
      case 0:
        timeout = 5;
        num1 = random() % 100;
        num2 = random() % 100;
        printf("What is the sum of %d and %d?\n", num1, num2);
        break;

      case SIGINT:
        printf(CHA(1) EL() "Bye bye!\n");
        exit(EXIT_FAILURE);

      case SIGALRM:
        timeout--;
        if (timeout < 0) {
          last_sig = 0;
          lives--;
          printf(CHA(1) EL() "Answer was %d!\n", num1 + num2);
          continue;
        }
        break;

      default:
        app_error("lastsig = %d not handled!\n", last_sig);
        break;
    }

    /* Rewrite user prompt to show current number of lives and timeout. */
    sigset_t set, oldset;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGALRM);
    Sigprocmask(SIG_BLOCK, &set, &oldset);

    int x, y;
    tty_curpos(tty, &x, &y);
    dprintf(STDOUT_FILENO, CHA(1) "lives: %d timeout: %d > ", lives, timeout);
    if (last_sig == SIGALRM)
      dprintf(STDOUT_FILENO, CHA(%d), y);

    Sigprocmask(SIG_SETMASK, &oldset, NULL);

    /* Read a number from user. */
    last_sig = readnum(&sum);
    if (last_sig)
      continue;

    /* Line contains user input (a number) terminated with '\0'. */
    if (sum == num1 + num2) {
      printf("Correct!\n");
      score++;
    } else {
      printf("Incorrect!\n");
      lives--;
    }
  }

  Close(tty);

  printf("Game over! Your score is %d.\n", score);
}

int main(void) {
  /* Initialize PRNG seed. */
  struct timeval tv;
  gettimeofday(&tv, NULL);
  srandom(tv.tv_usec);

  /* SIGALRM is used for timeouts, SIGINT for graceful exit. */
  Signal(SIGALRM, signal_handler);
  Signal(SIGINT, signal_handler);

  game();

  return EXIT_SUCCESS;
}
```

## Zadanie 8
![](https://i.imgur.com/gTMVcON.png)

```c=
#include "queue.h"
#include "csapp.h"

#define CORO_STKSIZE 4096
#define CORO_STKALIGN 16 /* As required by SysV ABI ! */

#ifndef EOF
#define EOF (-1)
#endif

#ifndef NOTHING
#define NOTHING (-2)
#endif

typedef struct coro {
  TAILQ_ENTRY(coro) co_link;
  const char *co_name;
  void *co_stack;
  Jmpbuf co_ctx;
} coro_t;

static TAILQ_HEAD(, coro) runqueue = TAILQ_HEAD_INITIALIZER(runqueue);
static coro_t *running;
static Jmpbuf dispatcher;

/* Initialize coroutine stucture with stack. */
static void coro_init(coro_t *co, const char *name) {
  memset(co, 0, sizeof(coro_t));
  co->co_name = name;
  /* Allocates a fresh stack for the coroutine! */
  if (posix_memalign(&co->co_stack, CORO_STKALIGN, CORO_STKSIZE) < 0)
    unix_error("posix_memalign error");
}

/* Detach a stack from coroutine structure. */
static void coro_destroy(coro_t *co) {
  free(co->co_stack);
}

/*
 * Switch between subsequent coroutines.
 *
 * Dead coroutines, i.e. ones that returned EOF, get removed from the run queue.
 * Feed next coroutine (value returned from coro_yield) with the result from
 * previous one (parameter passed to coro_yield).
 * Return to dispatcher if there're no more coroutines to run.
 */
static noreturn void coro_switch(int v) {
  coro_t *curr = running;
  /* TODO: Use description above to implement the body. */
  // Jeśli EOF usuwamy współprogram z kolejki
  if (v == EOF) { 
    TAILQ_REMOVE(&runqueue, curr, co_link);
    // Jeśli opróżniliśmy kolejkę wracamy do nadzorcy
    if (TAILQ_EMPTY(&runqueue))
      Longjmp(dispatcher, NOTHING);
  }
  // Wybieramy następny współprogram
  running = TAILQ_NEXT(curr, co_link);
  // Jeśli nie ma ostatniego elementu to wracamy do pierwszego współprogramu
  if (running == NULL) 
    running = TAILQ_FIRST(&runqueue);
  // Skaczemy do następnego współprogramu
  Longjmp(running->co_ctx, v);
  /* TODO: END*/
}

/* Save caller context and switch back to next coroutine. */
static int coro_yield(int v) {
  int nv = Setjmp(running->co_ctx);
  if (nv == 0)
    coro_switch(v);
  return nv;
}

/* Configure coroutine context to be executed. */
static void coro_add(coro_t *co, void (*fn)(int)) {
  int v = Setjmp(co->co_ctx);
  if (v) {
    /* This will get executed when coroutine is entered first time. */
    fn(v);
    /* Coroutine must pass EOF to be removed from runqueue! */
    coro_switch(EOF);
  }
  /* Coroutine will be running on its private stack! */
  co->co_ctx->rsp = co->co_stack + CORO_STKSIZE;
  TAILQ_INSERT_TAIL(&runqueue, co, co_link);
}

/* Take first coroutine and feed it with passed value. */
static int coro_run(int v) {
  running = TAILQ_FIRST(&runqueue);
  int nv = Setjmp(dispatcher);
  if (nv == 0)
    Longjmp(running->co_ctx, v);
  return nv;
}

/*
 * Actual coroutines that perform some useful work.
 */

static void func_1(int _) {
  int words = 0;
  char prev_ch = ' ';
  char ch;

  while (Read(0, &ch, 1) > 0) {
    if (isspace(ch)) {
      if (isspace(prev_ch))
        continue;
      words++;
    }
    coro_yield(ch);
    prev_ch = ch;
  }

  if (!isspace(ch))
    words++;

  dprintf(STDERR_FILENO, "\nfunc_1: words = %d\n", words);
}

static void func_2(int ch) {
  int removed = 0;

  while (ch != EOF) {
    if (!isalpha(ch)) {
      removed++;
      ch = NOTHING;
    }
    ch = coro_yield(ch);
  }
 
  dprintf(STDERR_FILENO, "func_2: removed = %d\n", removed);
}

static void func_3(int ch) {
  int printed = 0;

  while (ch != EOF) {
    if (ch != NOTHING) {
      printed++;
      if (islower(ch))
        ch = toupper(ch);
      else if (isupper(ch))
        ch = tolower(ch);
      Write(STDOUT_FILENO, &ch, 1);
    }
    ch = coro_yield(NOTHING);
  }

  dprintf(STDERR_FILENO, "func_3: printed = %d\n", printed);
}

int main(void) {
  coro_t co[3];

  coro_init(&co[0], "func_1");
  coro_init(&co[1], "func_2");
  coro_init(&co[2], "func_3");
  coro_add(&co[0], func_1);
  coro_add(&co[1], func_2);
  coro_add(&co[2], func_3);
  coro_run(NOTHING);
  coro_destroy(&co[0]);
  coro_destroy(&co[1]);
  coro_destroy(&co[2]);

  dprintf(STDERR_FILENO, "Bye, bye!\n");

  return EXIT_SUCCESS;
}
```