Linux, sygnały i spowolnienie procesu

Linux, sygnały i spowolnienie procesu

Przygotowywałem w ciągu ostatnich dni komputer do zainstalowania Win7 i zmiany układu partycji na dysku. Ot backup ważniejszych danych, wgranie muzyki do końca na telefon i tym podobne czynności. Backup danych windowsowych ograniczył się do pogodzenia z ich stratą – zbyt wiele tam jest śmiecia, żeby chciało mi się zgadywać które fragmenty są ważne, a które nie. Cenne dane trzymam na partycji linuksowej, dzięki trickowy z colinux który opisałem w poprzedniej notce.

O możliwość wygodnego importu ustawień użytkownika zgranych do spakowanej paczki, systemów z Redmond nie posądzam (chociaż podejrzewam, że jest narzędzie do tego).
Pod Linuksem mogę napisać: tar --lzma -cf /media/c/home.tar.lzma /home* i po iluś godzinach mieć pełny backup. Wszystko byłoby fajnie gdyby nie dwa szczegóły: musiałem w tym czasie popracować na wirtualnej maszynie, a lzma zarzyna procesor razem z ntfs-3g. To piekielnie obciąża komputer.

Google podpowiedziało obniżenie priorytetu procesów dla cpu i io, jednak nie o to chodzi. Nawet jeśli oba procesy będą chętnie pozwalać innym procesom na pracę, to laptop będzie wył wiatrakiem, cache będzie się często wymieniał, responsywność całości spadnie. Nie o to chodzi. Chcę aby te 2 procesy pracowały z mniejszym obciążeniem komputera, a nie niższym priorytetem.

Z pomocą przyszedł prosty skrypt znaleziony w ^g. Niestety nie zapisałem sobie skąd jest ten skrypt, więc nie dam creditsa. Skrypt prezentuje się tak:

#!/bin/sh
while true; do
  kill -s STOP $1
  sleep 0.2
  kill -s CONT $1
  sleep 0.5
done

Jego zadaniem jest naprzemienne wysyłanie sygnałów SIGSTOP i SIGCONT do procesów wskazanych przez wybrany PID. Tak jakbym miał rozwiązanie! No, może nie do końca.

Ten skrypt co prawda skutecznie zbija zbyt wysokie zużycie procesora i dysku, jednak na raz obsłuży tylko jeden proces. Dodatkowo, jeśli dam ^C to nie wiem, w którym momencie zostanie przerwane przetwarzanie pętli i może być tak, że lzma dostanie SIGCONT, a ntfs-3g SIGSTOP. Nie wiem, co się wtedy stanie, ale chyba wolę nie wiedzieć.

Drugi problem to fakt, że potrzebuję tyle razy odpalić ten skrypt, ile PIDów chcę spowolnić. Dla 2 procesów to jeszcze ujdzie, ale wygodniej byłoby odpalić jeden skrypt dla wielu PIDów, tak jak się wykonuje kill `pidof httpd` prawda?

Być może da się rozwiązać oba problemy stosując skrypty powłoki, jednak nie czuję się w tym na tyle pewnie, by się tego zadania podjąć. Zamiast tego poszukałem, jak wygląda obsługa sygnałów w C/C++. Z pomocą przyszedł artykuł na cc.byexamples.com dotyczący sygnałów. Nieocenioną pomoc stanowił też manual systemowy. Od ponad pół roku nie używałem C i składnię niektórych poleceń mogłem zapomnieć 🙂

Wykorzystując szkielet programu z linku powyżej napisałem taki program:

edit:
Korzystając z okazji chciałbym pozdrowić wszystkich studentów WATu którzy tu mogą przypadkiem trafić szukając materiałów na Systemy Operacyjne! 😀
#include<stdio.h>
#include<stdlib.h>  
#include<signal.h>  
#include<string.h>  
#include<unistd.h>  
  
struct sigaction osa;  
int *pids;
int pidCount;

void help();
void goSleep();
void wake();

void bypass_sigint(int sig_no){  
    printf("attempt to wake sleeping processes up\n");
    wake();
    printf("done... exiting!\n\n");
    
    if(pids) free(pids);
    sigaction(SIGINT,&osa,NULL);  
    kill(0,SIGINT);  
}  


  
int main(int argc, char** argv)  
{ 
  if(argc<2) {help(argv[0]); return (0);}
  
  int i;
  int work=10;
  int nap=10;
  pidCount=0;
  
  pids=(int*) malloc(sizeof(int) );
  pids[0]= -1;
  
  
  
  for(i=1; i<argc; i++){
   if(argv[i][0]=='-'){
     //opcja znaleziona
     if( (strlen(argv[i])==2 && argv[i][1]=='w') ||
         (strlen(argv[i])==6 && 0==strcmp(argv[i],"--work") )
       ){
           i++;
           work=atoi(argv[i] ); 
         }
     if( (strlen(argv[i])==2 && argv[i][1]=='n') ||
         (strlen(argv[i])==5 && 0==strcmp(argv[i],"--nap")  )
       ){
           i++;
           nap=atoi(argv[i] );
         }
     
   }else if(argv[i][0]>='0' && argv[i][0]<='9'){
    //mamy PID
      
      pidCount++;
      pids=realloc(pids, pidCount*sizeof(int));
      pids[pidCount-1]=atoi(argv[i]) ;
      
   }
  }
  
  
  printf("pidCount: %d\n",pidCount);
  if(!pidCount) return 0;
    
  printf("Working on pids: \n");
  for(i=0;i<pidCount;i++){
      printf("%d: %d\n", i+1, pids[i]);
  }
  
  
    struct sigaction sa,osa;  
    memset(&sa, 0, sizeof(sa));  
    sa.sa_handler = &bypass_sigint;  
    sigaction(SIGINT, &sa,&osa);  
  
     
    while(1){
                usleep(work);
                goSleep();
                //printf("stopped\n");
                usleep(nap);
                wake();
                //printf("working\n");
    }  
    free(pids);
    return 0;  
} 

void help(char*programName){
    printf("%s [-n usec] [-w usec] pid1 [pid2 [pid3 [pid4]]]\n", programName);
    printf("Slows down processes by sending them STOP and CONT signals\n");
    printf("--nap usec\n");
    printf("-n usec  \t  sleep for usec microseconds, default 10\n");
    printf("--work usec\n");
    printf("-w usec  \t  run for usec microseconds, default 10 \n\n");
}


void goSleep(){
  int i;
 if(pidCount)
  for(i=0;i<pidCount;i++){
      kill(pids[i], SIGSTOP);
  }
}

void wake(){
  int i;
 if(pidCount)
  for(i=0;i<pidCount;i++){
      kill(pids[i], SIGCONT);
  }
}


Słowem wyjaśnienia, co się dzieje:

Sercem programu jest pętla i 2 funkcje: wake, goSleep. Zadaniem tych 2 funkcji jest odpowiednio uśpienie i polecenie kontynuowania pracy przez wysłanie sygnałów SIGSTOP i SIGCONT procesom. Do wysyłania sygnału służy polecenie kill. Pętla natomiast usypia programy, czeka określony czas, wybudza, czeka i tak w kółko. Dla wygody dałem możliwość ustawienia długości spania i czekania, bo czasem wpisane na sztywno parametry mogą być niewygodne. Przedział czasu na spanie i pracę jest podany w mikrosekundach, dla polecenia usleep. Bałem się, że czas w sekundach zaowocuje wyczuwalną czkawką systemu.

Funkcja bypass_sigint zajmuje się obsługą sygnału INT, który jest wysyłany przez wciśnięcie ^C. W tej funkcji następuje przywrócenie procesów do życia, wyświetlenie komunikatów, zwolnienie tablicy z zapisanymi PIDami i na końcu wyjście. Sygnał SIGCONT wysłany do procesu który już pracuje jest ignorowany, więc nie muszę tutaj specjalnie rozpatrywać tej sytuacji.

Domyślam się, że niektóre elementy progamu można poprawić, zadbać o obsługę sygnałów przerwania pracy, czy błędów. Można też zmodyfikować program, aby za jednym wywołaniem, różne PIDy otrzymały różne interwały pracy i przestoju, jednak gdy to pisałem, nie potrzebowałem tych funkcjonalności.

Program podczas swojej pracy żre 3-4% procesora na mojej maszynie. Myślę, że da się ten wynik obniżyć, jednak nadal jest to dużo lepszy wynik, niż 100% zajęte przez lzma i ntfs-3g 🙂

Program jest na licencjach GPLv3 i beerware, czyli jeśli ktoś chce go wykorzystywać do celów komercyjnych, musi mi kupić piwo. 😀 Ewentualnie swój program rozpowszechnić na GPLv3.

Ps. Jak na joggerze wygodnie publikować źródła?

Bibliografia:
http://cc.byexamples.com/20070520/tap-the-interrupt-signal/
http://www.digipedia.pl/man/signal.2.html
http://linux.die.net/man/2/signal
http://linux.die.net/man/3/kill
I o tym samym, ale pod windows:
Wstrzymywanie procesów pod Windows, j00ru.vexillium.org
Opis wykorzystania cpulimit jako daemona do kontrolowania procesów, by Grzglo.jogger.pl

Edit: Joggerowicz Szymon wskazał program Cpulimit, który realizuje dokładnie to, czego potrzebowałem przed napisaniem mojego programu. Cpulimit jest o tyle lepszy, że ograniczenie podaje się w nim w postaci procentowego obciążenia procesora, którego proces nie może przekroczyć.
Podlinkowałem wpis Grzegorza dotyczący cpulimit.