سؤال

يبدو هذا سؤالًا بسيطًا، لكنني لم أجد شيئًا مشابهًا هنا.

نظرًا لعدم وجود وظيفة نسخ الملفات في لغة C، يتعين علينا تنفيذ نسخ الملفات بأنفسنا، لكنني لا أحب إعادة اختراع العجلة حتى بالنسبة للأشياء التافهة مثل هذه، لذلك أود أن أسأل السحابة:

  1. ما الكود الذي توصي به لنسخ الملفات باستخدام fopen()/fread()/fwrite()؟
    • ما الكود الذي توصي به لنسخ الملفات باستخدام open()/read()/write()؟

يجب أن يكون هذا الرمز محمولاً (windows/mac/linux/bsd/qnx/younameit)، ومستقرًا، وتم اختباره عبر الزمن، وسريعًا، وفعالًا في الذاكرة، وما إلى ذلك.نرحب بالدخول إلى الأجزاء الداخلية لنظام معين للحصول على مزيد من الأداء (مثل الحصول على حجم مجموعة نظام الملفات).

يبدو هذا سؤالًا تافهًا، ولكن، على سبيل المثال، الكود المصدري لأمر CP ليس 10 أسطر من كود C.

هل كانت مفيدة؟

المحلول

بقدر ما يتعلق الأمر بالإدخال/الإخراج الفعلي، فإن الكود الذي كتبته مليون مرة بأشكال مختلفة لنسخ البيانات من تدفق إلى آخر يسير على هذا النحو.تُرجع 0 عند النجاح، أو -1 مع تعيين الخطأ errno على الخطأ (وفي هذه الحالة قد يكون تم نسخ أي عدد من البايتات).

لاحظ أنه لنسخ الملفات العادية، يمكنك تخطي عناصر EAGAIN، نظرًا لأن الملفات العادية تمنع الإدخال/الإخراج دائمًا.ولكن حتمًا إذا كتبت هذا الرمز، فسيستخدمه شخص ما في أنواع أخرى من واصفات الملفات، لذا اعتبره هدية مجانية.

هناك تحسين خاص بالملف وهو GNU cp يفعل ذلك، وهو ما لم أزعجه هنا، وهو أنه بالنسبة للكتل الطويلة التي يبلغ حجمها 0 بايت بدلاً من الكتابة، ما عليك سوى توسيع ملف الإخراج عن طريق البحث عن النهاية.

void block(int fd, int event) {
    pollfd topoll;
    topoll.fd = fd;
    topoll.events = event;
    poll(&topoll, 1, -1);
    // no need to check errors - if the stream is bust then the
    // next read/write will tell us
}

int copy_data_buffer(int fdin, int fdout, void *buf, size_t bufsize) {
    for(;;) {
       void *pos;
       // read data to buffer
       ssize_t bytestowrite = read(fdin, buf, bufsize);
       if (bytestowrite == 0) break; // end of input
       if (bytestowrite == -1) {
           if (errno == EINTR) continue; // signal handled
           if (errno == EAGAIN) {
               block(fdin, POLLIN);
               continue;
           }
           return -1; // error
       }

       // write data from buffer
       pos = buf;
       while (bytestowrite > 0) {
           ssize_t bytes_written = write(fdout, pos, bytestowrite);
           if (bytes_written == -1) {
               if (errno == EINTR) continue; // signal handled
               if (errno == EAGAIN) {
                   block(fdout, POLLOUT);
                   continue;
               }
               return -1; // error
           }
           bytestowrite -= bytes_written;
           pos += bytes_written;
       }
    }
    return 0; // success
}

// Default value. I think it will get close to maximum speed on most
// systems, short of using mmap etc. But porters / integrators
// might want to set it smaller, if the system is very memory
// constrained and they don't want this routine to starve
// concurrent ops of memory. And they might want to set it larger
// if I'm completely wrong and larger buffers improve performance.
// It's worth trying several MB at least once, although with huge
// allocations you have to watch for the linux 
// "crash on access instead of returning 0" behaviour for failed malloc.
#ifndef FILECOPY_BUFFER_SIZE
    #define FILECOPY_BUFFER_SIZE (64*1024)
#endif

int copy_data(int fdin, int fdout) {
    // optional exercise for reader: take the file size as a parameter,
    // and don't use a buffer any bigger than that. This prevents 
    // memory-hogging if FILECOPY_BUFFER_SIZE is very large and the file
    // is small.
    for (size_t bufsize = FILECOPY_BUFFER_SIZE; bufsize >= 256; bufsize /= 2) {
        void *buffer = malloc(bufsize);
        if (buffer != NULL) {
            int result = copy_data_buffer(fdin, fdout, buffer, bufsize);
            free(buffer);
            return result;
        }
    }
    // could use a stack buffer here instead of failing, if desired.
    // 128 bytes ought to fit on any stack worth having, but again
    // this could be made configurable.
    return -1; // errno is ENOMEM
}

لفتح ملف الإدخال:

int fdin = open(infile, O_RDONLY|O_BINARY, 0);
if (fdin == -1) return -1;

فتح ملف الإخراج أمر صعب.كأساس تريد:

int fdout = open(outfile, O_WRONLY|O_BINARY|O_CREAT|O_TRUNC, 0x1ff);
if (fdout == -1) {
    close(fdin);
    return -1;
}

لكن هناك عوامل محيرة:

  • تحتاج إلى وضع حالة خاصة عندما تكون الملفات متماثلة، ولا أستطيع تذكر كيفية القيام بذلك بشكل متنقل.
  • إذا كان اسم الملف الناتج عبارة عن دليل، فقد ترغب في نسخ الملف إلى الدليل.
  • إذا كان ملف الإخراج موجودًا بالفعل (افتحه باستخدام O_EXCL لتحديد ذلك والتحقق من وجود EEXIST عند وجود خطأ)، فقد ترغب في القيام بشيء مختلف، مثل cp -i يفعل.
  • قد ترغب في أن تعكس أذونات ملف الإخراج تلك الخاصة بملف الإدخال.
  • قد ترغب في نسخ بيانات تعريفية أخرى خاصة بالنظام الأساسي.
  • قد ترغب أو لا ترغب في إلغاء ربط ملف الإخراج عند حدوث خطأ.

من الواضح أن الإجابات على كل هذه الأسئلة يمكن أن تكون "افعل نفس الشيء cp".في هذه الحالة تكون إجابة السؤال الأصلي هي "تجاهل كل ما قلته أنا أو أي شخص آخر، واستخدم المصدر cp".

راجع للشغل، الحصول على حجم كتلة نظام الملفات هو أقرب إلى عديمة الفائدة.ستلاحظ دائمًا زيادة السرعة مع حجم المخزن المؤقت لفترة طويلة بعد تجاوز حجم كتلة القرص.

نصائح أخرى

هذه هي الوظيفة التي أستخدمها عندما أحتاج إلى النسخ من ملف إلى آخر - باستخدام أدوات الاختبار:

/*
@(#)File:           $RCSfile: fcopy.c,v $
@(#)Version:        $Revision: 1.11 $
@(#)Last changed:   $Date: 2008/02/11 07:28:06 $
@(#)Purpose:        Copy the rest of file1 to file2
@(#)Author:         J Leffler
@(#)Modified:       1991,1997,2000,2003,2005,2008
*/

/*TABSTOP=4*/

#include "jlss.h"
#include "stderr.h"

#ifndef lint
/* Prevent over-aggressive optimizers from eliminating ID string */
const char jlss_id_fcopy_c[] = "@(#)$Id: fcopy.c,v 1.11 2008/02/11 07:28:06 jleffler Exp $";
#endif /* lint */

void fcopy(FILE *f1, FILE *f2)
{
    char            buffer[BUFSIZ];
    size_t          n;

    while ((n = fread(buffer, sizeof(char), sizeof(buffer), f1)) > 0)
    {
        if (fwrite(buffer, sizeof(char), n, f2) != n)
            err_syserr("write failed\n");
    }
}

#ifdef TEST

int main(int argc, char **argv)
{
    FILE *fp1;
    FILE *fp2;

    err_setarg0(argv[0]);
    if (argc != 3)
        err_usage("from to");
    if ((fp1 = fopen(argv[1], "rb")) == 0)
        err_syserr("cannot open file %s for reading\n", argv[1]);
    if ((fp2 = fopen(argv[2], "wb")) == 0)
        err_syserr("cannot open file %s for writing\n", argv[2]);
    fcopy(fp1, fp2);
    return(0);
}

#endif /* TEST */

من الواضح أن هذا الإصدار يستخدم مؤشرات الملفات من الإدخال/الإخراج القياسي وليس واصفات الملفات، ولكنه فعال إلى حد معقول ومحمول قدر الإمكان.


حسنا، باستثناء وظيفة الخطأ - وهذا أمر غريب بالنسبة لي.طالما أنك تتعامل مع الأخطاء بشكل نظيف، فيجب أن تكون على ما يرام.ال "jlss.h" يعلن الرأس fcopy();ال "stderr.h" يعلن الرأس err_syserr() من بين العديد من وظائف الإبلاغ عن الأخطاء المماثلة الأخرى.يتبع ذلك إصدار بسيط من الوظيفة - يضيف الإصدار الحقيقي اسم البرنامج ويقوم ببعض الأشياء الأخرى.

#include "stderr.h"
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void err_syserr(const char *fmt, ...)
{
    int errnum = errno;
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    if (errnum != 0)
        fprintf(stderr, "(%d: %s)\n", errnum, strerror(errnum));
    exit(1);
}

يمكن التعامل مع الكود أعلاه على أنه يحتوي على ترخيص BSD حديث أو GPL v3 حسب اختيارك.

يجب أن يكون حجم كل قراءة من مضاعفات 512 (حجم القطاع) 4096 أمر جيد

وإليك مثال سهل وواضح للغاية: انسخ ملفًا.نظرًا لأنه مكتوب بلغة ANSI-C دون أي استدعاءات دالة معينة، أعتقد أن هذا سيكون محمولاً إلى حد كبير.

اعتمادًا على ما تعنيه بنسخ ملف، فمن المؤكد أن الأمر ليس تافهًا.إذا كنت تقصد نسخ المحتوى فقط، فلن يكون هناك ما يمكنك فعله تقريبًا.لكن بشكل عام، تحتاج إلى نسخ البيانات الوصفية للملف، وهذا بالتأكيد يعتمد على النظام الأساسي.لا أعرف أي مكتبة C تفعل ما تريد بطريقة محمولة.إن مجرد التعامل مع اسم الملف بمفرده ليس أمرًا تافهًا إذا كنت تهتم بإمكانية النقل.

في لغة C++، توجد مكتبة الملفات يعزز

شيء واحد وجدته عند تنفيذ نسخة الملف الخاص بي، ويبدو واضحًا ولكنه ليس كذلك:I/O هي بطيء.يمكنك تحديد سرعة نسختك إلى حد كبير من خلال عدد النسخ التي تقوم بها.لذا من الواضح أنك تحتاج إلى القيام بأقل عدد ممكن منها.

أفضل النتائج التي وجدتها كانت عندما حصلت على مخزن مؤقت ضخم، وقرأت الملف المصدر بأكمله فيه في إدخال/إخراج واحد، ثم كتبت المخزن المؤقت بالكامل مرة أخرى منه في إدخال/إخراج واحد.ولو اضطررت إلى القيام بذلك على 10 دفعات، فسيكون الأمر بطيئًا للغاية.كانت محاولة قراءة وكتابة كل بايت، كما قد يحاول المبرمج الساذج أولاً، أمرًا مؤلمًا للغاية.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top