Nesprávně kombinujete funkce IO s vyrovnávací pamětí a bez vyrovnávací paměti. Taková kombinace musí být provedena velmi opatrně, zvláště když kód musí být přenosný. (a je špatné psát nepřenosný kód...)
Určitě je nejlepší vyhnout se kombinování IO s vyrovnávací pamětí a bez vyrovnávací paměti na stejném deskriptoru souboru.
Vstup do vyrovnávací paměti: fprintf()
, fopen()
, fclose()
, freopen()
...
Vstup bez vyrovnávací paměti: write()
, open()
, close()
, dup()
...
Když použijete dup2()
k přesměrování stdout. Funkce si není vědoma vyrovnávací paměti, která byla naplněna fprintf()
. Takže když dup2()
zavře starý deskriptor 1, nevyprázdní vyrovnávací paměť a obsah by mohl být vyprázdněn na jiný výstup. Ve vašem případě 2a byl odeslán na /dev/null
.
Řešení
Ve vašem případě je nejlepší použít freopen()
místo dup2()
. To vyřeší všechny vaše problémy:
- Vyprázdní vyrovnávací paměti původního
FILE
proud. (případ 2a) - Nastaví režim ukládání do vyrovnávací paměti podle nově otevřeného souboru. (případ 3)
Zde je správná implementace vaší funkce:
void RedirectStdout2File(const char* log_path) {
if(freopen(log_path, "a+", stdout) == NULL) err(EXIT_FAILURE, NULL);
}
Bohužel s bufferovaným IO nemůžete přímo nastavit oprávnění nově vytvořeného souboru. Ke změně oprávnění musíte použít jiná volání nebo můžete použít nepřenosná rozšíření glibc. Viz fopen() man page
.
Proplachování pro stdout
je určeno jeho chováním ve vyrovnávací paměti. Ukládání do vyrovnávací paměti lze nastavit na tři režimy:_IOFBF
(plné ukládání do vyrovnávací paměti:čeká na fflush()
pokud je to možné), _IOLBF
(ukládání do vyrovnávací paměti řádku:nový řádek spouští automatické vyprázdnění) a _IONBF
(vždy se používá přímý zápis). "Podpora těchto charakteristik je definována implementací a může být ovlivněna prostřednictvím setbuf()
a setvbuf()
funkce." [C99:7.19.3.3]
"Při spuštění programu jsou předdefinovány tři textové proudy a není třeba je explicitně otevírat – standardní vstup (pro čtení konvenčního vstupu), standardní výstup (pro zápis konvenčního výstupu) a standardní chyba (pro zápis diagnostického výstupu). Jak bylo původně otevřeno, standardní chyba stream není plně ukládán do vyrovnávací paměti; standardní vstupní a standardní výstupní toky jsou plně ukládány do vyrovnávací paměti pouze tehdy, pokud lze určit, že se tok neodkazuje na interaktivní zařízení." [C99:7.19.3.7]
Vysvětlení pozorovaného chování
Takže se stane, že implementace udělá něco pro platformu, aby rozhodla, zda stdout
bude ukládán do vyrovnávací paměti. Ve většině implementací libc se tento test provádí při prvním použití streamu.
- Chování č. 1 lze snadno vysvětlit:když je stream určen pro interaktivní zařízení, je ukládán do vyrovnávací paměti a
printf()
se automaticky vyplachuje. - Nyní se očekává i případ č. 2:když přesměrováváme na soubor, stream je plně ukládán do vyrovnávací paměti a nebude vyprázdněn kromě
fflush()
, pokud do něj nezapíšete spousty dat. - Konečně rozumíme i případu č. 3 pro implementace, které provádějí kontrolu základního fd pouze jednou. Protože jsme vynutili inicializaci vyrovnávací paměti stdout v prvním
printf()
, stdout získal režim s vyrovnávací pamětí řádků. Když vyměníme fd, aby šel do souboru, je stále ukládán do vyrovnávací paměti, takže se data automaticky vyprázdní.
Některé skutečné implementace
Každá knihovna libc má volnost v tom, jak interpretuje tyto požadavky, protože C99 nespecifikuje, co je "interaktivní zařízení", ani to nerozšiřuje položka stdio POSIX (kromě požadavku, aby byl stderr otevřen pro čtení).
-
Glibc. Viz filedoalloc.c:L111. Zde používáme
stat()
otestujte, zda je fd tty, a podle toho nastavte režim ukládání do vyrovnávací paměti. (To se volá z fileops.c.)stdout
zpočátku má nulovou vyrovnávací paměť a je alokována při prvním použití proudu na základě charakteristik fd 1. -
BSD libc. Velmi podobný, ale mnohem čistší kód, který je třeba dodržovat! Viz tento řádek na makebuf.c