PHP issues using flock - file locking

2019-08-02 09:04发布

问题:

I'm having a problem using the PHP flock() function. I need to write two different variables ($O and $o) but often it doesn't write the second variable ($o), maybe because the file is written two times in a row.

Here's the code:

include_once "changevar.php";
changevar("O",$seguimedia,$filename,0);
changevar("o",$offerta,$filename,0);

$seguimedia, $filename and $offerta are correctly set.

changevar.php:

function changevar($varname,$newval,$filename,$type)
{
    while(!$fp=fopen($filename,"c+"))
    {
        usleep(100000);
    }
    while(!flock($fp,LOCK_EX))
    {
        usleep(100000);
    }
    while(!include($filename))
    {
        usleep(100000);
    }
    ftruncate($fp,0);
    rewind($fp);
    $$varname=$newval;
    if($type==0)
    {
        foreach(array("u","p","t","d") as $v){$$v=str_replace("\\","\\\\",$$v);}
        $text="<?\$o=$o;\$u=\"$u\";\$c=$c;\$m=$m;\$p=\"$p\";\$C=$C;\$id=\"$id\";\$t=\"$t\";\$d=\"$d\";\$O=$O;?>";
    }
    else
    {
        $text="<?\$impressions=$impressions;\$clickunici=$clickunici;\$clicknulli=$clicknulli;\$creditiguadagnati=$creditiguadagnati;\$creditiacquistati=$creditiacquistati;\$creditiutilizzati=$creditiutilizzati;?>";
    }
    fwrite($fp,$text);
    flock($fp,LOCK_UN);
    fclose($fp);
}

Is PHP flock() a good way to avoid this kind of problems?
Which language/functions do I need to use?

回答1:

The problem is actually in the fopen() call.

You're opening the file in mode c+. This means that the file pointer is located at the beginning of the file, which would cause any write to overwrite what's already there. To add insult to injury, you're calling ftruncate(), truncating the file to 0 bytes right before writing -- therefore, before every write, you're manually erasing the whole file. That code is, thus, guaranteeing that only the last write to the file will remain, manually taking care of erasing every single other one.

The fopen() call should be made with mode a+, and both ftruncate() and rewind() need to go (the latter also having the effect of putting the file pointer at the beginning).



回答2:

It does not matter if you writing two times from within the same script. But in fact it does matter, when you try this from two different processes AND use file locking...

Anyway, your changevar() function actually truncates the file every time, so I guess thats why it "seems" that only one var has been written.



回答3:

Honestly , I really think is a very very very bad Idea reading and writing a PHP file. If you are looking at configurations then use ini or json.

If you really want to read and write to file then it can be as simple as :

$file = __DIR__ . "/include/config.json";
$var = new FileVar($file);
$var['o'] = "Small o";
$var['O'] = "Big O";
$var->name = "Simple json";

echo file_get_contents($file);

Output

{
    "o": "Small o",
    "O": "Big O",
    "name": "Simple json"
}

Another Example

// To remove
unset($var['O']);

// to update
$var['o'] = "Smaller o";

Output

{
    "o": "Smaller o",
    "name": "Simple json"
}

Please note that the include folder contains this .htaccess

<Files "*">
    Order Deny,Allow
    Deny from all
</Files>

To test if truly this lock is working i used pthreads to emulate race condition

for($i = 0; $i < 100; $i ++) {

    $ts = array();

    // Generate Thread
    foreach(range("A", "E") as $letter) {
        $ts[] = new T($file, $letter);
    }

    // Write all files at the same time
    foreach($ts as $t) {
        $t->start();
    }

    // Wait for all files to finish
    foreach($ts as $t) {
        $t->join();
    }
}

// What do we have
echo file_get_contents($file);

Main Class

class FileVar implements ArrayAccess {
    private $file;
    private $data;
    private $timeout = 5;

    function __construct($file) {
        touch($file);
        $this->file = $file;
        $this->data = json_decode(file_get_contents($file), true);
    }

    public function __get($offset) {
        return $this->offsetGet($offset);
    }

    public function __set($offset, $value) {
        $this->offsetSet($offset, $value);
    }

    public function offsetSet($offset, $value) {
        if (is_null($offset)) {
            $this->data[] = $value;
        } else {
            $this->data[$offset] = $value;
        }
        $this->update();
    }

    public function offsetExists($offset) {
        return isset($this->data[$offset]);
    }

    public function offsetUnset($offset) {
        unset($this->data[$offset]);
        $this->update();
    }

    public function offsetGet($offset) {
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
    }

    private function update() {
        // Open file with locking
        $time = time();
        while(! $fp = fopen($this->file, "c+")) {
            if (time() - $time > $this->timeout)
                throw new Exception("File can not be accessed");
            usleep(100000);
        }

        // Lock the file for writing
        flock($fp, LOCK_EX);

        // Overwrite the old data
        ftruncate($fp, 0);
        rewind($fp);

        // Write the new array to file
        fwrite($fp, json_encode($this->data, 128));

        // Unlock the file
        flock($fp, LOCK_UN);

        // Close the file
        fclose($fp);
    }
}

Testing Class

class T extends Thread {

    function __construct($file, $name) {
        $this->file = $file;
        $this->name = $name;
    }

    function run() {
        $var = new FileVar($this->file);
        $var[$this->name] = sprintf("Letter  %s", $this->name);
    }
}