📜 ⬆️ ⬇️

File transfer by signals

Good afternoon, habrazhiteli. Surely everyone knows what signals are in Linux and why they are needed. But today, I would like to talk about, as it seems to me, their unconventional application.

The task is very contrived and aimed at training their skills in working with signals and, a little bit, bitwise operations. In general, the task:
The program should generate a process that, through only signals, transmitted to the parent the file specified as a command line argument. The parent prints the resulting file to stdout.


The solution is very simple: we will use signals as Morse code, only we will use SIGUSR1 and SIGUSR2 instead of “Dot” and “Dash”.
')

Decision


We will transfer the file bit by bit using SIGUSR2 and SIGUSR1 signals.
Let SIGUSR2 be a bit equal to zero, SIGUSR1 be a bit equal to one.

Sending data

Read byte from file to variable c .
Create a counter variable equal to 0b10000000 or 128.
If c AND (meaning “bitwise and”) counter is equal to one, then the most significant bit is equal to one, we send SIGUSR1, otherwise we send SIGUSR2.
We divide the counter in half (we get 0b01000000 or 64), that is, go to the second bit to the left.
Repeat until counter reaches zero. Then we read the new byte from the file.

In C, it looks like this:
while (read(fd, &c, 1) > 0) { for ( i = 128; i >= 1; i /= 2) { if ( i & c ) // 1 kill(ppid, SIGUSR1); else // 0 kill(ppid, SIGUSR2); } 

Receiving data

We will accept in the variable out_char , initially equal to zero.
While counter is not zero, we process the signals as follows:
If SIGUSR1 arrived, then out_char + = counter , then counter / = 2 .
If SIGUSR1 came, then counter / = 2 .

Write handlers for signals:
 // SIGUSR1 void one(int signo) { out_char += counter; counter /= 2; } // SIGUSR2 void zero(int signo) { counter/=2; } 

Working version


Now we need to consider the cases of unforeseen death of a parent or child. If the child dies, then everything is simple - the parent will receive a SIGCHLD .
 // SIGCHLD void childexit(int signo) { exit(EXIT_SUCCESS); } 

It will be a bit more difficult with the child, there is no guarantee that the child will be notified of the death of his parent. Therefore, we will ask the core to send us SIGALRM if no other signals are sent to us after a specified period of time. Add this to the sending loop:
 while (read(fd, &c, 1) > 0) { // SIGALRM          alarm(1); //   for ( i = 128; i >= 1; i /= 2) { if ( i & c ) // 1 kill(ppid, SIGUSR1); else // 0 kill(ppid, SIGUSR2); } } 

Add a mechanism to confirm receipt of a signal from the child by the parent. That is, until the parent confirms the receipt of the bit, the child will not pass the next one.

This is done simply, in the one and zero functions, you must add a response sending. We will respond with a signal SIGUSR1 . After the changes, the functions will be as follows:
 // SIGUSR1 void one(int signo) { out_char += counter; counter /= 2; kill(pid, SIGUSR1); } // SIGUSR2 void zero(int signo) { counter/=2; kill(pid, SIGUSR1); } 

And to wait for the child to confirm, add sigsuspend (& set) :
 while (read(fd, &c, 1) > 0){ // SIGALRM          alarm(1); //   for ( i = 128; i >= 1; i /= 2){ if ( i & c ) // 1 kill(ppid, SIGUSR1); else // 0 kill(ppid, SIGUSR2); //      //     sigsuspend(&set); } 

The function performed upon the arrival of the confirmation signal from the parent is empty, but it should be, otherwise the action for the default signal, Exit, will be performed.

Actually function and set :
 // Nothing void empty(int signo) { } // SET sigemptyset(&set); //    // SIGUSR1 - empty() struct sigaction act_empty; memset(&act_empty, 0, sizeof(act_empty)); act_empty.sa_handler = empty; sigfillset(&act_empty.sa_mask); sigaction(SIGUSR1, &act_empty, NULL); // SIGALRM - parentexit() struct sigaction act_alarm; memset(&act_alarm, 0, sizeof(act_alarm)); act_alarm.sa_handler = parentexit; sigfillset(&act_alarm.sa_mask); sigaction(SIGALRM, &act_alarm, NULL); 

In the parent, the signal mask should be as follows:
 // SIGCHLD - exit struct sigaction act_exit; memset(&act_exit, 0, sizeof(act_exit)); act_exit.sa_handler = childexit; sigfillset(&act_exit.sa_mask); sigaction(SIGCHLD, &act_exit, NULL); // SIGUSR1 - one() struct sigaction act_one; memset(&act_one, 0, sizeof(act_one)); act_one.sa_handler = one; sigfillset(&act_one.sa_mask); sigaction(SIGUSR1, &act_one, NULL); // SIGUSR2 - zero() struct sigaction act_zero; memset(&act_zero, 0, sizeof(act_zero)); act_zero.sa_handler = zero; sigfillset(&act_zero.sa_mask); sigaction(SIGUSR2, &act_zero, NULL); //   sigaddset(&set, SIGUSR1); sigaddset(&set, SIGUSR2); sigaddset(&set, SIGCHLD); sigprocmask(SIG_BLOCK, &set, NULL ); 

Since in the process of the program's operation, a new process is spawned, which immediately starts sending us signals (data), sigprocmask (SIG_BLOCK, & set, NULL) must be done before the fork, otherwise there is a chance to get an error due to the race effect (race condition).
As a result, the program will look like this:
 int out_char = 0, counter = 128; pid_t pid; //      // SIGCHLD void childexit(int signo) { exit(EXIT_SUCCESS); } // SIGALRM void parentexit(int signo) { exit(EXIT_SUCCESS); } // Nothing void empty(int signo) { } // SIGUSR1 void one(int signo) { out_char += counter; counter /= 2; kill(pid, SIGUSR1); } // SIGUSR2 void zero(int signo) { counter/=2; kill(pid, SIGUSR1); } int main(int argc, char ** argv){ if (argc != 2) { fprintf(stderr, "Use: %s [source]\n", argv[0]); exit(EXIT_FAILURE); } pid_t ppid = getpid(); //   ,    sigset_t set; //     //  SIGCHLD -  struct sigaction act_exit; memset(&act_exit, 0, sizeof(act_exit)); act_exit.sa_handler = childexit; sigfillset(&act_exit.sa_mask); sigaction(SIGCHLD, &act_exit, NULL); // SIGUSR1 - one() struct sigaction act_one; memset(&act_one, 0, sizeof(act_one)); act_one.sa_handler = one; sigfillset(&act_one.sa_mask); sigaction(SIGUSR1, &act_one, NULL); // SIGUSR2 - zero() struct sigaction act_zero; memset(&act_zero, 0, sizeof(act_zero)); act_zero.sa_handler = zero; sigfillset(&act_zero.sa_mask); sigaction(SIGUSR2, &act_zero, NULL); //sigemptyset(&set); //   sigaddset(&set, SIGUSR1); sigaddset(&set, SIGUSR2); sigaddset(&set, SIGCHLD); sigprocmask(SIG_BLOCK, &set, NULL ); sigemptyset(&set); //  pid = fork(); //  () if (pid == 0) { unsigned int fd = 0; char c = 0; sigemptyset(&set); //    // SIGUSR1 - empty() struct sigaction act_empty; memset(&act_empty, 0, sizeof(act_empty)); act_empty.sa_handler = empty; sigfillset(&act_empty.sa_mask); sigaction(SIGUSR1, &act_empty, NULL); // SIGALRM - parentexit() struct sigaction act_alarm; memset(&act_alarm, 0, sizeof(act_alarm)); act_alarm.sa_handler = parentexit; sigfillset(&act_alarm.sa_mask); sigaction(SIGALRM, &act_alarm, NULL); if ((fd = open(argv[1], O_RDONLY)) < 0 ){ perror("Can't open file"); exit(EXIT_FAILURE); } int i; while (read(fd, &c, 1) > 0){ // SIGALRM          alarm(1); //   for ( i = 128; i >= 1; i /= 2){ if ( i & c ) // 1 kill(ppid, SIGUSR1); else // 0 kill(ppid, SIGUSR2); //     //      sigsuspend(&set); } } //   exit(EXIT_SUCCESS); } errno = 0; //      do { if(counter == 0){ // Whole byte write(STDOUT_FILENO, &out_char, 1); // fflush(stdout); counter=128; out_char = 0; } sigsuspend(&set); //     } while (1); exit(EXIT_SUCCESS); } 

The source code of the program can be downloaded here .

This is how you can transfer files from one program to another, using only signals. True, this approach is not very effective and I do not think that it has a use in real projects.

Thanks for attention!

Source: https://habr.com/ru/post/122823/


All Articles