Windows মেমরির গভীরতা (পর্ব ৩) — সেকশন অবজেক্ট ও কপি-অন-রাইট: DLL ও ফাইল ম্যাপিং আসলে কী

· · Windows, মেমরি ব্যবস্থাপনা, শেয়ার্ড মেমরি, ফাইল ম্যাপিং, কপি-অন-রাইট, DLL, Cache Manager

আগের নিবন্ধে, «Windows মেমরির গভীরতা (পর্ব ২) — একটি ভৌত পেজের জীবন», আমরা দেখেছি Working Set ছাড়া ভৌত পেজ কীভাবে Modified, Standby, Free ও Zeroed দিয়ে যায়। সেই Standby-তে DLL, EXE, ম্যাপ করা ফাইল ও ফাইল ক্যাশের পেজও থাকে।

এখানে প্রশ্ন ওঠে। ১০০টি প্রক্রিয়া একই kernel32.dll ব্যবহার করলে Windows কি RAM-এ কোড পেজের ১০০ সেট রাখে? দুই প্রক্রিয়া একই ফাইল মেমরি-ম্যাপ করলে, একজন পড়া পেজ অন্যজন ব্যবহার করতে পারে?

উত্তর হলো একাধিক ভার্চুয়াল ঠিকানা একই ভৌত পেজে ম্যাপ করা। সেই ভাগাভাগির একক প্রতিনিধিত্ব করে সেকশন অবজেক্ট, এবং শুধু লেখার সময় ভাগাভাগি ভাঙার ব্যবস্থা কপি-অন-রাইট (Copy-on-Write, CoW)।

এই নিবন্ধ অনুসরণ করে EXE ও DLL, ডেটা ফাইল, পেজফাইল-ব্যাকড শেয়ার্ড মেমরি এবং ফাইল ক্যাশ একই ফাইল স্ট্রিমে কীভাবে লাগে, আর প্রক্রিয়াকরণ পথ কোথায় আলাদা হয়। সংখ্যা পড়া নিজে পরিচিতি নিবন্ধ «What Does Windows’ “Memory Usage” Actually Mean?»-কে ভিত্তি ধরে।

«Windows মেমরির গভীরতা» — তিন পর্ব

  1. পর্ব ১: ভার্চুয়াল ঠিকানা ও পেজ ফল্ট
    Commit করা ভার্চুয়াল পেজ ভৌত RAM পাওয়ার মুহূর্ত অনুসরণ করি।
  2. পর্ব ২: একটি ভৌত পেজের জীবন
    Working Set ছাড়া পেজের অবস্থা পরিবর্তন অনুসরণ করি।
  3. পর্ব ৩ (এই নিবন্ধ): সেকশন অবজেক্ট ও কপি-অন-রাইট
    DLL, ফাইল ম্যাপিং ও শেয়ার্ড মেমরি কীভাবে ভৌত পেজ ভাগ করে সেই ব্যবস্থা অনুসরণ করি।

পর্ব ৩ যে প্রশ্নের উত্তর দেয় তা একটিই।

একাধিক প্রক্রিয়া একই DLL বা ফাইলকে ভৌত পেজের এক সেট হিসেবে কেন ব্যবহার করতে পারে?

লক্ষ্য পাঠক সেই ডেভেলপার ও অপারেটর যারা DLL ভাগাভাগি, CreateFileMapping, MapViewOfFile, শেয়ার্ড মেমরি, CoW ও ফাইল ক্যাশের সম্পর্ক শুধু API ব্যবহার নয় অভ্যন্তরীণ কাঠামো থেকে বুঝতে চান। পূর্বশর্ত Windows 10/11 বা বর্তমান Windows Server, এবং প্রয়োজনীয় পটভূমি ভার্চুয়াল ঠিকানা, পেজ ফল্ট ও Working Set-এর ভিত্তি। কঠিনতা মাঝারি; Control Area ও Prototype PTE-এর মতো অভ্যন্তরীণ শব্দও ব্যবহার করি, কিন্তু পর্যবেক্ষণ VMMap, Process Explorer ও QueryWorkingSetEx দিয়ে পুনরাবৃত্তিযোগ্য।

১. আগে উপসংহার

শুরুতে সামগ্রিক ছবি।

  • সেকশন অবজেক্ট ভাগযোগ্য মেমরি পরিসর প্রতিনিধিত্ব করে।
    প্রতিটি প্রক্রিয়া সেই সেকশনের অংশ নিজের ভার্চুয়াল স্পেসে «ভিউ» হিসেবে ম্যাপ করে।1
  • একই সেকশনের ভিউ একই ভার্চুয়াল ঠিকানায় থাকতে হয় না।
    প্রক্রিয়া A-এর 0x000001... ও প্রক্রিয়া B-এর 0x000002... একই সেকশন অফসেট ও একই ভৌত পেজ নির্দেশ করতে পারে।2
  • ফাইল-ব্যাকড ও পেজফাইল-ব্যাকড সেকশন আছে।
    প্রথমটি আসল ফাইল ব্যবহার করে; দ্বিতীয়টি স্পষ্ট ফাইল ছাড়া শেয়ার্ড মেমরি ইত্যাদিতে।2
  • EXE/DLL লোড ইমেজ সেকশন; সাধারণ ফাইল ম্যাপিং ডেটা সেকশন।
    SEC_IMAGE-এ PE-এর ভিতরের সেকশন বৈশিষ্ট্য পেজ সুরক্ষা নির্ধারণ করে।3
  • পড়ার সময় একই ভৌত পেজ ভাগ করা যায়।
    এক পক্ষ CoW পেজে লিখলে শুধু সেই পেজ কপি হয় এবং লেখক প্রক্রিয়ার PTE বদলে যায়।4
  • একই ফাইল স্ট্রিমেও ক্যাশ, ডেটা ও ইমেজ পথ আলাদা।
    Cache Manager-এর ক্যাশড I/O SharedCacheMap ব্যবহার করে, ডেটা ম্যাপিং DataSectionObject, EXE/DLL ImageSectionObject। তিনটি SECTION_OBJECT_POINTERS দিয়ে একই স্ট্রিমে লাগে, কিন্তু ইমেজ ফল্ট Cache Manager দিয়ে যায় না।5
  • শুধু Private Bytes দিয়ে CoW হয়েছে নিশ্চিত করা যায় না।
    FILE_MAP_COPY ম্যাপের সময় পুরো ভিউয়ের Commit আগেই ধার্য করে, যদি পরে প্রতিটি পেজ ব্যক্তিগত হয়। পেজ-প্রতি পরীক্ষায় QueryWorkingSetEx-এর Shared বিট ব্যবহার করুন।67

এক বাক্যে: যা ভাগ হয় তা ভার্চুয়াল ঠিকানা নয়, সেকশনের ভিতরের বিষয়বস্তু এবং সেই মুহূর্তের সংশ্লিষ্ট ভৌত পেজ

২. সেকশন অবজেক্ট ও ভিউ

Microsoft-এর সংজ্ঞায় সেকশন অবজেক্ট ভাগযোগ্য মেমরি অঞ্চল প্রতিনিধিত্ব করে এবং ফাইলকে প্রক্রিয়ার অ্যাড্রেস স্পেসে ম্যাপ করার ব্যবস্থাও।1

বুঝতে কৌশল হলো সেকশন নিজে এবং প্রতিটি প্রক্রিয়া যে ভিউ দেখে তা আলাদা রাখা।

ধারণা ভূমিকা
সেকশন অবজেক্ট ভাগ করার বিষয়বস্তু, আকার, ব্যাকিং স্টোর ও সুরক্ষার সর্বোচ্চ সীমা প্রতিনিধিত্ব করে
ভিউ সেকশনের অংশ কোনো প্রক্রিয়ায় ভার্চুয়াল ঠিকানা পরিসর হিসেবে দেখায়
PTE ভিউয়ের প্রতিটি ভার্চুয়াল পেজকে বর্তমান ভৌত পেজ বা অবাস্তবায়িত অবস্থার সঙ্গে বাঁধে
PFN RAM-এ সত্যি থাকা ভৌত পেজ প্রতিনিধিত্ব করে

Win32-এ CreateFileMapping ফাইল-ম্যাপিং অবজেক্টের হ্যান্ডেল ফেরায়, এবং MapViewOfFile প্রক্রিয়ার ভার্চুয়াল স্পেসে ভিউ তৈরি করে।3

HANDLE mapping = CreateFileMappingW(
    file,
    nullptr,
    PAGE_READONLY,
    0,
    0,
    nullptr);

void* view = MapViewOfFile(
    mapping,
    FILE_MAP_READ,
    0,
    0,
    0);

শুধু CreateFileMapping ডাকলে প্রক্রিয়া পড়তে পারে এমন ঠিকানা এখনো মেলে না। ভিউ তৈরি করলেও প্রতিটি পেজ তৎক্ষণাৎ RAM-এ যায় না। প্রথম স্পর্শ করা পেজ থেকে পেজ ফল্ট ফাইল বিষয়বস্তু পড়ে PTE-তে ভৌত পেজ বাঁধে।8 অর্থাৎ পর্ব ১-এর ডিমান্ড পেজিং সেকশন ভিউতেও প্রযোজ্য।

২.১. একই সেকশন, ভিন্ন ভার্চুয়াল ঠিকানা

প্রক্রিয়া A ও প্রক্রিয়া B একই সেকশনের একই অফসেট ম্যাপ করলেও ভিউয়ের শুরুর ঠিকানা ভিন্ন হতে পারে।

ভিন্ন ভার্চুয়াল ঠিকানা একই ভৌত পেজে ম্যাপ করাপ্রক্রিয়া A ও প্রক্রিয়া B-এর প্রত্যেকের ভিন্ন ভার্চুয়াল ঠিকানায় ভিউ আছে, কিন্তু তারা একই সেকশন অফসেট দিয়ে একই ভৌত পেজ PFN X-এ পৌঁছায়প্রক্রিয়া A: 0x000001A00000 + 0x3000সেকশন অফসেট 0x3000প্রক্রিয়া B: 0x000002700000 + 0x3000একই ভৌত পেজ PFN X

চিত্র ১: যা ভাগ হয় তা সেকশনের ভিতরের বিষয়বস্তু ও ভৌত পেজ, ভার্চুয়াল ঠিকানা নয়।

তাই শেয়ার্ড মেমরিতে কাঁচা পয়েন্টার রাখা উচিত নয়। প্রক্রিয়া A-এর পয়েন্টার মান প্রক্রিয়া B-তে অপ্রাসঙ্গিক ঠিকানা হতে পারে।

শেয়ার্ড কাঠামোতে ভিউয়ের শুরু থেকে অফসেট, নির্দিষ্ট-প্রস্থ পূর্ণসংখ্যা এবং স্পষ্ট সংস্করণ ও অ্যালাইনমেন্ট ব্যবহার করুন। Microsoft-এর MapViewOfFileEx নথিও পয়েন্টারের বদলে বেস থেকে অফসেট রাখতে বলে, কারণ ভবিষ্যতে একই ঠিকানা পাওয়া যাবে এমন নিশ্চয়তা নেই।6

৩. ফাইল-ব্যাকড ও পেজফাইল-ব্যাকড

বিষয়বস্তু কোথা থেকে পুনরুদ্ধার করা যায় সেই অনুসারে সেকশন দুই বড় ধরনে ভাগ।

ফাইল-ব্যাকড ও পেজফাইল-ব্যাকড কীভাবে আলাদা হয়CreateFileMapping-এ আসল ফাইল দিলে ফাইল-ব্যাকড সেকশন হয়, এবং পরিষ্কার পেজ মূল ফাইল থেকে আবার পড়া যায়। INVALID_HANDLE_VALUE দিলে পেজফাইল-ব্যাকড সেকশন হয়; পেজ ফাইল বিষয়বস্তু ধরে, অবজেক্ট ধ্বংস হলে তা অদৃশ্য হয়আসল ফাইল হ্যান্ডেল দিনINVALID_HANDLE_VALUE দিনCreateFileMappingফাইল-ব্যাকড সেকশনপেজফাইল-ব্যাকড সেকশনপরিষ্কার পেজ মূল ফাইল থেকে আবার পড়া যায়পেজ ফাইল বিষয়বস্তু ধরে, ধ্বংসে অদৃশ্য হয়

চিত্র ২: ব্যাকিং স্টোরের পার্থক্য নির্ধারণ করে বিষয়বস্তু কোথা থেকে ফেরে এবং কতক্ষণ বাঁচে।

৩.১. ফাইল-ব্যাকড সেকশন

CreateFileMapping-এ আসল ফাইল দিলে ফাইল-ব্যাকড সেকশন হয়।

  • শুধু-পঠন ভিউ প্রয়োজনীয় পেজ ফাইল থেকে পড়ে।
  • পঠন/লিখন ভিউয়ের পরিবর্তন সেই ফাইলের ডেটা হিসেবে গণ্য।
  • CoW ভিউয়ের পরিবর্তন মূল ফাইলে লেখা হয় না; সেগুলো নিজস্ব পেজ হয়।

ফাইল-ব্যাকড পেজ পরিষ্কার হলে ভৌত পেজ ফেলে মূল ফাইল থেকে আবার পড়া যায়। সেই গুণ পর্ব ২-এর Standby ও ফাইল ক্যাশের দক্ষতা ধরে।

৩.২. পেজফাইল-ব্যাকড সেকশন

CreateFileMapping-এর hFile-এ INVALID_HANDLE_VALUE দিয়ে আকার নির্দিষ্ট করলে পেজফাইল-ব্যাকড সেকশন হয়।

HANDLE mapping = CreateFileMappingW(
    INVALID_HANDLE_VALUE,
    nullptr,
    PAGE_READWRITE,
    0,
    64 * 1024,
    L"Local\\KomuraMemoryDemo");

এটি স্পষ্ট ডেটা ফাইল ছাড়া সেকশন, পেজ ফাইল দিয়ে ধরা। প্রাথমিক বিষয়বস্তু শূন্য, এবং একাধিক প্রক্রিয়া নাম, হ্যান্ডেল উত্তরাধিকার, DuplicateHandle ইত্যাদি দিয়ে একই অবজেক্ট খুলতে পারে।93 পরিবর্তন একই শেয়ার্ড পেজ ম্যাপ করা প্রক্রিয়ায় দেখা যায়। অন্যদিকে সেকশন অবজেক্ট ধ্বংস হলে বিষয়বস্তু থাকে না, তাই স্থায়ী ফাইল রাখতে উপযুক্ত নয়।2

সতর্কতা: শেয়ার্ড মেমরি স্বয়ংক্রিয় পারস্পরিক বর্জন আনে না। মিউটেক্স, সেমফোর, ইভেন্ট, লক-ফ্রি প্রোটোকল ইত্যাদি আলাদা ডিজাইন করুন।9

৪. ইমেজ ম্যাপিং ও ডেটা ম্যাপিং

«EXE বা DLL-ও ফাইল ম্যাপিং» বললেও সাধারণ ডেটা ফাইল থেকে পার্থক্য আলাদা রাখতে হয়।

বিষয় ইমেজ ম্যাপিং ডেটা ম্যাপিং
প্রধান ব্যবহার EXE বা DLL লোড সাধারণ ফাইল, শেয়ার্ড ডেটা
তৈরির বৈশিষ্ট্য SEC_IMAGE PAGE_READONLY, PAGE_READWRITE ইত্যাদি
পেজ সুরক্ষা PE ইমেজের ভিতরের বৈশিষ্ট্য নির্ধারণ করে ম্যাপিং ও ভিউ নির্দিষ্টকরণ নির্ধারণ করে
লেখা লেখার যোগ্য সেকশন বা CoW দিয়ে ব্যক্তিগত করা যায় শেয়ার্ড লেখা বা CoW বেছে নেওয়া যায়
VirtualQuery Type MEM_IMAGE MEM_MAPPED

SEC_IMAGE-এ ভিউয়ের পেজ সুরক্ষা মূলত চালু ইমেজের নিজস্ব সেকশন বৈশিষ্ট্য নির্ধারণ করে, CreateFileMapping-এ দেওয়া সাধারণ সুরক্ষা মানের চেয়ে বেশি।3

এই ব্যবস্থায় কোডের মতো অপরিবর্তিত পেজ অনেক প্রক্রিয়ায় একই ভৌত পেজ ভাগ করতে পারে, এবং শুধু প্রক্রিয়া-নিজস্ব পরিবর্তন দরকার এমন পেজ CoW দিয়ে শাখা নেয়। তবু প্রতিটি DLL পেজ অবশ্যই ভাগ হয় না — ASLR রিলোকেশন, লোডার সংশোধন, হটপ্যাচ, প্রকৃত PE সেকশন বৈশিষ্ট্য ইত্যাদির জন্য।

গুরুত্বপূর্ণ নকশা হলো আগে ভাগযোগ্য পেজ ভাগ করুন, এবং শুধু পরিবর্তন দরকার এমন পেজ দেরিতে কপি করুন

৫. কপি-অন-রাইট শুরু থেকে শেষ পর্যন্ত

দুই প্রক্রিয়া একই CoW পেজ পড়ছে এমন অবস্থা থেকে প্রক্রিয়া A এক বাইট লেখা পর্যন্ত অনুসরণ করি।

৫.১. লেখার আগে

লেখা হওয়ার আগে দুই প্রক্রিয়ার PTE ধারণাগতভাবে একই শেয়ার্ড পেজে পৌঁছায়, এবং পড়া তেমনই সফল হয়।

কপি-অন-রাইটের আগের শেয়ার্ড অবস্থালেখা হওয়ার আগে প্রক্রিয়া A-এর PTE ও প্রক্রিয়া B-এর PTE দুটোই একই শেয়ার্ড পেজ PFN X-এ পৌঁছায়, এবং পড়া তেমনই সফল হয়প্রক্রিয়া A PTEশেয়ার্ড PFN X(পড়া / কপি-অন-রাইট)প্রক্রিয়া B PTE

চিত্র ৩: লেখার আগে দুই প্রক্রিয়ার PTE একই ভৌত পেজ নির্দেশ করে।

৫.২. লেখার সময় সুরক্ষা ফল্ট

CoW পেজ শুরু থেকে সাধারণ শেয়ার্ড লেখার যোগ্য পেজ নয়। প্রক্রিয়া A লিখতে চাইলে CPU সুরক্ষা ফল্ট তোলে। নিয়ন্ত্রণ পাওয়া মেমরি ম্যানেজার বিচার করে এটি অবৈধ লেখা নয়, CoW বৈশিষ্ট্যে লেখা।

৫.৩. নতুন ভৌত পেজ তৈরি

সেই বিচারে Windows নিচের কাজ করে।

  1. প্রক্রিয়া A-এর জন্য একটি ভৌত পেজ নিন।
  2. PFN X-এর বিষয়বস্তু নতুন পেজ PFN Y-এ কপি করুন।
  3. প্রক্রিয়া A-এর PTE PFN Y-এ বদলান।
  4. প্রক্রিয়া A-এর সুরক্ষা সাধারণ পঠন/লিখনে বদলান।
  5. ব্যর্থ লেখার নির্দেশ আবার চালান।
কপি-অন-রাইটের পর বিভক্ত অবস্থাপ্রক্রিয়া A লেখার পর শুধু প্রক্রিয়া A-এর PTE বিষয়বস্তুর কপি পাওয়া নিজস্ব পেজ PFN Y-এ বদলায়, আর প্রক্রিয়া B-এর PTE আসল শেয়ার্ড পেজ PFN X নির্দেশ করে থাকেলেখার সময় কপিপ্রক্রিয়া A PTEনিজস্ব PFN Y(R/W, লেখার পর)প্রক্রিয়া B PTEশেয়ার্ড PFN X(আসল)

চিত্র ৪: শুধু লেখক প্রক্রিয়ার PTE নতুন নিজস্ব পেজে বদলায়; অন্য পক্ষ আসল বিষয়বস্তু পড়ে যায়।

প্রক্রিয়া B আসল বিষয়বস্তু পড়ে যায় এবং প্রক্রিয়া A-এর পরিবর্তন দেখে না। এটাই Copy-on-Write। DLL ভাগাভাগি ও FILE_MAP_COPY একই নীতি ব্যবহার করে: না লেখা পর্যন্ত কপি করবেন না46

৫.৪. FILE_MAP_WRITE-এর পার্থক্য

FILE_MAP_WRITE-এর শেয়ার্ড লেখায় লেখা পেজ এমনভাবে নকশা যে এক পক্ষের পরিবর্তন একই ফাইল ম্যাপিং ব্যবহার করা অন্য ভিউ থেকেও দেখা যায়। FILE_MAP_COPY-এ বিপরীতে শুধু লেখা পেজ প্রক্রিয়া-নিজস্ব হয়; পরিবর্তন মূল ফাইলে ফেরত লেখা হয় না এবং ভিউ আনম্যাপ হলে হারিয়ে যায়।6

আপনি কি চান «শেয়ার্ড মেমরি দিয়ে হালনাগাদ পৌঁছানো», নাকি «প্রতিটি প্রক্রিয়া সাধারণ প্রাথমিক ডেটা থেকে নিজস্ব পরিবর্তন করুক»? উদ্দেশ্য অনুসারে সঠিক পছন্দ উল্টো।

৬. CoW-এর পরেও MEM_MAPPED / MEM_IMAGE

CoW-এর পর পেজ ভৌতভাবে Private হয়েছে। তখন মনে হতে পারে VirtualQuery-এর Type-ও MEM_PRIVATE হবে, কিন্তু আসলে ডেটা ভিউ MEM_MAPPED এবং চালুযোগ্য ইমেজ MEM_IMAGE থাকে। VirtualQuery জানায় অঞ্চল কোন প্রাথমিক বরাদ্দ থেকে এসেছে।7

পেজ-প্রতি CoW ইতিমধ্যে হয়েছে কি না দেখতে এই পদ্ধতি ব্যবহার করুন।

  1. লক্ষ্য পেজ অ্যাক্সেস করে রেসিডেন্ট করুন।
  2. QueryWorkingSetEx দিয়ে পেজের Working Set তথ্য নিন।
  3. Shared বিট দেখুন।
  4. Shared == 0 হলে সেই রেসিডেন্ট পেজ Private।

VMMap-এ নিশ্চিত করার সময়ও শুধু অঞ্চলের Type নয়, Working Set-এর Private/Shareable ভাঙন দেখুন।

৬.১. Private Bytes নাও বাড়তে পারে

FILE_MAP_COPY-এ প্রক্রিয়া পরে ভিউয়ের প্রতিটি পেজে লিখতে পারে। তাই Windows ম্যাপের সময় পুরো ভিউয়ের সমান Commit খরচ নেয়।6 ফলে প্রথম পেজ লিখলে সেই মুহূর্তে Private Bytes ৪KiB নাও বাড়তে পারে।

CoW পর্যবেক্ষণে অগ্রাধিকার দেওয়ার মেট্রিক নিচেরগুলো।

  • QueryWorkingSetEx-এর Shared বিট
  • VMMap-এর Private WS / Shareable WS
  • RAMMap-এর ভৌত-পেজ তথ্য
  • সম্পূরক তথ্য হিসেবে Private Bytes

«লেখার মুহূর্তে Private Bytes বেড়েছে কি না»-কে পাস/ফেল পরীক্ষা ধরলে সঠিক কাজ করা CoW মিস হবে।

৭. Cache Manager-এর সংযোগবিন্দু — তিন পথ আলাদা

«EXE/DLL লোড, ফাইল ক্যাশ ও শেয়ার্ড মেমরি সব সেকশন» বললে সামগ্রিক ছবি মেলে, কিন্তু বাস্তবায়ন এক অবজেক্টে গুটিয়ে ফেলা যাবে না।

ফাইল স্ট্রিমের SECTION_OBJECT_POINTERS আছে, যা মেমরি ম্যানেজার ও Cache Manager ব্যবহার করে।

typedef struct _SECTION_OBJECT_POINTERS {
    PVOID DataSectionObject;
    PVOID SharedCacheMap;
    PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
  • DataSectionObject: ডেটা ফাইলের সেকশন অবস্থা
  • SharedCacheMap: Cache Manager যে ক্যাশ ভিউ ট্র্যাক করে
  • ImageSectionObject: চালুযোগ্য ইমেজের সেকশন অবস্থা

Microsoft নথি বলে এই কাঠামো ফাইল অবজেক্টকে ফাইল স্ট্রিমের সেকশনের সঙ্গে বাঁধে এবং মেমরিতে বিষয়বস্তু ও ক্যাশ তথ্য ট্র্যাক করে।5

এখানে I/O ধারা ও মেমরি ধারা মিলে। তবু তিন পথ আলাদা বুঝুন।

  • ক্যাশড ReadFile / WriteFile Cache Manager-এর SharedCacheMap ও ক্যাশ ভিউ ব্যবহার করে।
  • ডেটা ফাইলে ম্যাপিং ফল্ট মেমরি ম্যানেজার DataSectionObject দিকে সামলায়। একই স্ট্রিমের ক্যাশড I/O-এর সঙ্গে সহযোগ করে বিষয়বস্তু সামঞ্জস্য রাখে।
  • EXE/DLL-এ ইমেজ ফল্ট মেমরি ম্যানেজার ImageSectionObject ও পেজিং I/O দিয়ে সামলায়। এটি Cache Manager-এর SharedCacheMap দিয়ে যাওয়া পথ নয়।
একই ফাইল স্ট্রিমে লাগা তিন পথক্যাশড ReadFile/WriteFile SharedCacheMap ব্যবহার করে, ডেটা-ম্যাপিং ফল্ট DataSectionObject, EXE/DLL ইমেজ ফল্ট ImageSectionObject; তিনটি SECTION_OBJECT_POINTERS দিয়ে একই ফাইল স্ট্রিমে লাগেক্যাশড ReadFile / WriteFileSharedCacheMapডেটা-ম্যাপিং ফল্টDataSectionObjectEXE/DLL ইমেজ ফল্টImageSectionObjectএকই ফাইল স্ট্রিম(SECTION_OBJECT_POINTERS)

চিত্র ৫: তিন পথ আলাদা প্রক্রিয়াজাত হয়, কিন্তু একই ফাইল স্ট্রিমে লাগে।

তিনটির মিল এই নয় যে «সব Cache Manager-এ ঢোকে», বরং একই ফাইল স্ট্রিম ক্যাশ, ডেটা সেকশন ও ইমেজ সেকশন—আলাদা অবস্থা—SECTION_OBJECT_POINTERS দিয়ে বাঁধে।5 ক্যাশ পঠন-লিখন, Lazy Writer, এবং Cc ও Mm-এর সম্পর্ক «The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?»-এ আছে।

মনে রাখুন মেমরি-ম্যাপ ভিউকে ReadFile/WriteFile-এর সঙ্গে মেশালে সবসময় একই মুহূর্তের বিষয়বস্তু দেখার নিশ্চয়তা নেই। নকশায় সিঙ্ক, ফ্লাশ ও ফাইল-শেয়ার মোড থাকতে হবে।36

৮. অবজেক্ট ও ভিউয়ের আয়ু

শুধু CreateFileMapping হ্যান্ডেল বন্ধ করলে বিদ্যমান ভিউ ধ্বংস হয় না। ভিউ সেকশনের অভ্যন্তরীণ রেফারেন্স ধরে, এবং প্রতিটি ভিউ UnmapViewOfFile ও প্রতিটি হ্যান্ডেল CloseHandle হওয়ার পরেই অবজেক্ট ধ্বংসযোগ্য হয়।3

UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);

আয়ুর এই বিচ্ছেদ «ফাইল বন্ধ করেছি, তবু ব্যবহার হচ্ছে» ঘটনার কারণ। ফাইল হ্যান্ডেল বন্ধ করার পরেও ইমেজ সেকশন বা ডেটা ভিউ ফাইল স্ট্রিম নির্দেশ করলে ফাইলের চূড়ান্ত বন্ধ পরে আসে।

I/O দিকের পরিষ্কার/বন্ধের সম্পর্ক «The Depths of Windows I/O (Part 1)»-এ, এবং বাস্তবায়ন ফাঁদ «Shared Memory Pitfalls and Practical Best Practices»-এ।

৯. নিজে দেখুন

৯.১. দুই প্রক্রিয়া থেকে একই DLL দেখা

আগে বিদ্যমান প্রক্রিয়া দিয়ে DLL ভাগাভাগি নিশ্চিত করুন।

  1. প্রশাসক হিসেবে Process Explorer চালু করুন।
  2. দুটি cmd.exe প্রক্রিয়া চালু করুন।
  3. View > Lower Pane View > DLLs বেছে নিন।
  4. দুই প্রক্রিয়ায় একই DLL-এর পথ ও ম্যাপিং নিশ্চিত করুন।
  5. VMMap-এ প্রতিটি cmd.exe খুলে Images Working Set, Private ও Shareable তুলনা করুন।

Process Explorer-এ একই DLL দেখা প্রমাণ যে দুজনেই একই ইমেজ ম্যাপ করেছে। শুধু তা প্রতিটি পেজের PFN মিলে যায় প্রমাণ করে না। পেজ-প্রতি ভাগাভাগি নিশ্চিত করতে VMMap-এর Shareable ভাঙন, RAMMap ও QueryWorkingSetEx মিলিয়ে দেখুন। Process Explorer ও VMMap Sysinternals দেয়।1011

৯.২. দুই প্রক্রিয়া থেকে FILE_MAP_COPY দেখা

নিচের প্রোগ্রাম একই ফাইল CoW ভিউ হিসেবে ম্যাপ করে এবং QueryWorkingSetEx-এর Shared বিট দেখায়। ফাইল-ম্যাপিং অবজেক্ট PAGE_READONLY দিয়ে তৈরি, কিন্তু সেই সুরক্ষা FILE_MAP_COPY ভিউয়ের সঙ্গে সামঞ্জস্যপূর্ণ, এবং ভিউ দিকের প্রথম লেখা CoW ঘটায়।3

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>

#include <cstdio>
#include <cwchar>

#pragma comment(lib, "Psapi.lib")

void PrintPage(const char* stage, void* address)
{
    MEMORY_BASIC_INFORMATION mbi{};
    if (!VirtualQuery(address, &mbi, sizeof(mbi))) {
        std::printf("VirtualQuery failed: %lu\n", GetLastError());
        return;
    }

    for (int attempt = 0; attempt < 3; ++attempt) {
        // The page may have been trimmed while the user was waiting.
        // Touch it immediately before querying the working-set attributes.
        volatile unsigned char resident =
            *static_cast<volatile unsigned char*>(address);
        (void)resident;

        PSAPI_WORKING_SET_EX_INFORMATION ws{};
        ws.VirtualAddress = address;
        if (!QueryWorkingSetEx(GetCurrentProcess(), &ws, sizeof(ws))) {
            std::printf("QueryWorkingSetEx failed: %lu\n", GetLastError());
            return;
        }

        if (!ws.VirtualAttributes.Valid) {
            Sleep(0);
            continue;
        }

        std::printf(
            "%s: Type=0x%lx Valid=1 Shared=%llu ShareCount=%llu\n",
            stage,
            static_cast<unsigned long>(mbi.Type),
            static_cast<unsigned long long>(ws.VirtualAttributes.Shared),
            static_cast<unsigned long long>(ws.VirtualAttributes.ShareCount));
        return;
    }

    std::printf(
        "%s: page is not resident; Shared/ShareCount were not interpreted\n",
        stage);
}

int wmain(int argc, wchar_t** argv)
{
    if (argc != 3) {
        std::fwprintf(stderr, L"usage: cow_demo <file> <read|write>\n");
        return 2;
    }

    HANDLE file = CreateFileW(
        argv[1], GENERIC_READ, FILE_SHARE_READ,
        nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (file == INVALID_HANDLE_VALUE) return 3;

    HANDLE mapping = CreateFileMappingW(
        file, nullptr, PAGE_READONLY, 0, 0, nullptr);
    if (!mapping) {
        CloseHandle(file);
        return 4;
    }

    auto* view = static_cast<unsigned char*>(
        MapViewOfFile(mapping, FILE_MAP_COPY, 0, 0, 0));
    if (!view) {
        CloseHandle(mapping);
        CloseHandle(file);
        return 5;
    }

    volatile unsigned char value = view[0];
    (void)value;

    std::puts("Start the other process. When both are waiting, press Enter...");
    (void)std::getchar();
    PrintPage("before", view);

    if (std::wcscmp(argv[2], L"write") == 0) {
        std::puts("Press Enter to trigger copy-on-write...");
        (void)std::getchar();
        view[0] ^= 0x5a;
        PrintPage("after write", view);
    } else {
        std::puts("After the writer changes its page, press Enter...");
        (void)std::getchar();
        PrintPage("reader after peer write", view);
    }

    std::puts("Press Enter to exit...");
    (void)std::getchar();

    UnmapViewOfFile(view);
    CloseHandle(mapping);
    CloseHandle(file);
}

তৈরি ও প্রস্তুত করুন।

cl /std:c++20 /EHsc /W4 cow_demo.cpp

$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))

তারপর দুই কনসোল থেকে একই ফাইল খুলুন।

.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write

দুটো চালু করার পর আগে পড়ার দিকে তারপর লেখার দিকে Enter চাপুন, এবং দুই দিকের before-এ Shared সেট আছে নিশ্চিত করুন। লেখার দিকে আর একবার Enter চাপলে সেই প্রক্রিয়ার পেজ Shared ০ হয়। তারপর পড়ার দিকে Enter চাপলে পাঠক আসল পেজ পড়ে যাচ্ছে নিশ্চিত করা যায়। লেখার পরেও VirtualQuery-এর Type MEM_MAPPED থাকে।

মনে রাখুন PrintPage জিজ্ঞাসার ঠিক আগে লক্ষ্য পেজ আবার স্পর্শ করে, এবং Valid == 0 হলে SharedShareCount ব্যাখ্যা করে না এবং তিনবার পর্যন্ত আবার চেষ্টা করে। পেজ এখনো রেসিডেন্ট না হলে ফল দেয় না এবং তা জানায়। ShareCount সময় ও মেমরি চাপে বদলাতে পারে, তাই Valid == 1-এ নিশ্চিত Shared বিটের পরিবর্তন দেখুন, স্থির মান নয়।

১০. কাজে এড়ানোর পাঁচ ভুল পাঠ

১০.১. «শেয়ার্ড মেমরি একই ভার্চুয়াল ঠিকানায় শেষ হয়»

যা ভাগ হয় সেকশন ও ভৌত পেজ। ভিউয়ের ভার্চুয়াল ঠিকানা প্রক্রিয়া অনুসারে ভিন্ন হতে পারে, তাই কাঁচা পয়েন্টার নয় অফসেট রাখুন।

১০.২. «একই DLL হলে প্রতিটি পেজ অবশ্যই ভাগ»

পরিষ্কার কোড পেজ ভাগ করা সহজ, আর রিলোকেশন, লেখার যোগ্য সেকশন, CoW ও পরিমাপের মুহূর্তের রেসিডেন্সিও Private পেজ তৈরি করে।

১০.৩. «CoW-এর পর এটি MEM_PRIVATE হয়»

VirtualQuery-এর Type MEM_MAPPED বা MEM_IMAGE থাকে। প্রকৃত ভাগাভাগি QueryWorkingSetEx দিয়ে নিশ্চিত করুন।7

১০.৪. «Private Bytes না বাড়লে CoW হয়নি»

FILE_MAP_COPY পুরো ভিউয়ের Commit আগেই ধার্য করে। Private WS ও Shared বিট অগ্রাধিকার দিন।6

১০.৫. «পেজ ভাগ হলে সিঙ্ক অপ্রয়োজনীয়»

একই ভৌত পেজ দেখা এবং একাধিক CPU কোর থেকে নিরাপদে হালনাগাদ করা আলাদা সমস্যা। পরমাণবিকতা, মেমরি ক্রম, পারস্পরিক বর্জন, ক্র্যাশের মধ্যবর্তী অবস্থা ও সংস্করণ সামঞ্জস্য ডিজাইন করুন।

রেফারেন্স ও আয়ু আলাদা অনুসরণ করার ধারণা Excel COM ইন্টারঅপের পর প্রক্রিয়া থেকে যাওয়ার সমস্যায়ও প্রযোজ্য। আরও দেখুন «Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision»।

১১. সারসংক্ষেপ

  • সেকশন অবজেক্ট ভাগযোগ্য মেমরি পরিসর প্রতিনিধিত্ব করে, এবং প্রতিটি প্রক্রিয়া নিজের ভার্চুয়াল স্পেসে ভিউ হিসেবে ম্যাপ করে।1
  • একই সেকশনের একই অফসেট ভিন্ন ভার্চুয়াল ঠিকানা থেকে একই ভৌত পেজে ম্যাপ হয়।2
  • ফাইল-ব্যাকড সেকশন আসল ফাইল ধরে; পেজফাইল-ব্যাকড সেকশন নামযুক্ত শেয়ার্ড মেমরি ইত্যাদি ধরে।9
  • EXE/DLL ইমেজ সেকশন এবং সাধারণ ফাইল ডেটা সেকশন হিসেবে গণ্য; সুরক্ষা ও ফেরত লেখার গন্তব্য ভিন্ন।3
  • CoW পড়ার সময় ভৌত পেজ ভাগ করে এবং প্রথম লেখায় শুধু সেই পেজ কপি করে PTE বদলায়।4
  • CoW-এর পরেও VirtualQuery MEM_MAPPED/MEM_IMAGE ফেরায়, তাই QueryWorkingSetEx-এর Shared বিট দিয়ে নিশ্চিত করুন।7
  • FILE_MAP_COPY-এ পুরো ভিউয়ের Commit আগে ধার্য হয়, তাই শুধু Private Bytes দিয়ে CoW বিচার করা যায় না।6
  • Cache Manager-এর ক্যাশড I/O, ডেটা ম্যাপিং ও ইমেজ ম্যাপিং একই ফাইল স্ট্রিমে আলাদা পথ হিসেবে লাগে যেগুলো যথাক্রমে SharedCacheMap, DataSectionObjectImageSectionObject ব্যবহার করে।5
  • শেয়ার্ড মেমরি নিরাপদ হয় তখনই যখন ভিউ ও হ্যান্ডেলের আয়ু, সিঙ্ক, ACL ও অফসেট নকশা অন্তর্ভুক্ত থাকে।

এতে «Windows মেমরির গভীরতা»-এর তিন পর্ব শেষ। আপনি ভার্চুয়াল ঠিকানা Reserve/Commit করেন, পেজ ফল্ট দিয়ে ভৌত পেজ পান, পেজ Working Set থেকে পেজ তালিকায় নিয়ে যান, সেকশন দিয়ে ভাগ করেন, এবং শুধু লেখা পেজ CoW দিয়ে ভাঙেন — Windows মেমরি ব্যবস্থাপনা এই এক প্রবাহে যুক্ত।

সম্পর্কিত নিবন্ধ

সম্পর্কিত পরামর্শ ক্ষেত্র

KomuraSoft LLC Windows অ্যাপ্লিকেশনের শেয়ার্ড মেমরি, ফাইল ম্যাপিং, DLL লোডিং, ফাইল লক, আন্তঃপ্রক্রিয়া যোগাযোগ ও মেমরি ব্যবহারের ত্রুটি অনুসন্ধান করে।

তথ্যসূত্র

  1. Microsoft Learn, Section Objects and Views. সেকশন অবজেক্ট যে ভাগযোগ্য মেমরি পরিসর প্রতিনিধিত্ব করে এবং প্রতিটি প্রক্রিয়া সেকশনের অংশ ভিউ হিসেবে ম্যাপ করে সে বিষয়ে।  2 3

  2. Microsoft Learn, File-Backed and Page-File-Backed Sections. ফাইল-ব্যাকড ও পেজফাইল-ব্যাকড সেকশন, CoW, এবং ভিন্ন প্রক্রিয়ার ভার্চুয়াল ঠিকানা থেকে একই ভৌত মেমরি ভাগ করা যায় সে বিষয়ে।  2 3 4

  3. Microsoft Learn, CreateFileMappingW function. ফাইল-ম্যাপিং অবজেক্ট, পেজফাইল-ব্যাকড সেকশন, SEC_IMAGE, ভিউ ও হ্যান্ডেলের আয়ু, এবং একই ফাইল ধরা ভিউগুলোর সামঞ্জস্য বিষয়ে।  2 3 4 5 6 7 8

  4. Microsoft Learn, Memory Protection. একাধিক প্রক্রিয়া একই DLL-এর ভৌত পেজ ভাগ করে, এবং এক পক্ষ লিখলে CoW নতুন ভৌত পেজে কপি করে PTE হালনাগাদ করে সে বিষয়ে।  2 3

  5. Microsoft Learn, SECTION_OBJECT_POINTERS structure. DataSectionObject, SharedCacheMap ও ImageSectionObject কীভাবে ফাইল স্ট্রিমের ম্যাপিং ও ক্যাশ তথ্য মেমরি ম্যানেজার / Cache Manager-এর সঙ্গে বাঁধে সে বিষয়ে।  2 3 4

  6. Microsoft Learn, MapViewOfFileEx function. FILE_MAP_COPY-এর CoW; পেজ ফাইল দিয়ে ধরা নিজস্ব পেজ; পুরো ভিউয়ের Commit খরচ; এবং ভার্চুয়াল ঠিকানার বদলে অফসেট রাখা বিষয়ে।  2 3 4 5 6 7 8

  7. Microsoft Learn, VirtualQuery function. CoW-এর পরেও Type MEM_MAPPED/MEM_IMAGE থাকে, এবং QueryWorkingSetEx-এর Shared বিট দিয়ে ব্যক্তিগতকরণ নিশ্চিত করা যায় সে বিষয়ে।  2 3 4

  8. Microsoft Learn, Managing Memory Sections. ভিউ অ্যাক্সেস না হওয়া পর্যন্ত ভৌত মেমরি না দেওয়া, এবং প্রথম অ্যাক্সেসের পেজ ফল্ট যে ফাইল বিষয়বস্তু পড়ে সে বিষয়ে। 

  9. Microsoft Learn, Sharing Files and Memory. নাম বা হ্যান্ডেল দিয়ে একই ফাইল-ম্যাপিং অবজেক্ট ভাগ; INVALID_HANDLE_VALUE দিয়ে পেজফাইল-ব্যাকড শেয়ার্ড মেমরি তৈরি; এবং সিঙ্ক আলাদা প্রয়োজন সে বিষয়ে।  2 3

  10. Microsoft Learn, Process Explorer - Sysinternals. Process Explorer যে প্রক্রিয়ার হ্যান্ডেল ও লোড করা DLL / মেমরি-ম্যাপ ফাইল দেখাতে পারে সে বিষয়ে। 

  11. Microsoft Learn, VMMap - Sysinternals. VMMap যে প্রক্রিয়ার ভার্চুয়াল মেমরি Image, Mapped File, Private ইত্যাদিতে ভাঙে এবং Working Set-এর Private/Shareable ভাঙন দেখায় সে বিষয়ে। 

কাছাকাছি বিষয়ে গভীরে যেতে একই ট্যাগযুক্ত সাম্প্রতিক নিবন্ধ।

DllMain ও লোডার লক — DLL ইনিশিয়ালাইজেশনে "কিছুই করবেন না" বলা হয় তার আসল কারণ

DllMain থেকে LoadLibrary ডাকা বা অন্য থ্রেডের সাথে সিঙ্ক্রোনাইজ করা যায় না কেন। প্রাথমিক উৎস থেকে এই নিবন্ধ ব্যাখ্যা করে লোডার লক প্রতিট...

Windows মেমরির গভীরতা (পর্ব ২) — একটি ভৌত পেজের জীবন: পাঁচটি তালিকা ও পেজ ফাইলের সত্য

এই নিবন্ধ PFN ডেটাবেস, Standby, Modified, মেমরি সংকোচন ও পেজ ফাইল জুড়ে Working Set ছাড়ার পর একটি ভৌত পেজ কোথায় যায় তা ব্যাখ্যা করে।

Windows ভার্চুয়ালাইজেশনের গভীরতা (পর্ব ৩) — সেকেন্ডে বুট হওয়া ভার্চুয়াল মেশিন: WSL2, Windows Sandbox ও কন্টেইনার এত হালকা কেন

WSL2 ও Windows Sandbox সেকেন্ডে শুরু হয়ে এত হালকা মনে হয় কেন? এই নিবন্ধ ডায়নামিক বেস ইমেজ ও ডাইরেক্ট ম্যাপ থেকে ডায়নামিক মেমরি বরাদ্দ...

Windows ভার্চুয়ালাইজেশনের গভীরতা (পর্ব ২) — কার্নেলও দেখতে পায় না এমন মেমরি: VBS, HVCI ও Credential Guard কীভাবে কাজ করে

সামঞ্জস্যপূর্ণ হার্ডওয়্যারে ক্লিন ইনস্টলে VBS ডিফল্টে চালু থাকে এবং হাইপারভাইজার ও SLAT দিয়ে কার্নেলের চেয়ে শক্তিশালী আইসোলেশন তৈরি কর...

Windows ভার্চুয়ালাইজেশনের গভীরতা (পর্ব ১) — আপনার Windows আসলে কোথায় চলছে? হাইপারভাইজার ও পার্টিশন

Hyper-V চালু করলে হোস্ট Windows নিজেই রুট পার্টিশন হিসেবে হাইপারভাইজারের উপর চলে। এই নিবন্ধ VT-x, SLAT ও VMBus-এর ভূমিকা দিয়ে ভার্চুয়াল...

এই পৃষ্ঠাগুলো বিষয়টিকে সেবা ও সিদ্ধান্তের বৃহত্তর প্রেক্ষাপটে স্থাপন করে।

নিবন্ধটি নিচের সেবাগুলোর সাথে সরাসরি সম্পর্কিত।

প্রায়শ জিজ্ঞাসিত প্রশ্ন

এই নিবন্ধের বিষয়ে পরামর্শে প্রায়ই আসা প্রশ্ন।

একই DLL ব্যবহার করা প্রতিটি প্রক্রিয়া কি RAM-এ সেই DLL-এর পূর্ণ কপি পায়?
সাধারণত না। একই ইমেজের অপরিবর্তিত পেজ প্রতিটি প্রক্রিয়ার ভিন্ন ভার্চুয়াল ঠিকানা থেকে একই ভৌত পেজে ম্যাপ হয়। শুধু যে পেজ লিখতে হয় সেগুলো কপি-অন-রাইট ইত্যাদির মাধ্যমে প্রক্রিয়া-নিজস্ব ভৌত পেজ হয়।
CreateFileMapping কি সেই মুহূর্তে প্রক্রিয়াকে মেমরি বরাদ্দ করে?
CreateFileMapping ফাইল-ম্যাপিং অবজেক্ট তৈরি করে, কিন্তু প্রক্রিয়ার ভার্চুয়াল স্পেসে দেখায় MapViewOfFile। আরও, ভিউয়ের ভৌত পেজ সাধারণত প্রথম অ্যাক্সেস করা পেজ থেকে পেজ ফল্টে বাস্তবায়িত হয়।
FILE_MAP_WRITE ও FILE_MAP_COPY-এর পার্থক্য কী?
FILE_MAP_WRITE-এর পরিবর্তন শেয়ার্ড ফাইল-ডেটা দিকে প্রতিফলিত লেখা। FILE_MAP_COPY প্রাথমিক পেজ ভাগ করে, কিন্তু শুধু লেখা পেজ প্রক্রিয়া-নিজস্ব কপি হয়; পরিবর্তন মূল ফাইলে ফেরত লেখা হয় না এবং ভিউ আনম্যাপ হলে হারিয়ে যায়।
কপি-অন-রাইটের পর VirtualQuery কি MEM_PRIVATE ফেরায়?
না। ডেটা ভিউ MEM_MAPPED এবং ইমেজ ভিউ MEM_IMAGE থাকে। পেজ সত্যিই ব্যক্তিগত হয়েছে কি না দেখতে পেজ রেসিডেন্ট করে QueryWorkingSetEx-এর Shared বিট দেখুন।
শেয়ার্ড মেমরিতে কাঁচা পয়েন্টার রাখা যায়?
সাধারণত উচিত নয়। একই সেকশন হলেও প্রতিটি প্রক্রিয়ার ভিউ একই ভার্চুয়াল ঠিকানায় বসবে এমন নিশ্চয়তা নেই। শেয়ার্ড কাঠামোতে বেস থেকে অফসেট, নির্দিষ্ট-প্রস্থ পূর্ণসংখ্যা এবং স্পষ্ট লেআউট ও সিঙ্ক স্কিম ব্যবহার করুন।

লেখকের প্রোফাইল

নিবন্ধের লেখকের পরিচিতি পৃষ্ঠা।

Go Komura

KomuraSoft LLC-এর প্রতিনিধি

Windows সফটওয়্যার ডেভেলপমেন্ট, প্রযুক্তিগত পরামর্শ ও বাগ তদন্তে বিশেষজ্ঞ, বিশেষ করে বিদ্যমান সিস্টেমযুক্ত প্রকল্প ও পুনরুৎপাদন করা কঠিন বাগে।

পাবলিক লিঙ্ক

ব্লগে ফিরে যান