فصل ۱۵: File و Directory، Path، امنیت سیستمعامل و Memory-Mapped File
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
Operationهای File و Directory
Namespace با نام System.IO مجموعهای از Typeها برای انجام Operationهای «Utility» روی File و Directory ارائه میدهد؛ مانند Copy و Move، ساخت Directory و تنظیم Attributeها و Permissionهای File. برای بیشتر Featureها میتوانید یکی از دو Class را انتخاب کنید: یکی با Methodهای استاتیک و دیگری با Methodهای Instance.
- Classهای استاتیک
File و Directory- Classهای Instance-method که با نام File یا Directory ساخته میشوند
FileInfo و DirectoryInfo
علاوه بر این، Class استاتیک Path وجود دارد. این Class کاری روی File یا Directory انجام نمیدهد؛ در عوض Methodهای String Manipulation برای Filename و Directory Path فراهم میکند. Path همچنین در کار با Temporary Fileها کمک میکند.
Class با نام File
File یک Class استاتیک است و همهٔ Methodهای آن Filename میپذیرند. Filename میتواند Relative به Current Directory یا Fully Qualified همراه با Directory باشد. Methodهای آن ــ همگی Public و Static ــ عبارتاند از:
bool Exists (string path); // Returns true if the file is present
void Delete (string path);
void Copy (string sourceFileName, string destFileName);
void Move (string sourceFileName, string destFileName);
void Replace (string sourceFileName, string destinationFileName,
string destinationBackupFileName);
FileAttributes GetAttributes (string path);
void SetAttributes (string path, FileAttributes fileAttributes);
void Decrypt (string path);
void Encrypt (string path);
DateTime GetCreationTime (string path); // UTC versions are
DateTime GetLastAccessTime (string path); // also provided.
DateTime GetLastWriteTime (string path);
void SetCreationTime (string path, DateTime creationTime);
void SetLastAccessTime (string path, DateTime lastAccessTime);
void SetLastWriteTime (string path, DateTime lastWriteTime);
FileSecurity GetAccessControl (string path);
FileSecurity GetAccessControl (string path,
AccessControlSections includeSections);
void SetAccessControl (string path, FileSecurity fileSecurity);
Move اگر Destination File از قبل وجود داشته باشد Exception پرتاب میکند؛ Replace چنین نمیکند. هر دو Method اجازه میدهند File هم Rename و هم به Directory دیگر Move شود.
Delete اگر File با Attribute از نوع Read-only علامتگذاری شده باشد UnauthorizedAccessException پرتاب میکند؛ میتوانید با GetAttributes پیشاپیش متوجه شوید. اگر OS اجازهٔ Delete آن File را به Process شما ندهد نیز همین Exception پرتاب میشود. همهٔ Memberهای Enum با نام FileAttribute که GetAttributes برمیگرداند:
Archive, Compressed, Device, Directory, Encrypted,
Hidden, IntegritySystem, Normal, NoScrubData, NotContentIndexed,
Offline, ReadOnly, ReparsePoint, SparseFile, System, Temporary
Memberهای این Enum قابل ترکیباند. برای Toggle کردن یک File Attribute بدون بههمزدن بقیه:
string filePath = "test.txt";
FileAttributes fa = File.GetAttributes (filePath);
if ((fa & FileAttributes.ReadOnly) != 0)
{
// Use the exclusive-or operator (^) to toggle the ReadOnly flag
fa ^= FileAttributes.ReadOnly;
File.SetAttributes (filePath, fa);
}
// Now we can delete the file, for instance:
File.Delete (filePath);
FileInfo راه سادهتری برای تغییر Read-only Flag یک File دارد:
new FileInfo ("test.txt").IsReadOnly = false;
Attributeهای Compression و Encryption
Attributeهای Compressed و Encrypted متناظر Checkboxهای Compression و Encryption در Dialog با نام Properties یک File یا Directory در Windows Explorer هستند. این نوع Compression و Encryption شفاف است، چون OS همهٔ کار را در پشت صحنه انجام میدهد و شما Plain Data را Read و Write میکنید.
نمیتوانید با SetAttributes، Attributeهای Compressed یا Encrypted File را تغییر دهید؛ اگر تلاش کنید بیصدا Fail میشود. برای Encryption راهحل ساده است: بهجای آن Encrypt() و Decrypt() را در Class با نام File Call میکنید. برای Compression پیچیدهتر است؛ یک راه استفاده از Windows Management Instrumentation یا WMI API در System.Management است. Method زیر Directory را Compress میکند و در صورت Success مقدار 0 و در غیر این صورت WMI Error Code برمیگرداند:
static uint CompressFolder (string folder, bool recursive)
{
string path = "Win32_Directory.Name='" + folder + "'";
using (ManagementObject dir = new ManagementObject (path))
using (ManagementBaseObject p = dir.GetMethodParameters ("CompressEx"))
{
p ["Recursive"] = recursive;
using (ManagementBaseObject result = dir.InvokeMethod ("CompressEx",
p, null))
return (uint) result.Properties ["ReturnValue"].Value;
}
}
برای Uncompress، CompressEx را با UncompressEx جایگزین کنید.
Transparent Encryption به Keyای متکی است که Seed آن از Password کاربر Loginشده میآید. System در برابر Password Changeای که توسط خود User احراز هویتشده انجام شود Robust است؛ اما اگر Password توسط Administrator Reset شود، Data داخل Fileهای Encrypted دیگر قابل Recovery نیست.
با Win32 Interop میتوانید مشخص کنید Volume از Compression و Encryption پشتیبانی میکند یا نه:
using System;
using System.IO;
using System.Text;
using System.ComponentModel;
using System.Runtime.InteropServices;
class SupportsCompressionEncryption
{
const int SupportsCompression = 0x10;
const int SupportsEncryption = 0x20000;
[DllImport ("Kernel32.dll", SetLastError = true)]
extern static bool GetVolumeInformation (string vol, StringBuilder name,
int nameSize, out uint serialNum, out uint maxNameLen, out uint flags,
StringBuilder fileSysName, int fileSysNameSize);
static void Main()
{
uint serialNum, maxNameLen, flags;
bool ok = GetVolumeInformation (@"C:\", null, 0, out serialNum,
out maxNameLen, out flags, null, 0);
if (!ok)
throw new Win32Exception();
bool canCompress = (flags & SupportsCompression) != 0;
bool canEncrypt = (flags & SupportsEncryption) != 0;
}
}
امنیت File در Windows
Class با نام FileSecurity اجازه میدهد Permissionهای OS اختصاصدادهشده به Userها و Roleها را Query و تغییر دهید؛ Namespace با نام System.Security.AccessControl.
در مثال زیر Permissionهای موجود یک File را List میکنیم و سپس Permission از نوع Write را به Group با نام «Users» میدهیم:
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
void ShowSecurity (FileSecurity sec)
{
AuthorizationRuleCollection rules = sec.GetAccessRules (true, true,
typeof (NTAccount));
foreach (FileSystemAccessRule r in rules.Cast<FileSystemAccessRule>()
.OrderBy (rule => rule.IdentityReference.Value))
{
// e.g., MyDomain/Joe
Console.WriteLine ($" {r.IdentityReference.Value}");
// Allow or Deny: e.g., FullControl
Console.WriteLine ($" {r.FileSystemRights}: {r.AccessControlType}");
}
}
var file = "sectest.txt";
File.WriteAllText (file, "File security test.");
var sid = new SecurityIdentifier (WellKnownSidType.BuiltinUsersSid, null);
string usersAccount = sid.Translate (typeof (NTAccount)).ToString();
Console.WriteLine ($"User: {usersAccount}");
FileSecurity sec = new FileSecurity (file,
AccessControlSections.Owner |
AccessControlSections.Group |
AccessControlSections.Access);
Console.WriteLine ("AFTER CREATE:");
ShowSecurity(sec); // BUILTIN\Users doesn't have Write permission
sec.ModifyAccessRule (AccessControlModification.Add,
new FileSystemAccessRule (usersAccount, FileSystemRights.Write,
AccessControlType.Allow),
out bool modified);
Console.WriteLine ("AFTER MODIFY:");
ShowSecurity (sec); // BUILTIN\Users has Write permission
مثال دیگری را بعداً در «Special Folders» صفحهٔ 730 ارائه میکنیم.
امنیت File در Unix
از .NET 7، Class با نام File Methodهای GetUnixFileMode و SetUnixFileMode را برای Get و Set کردن File Permission در Unix دارد. Method با نام Directory.CreateDirectory نیز حالا Overloadی دارد که Unix File Mode میپذیرد و هنگام Create کردن File میتوان File Mode را چنین مشخص کرد:
var fs = new FileStream ("test.txt",
new FileStreamOptions
{
Mode = FileMode.Create,
UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite
});
Class با نام Directory
Class استاتیک Directory مجموعهای از Methodها مشابه Class با نام File دارد: بررسی وجود Directory با Exists، Move با Move، Delete با Delete، Get/Set کردن Creation Time یا Last Access و Get/Set کردن Security Permission. علاوه بر آن، Directory Methodهای استاتیک زیر را ارائه میدهد:
string GetCurrentDirectory ();
void SetCurrentDirectory (string path);
DirectoryInfo CreateDirectory (string path);
DirectoryInfo GetParent (string path);
string GetDirectoryRoot (string path);
string[] GetLogicalDrives(); // Gets mount points on Unix
// The following methods all return full paths:
string[] GetFiles (string path);
string[] GetDirectories (string path);
string[] GetFileSystemEntries (string path);
IEnumerable<string> EnumerateFiles (string path);
IEnumerable<string> EnumerateDirectories (string path);
IEnumerable<string> EnumerateFileSystemEntries (string path);
Methodهای Enumerate* و Get* Overloadهایی نیز دارند که Parameterهای searchPattern از نوع String و searchOption از نوع Enum میپذیرند. اگر SearchOption.SearchAllSubDirectories را مشخص کنید Search بازگشتی Subdirectory انجام میشود. Methodهای *FileSystemEntries Result مربوط به *Files را با *Directories ترکیب میکنند.
روش ساخت Directory اگر از قبل وجود نداشته باشد:
if (!Directory.Exists (@"d:\test"))
Directory.CreateDirectory (@"d:\test");
FileInfo و DirectoryInfo
Methodهای استاتیک File و Directory برای اجرای یک Operation منفرد File یا Directory راحتاند. اگر لازم است Seriesی از Methodها را پشتسرهم Call کنید، Classهای FileInfo و DirectoryInfo یک Object Model ارائه میکنند که کار را سادهتر میکند.
FileInfo بیشتر Methodهای استاتیک File را بهصورت Instance ارائه میدهد، همراه با Propertyهای اضافه مانند Extension، Length، IsReadOnly و Directory که Object از نوع DirectoryInfo برمیگرداند. مثال:
static string TestDirectory =>
RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
? @"C:\Temp"
: "/tmp";
Directory.CreateDirectory (TestDirectory);
FileInfo fi = new FileInfo (Path.Combine (TestDirectory, "FileInfo.txt"));
Console.WriteLine (fi.Exists); // false
using (TextWriter w = fi.CreateText())
w.Write ("Some text");
Console.WriteLine (fi.Exists); // false (still)
fi.Refresh();
Console.WriteLine (fi.Exists); // true
Console.WriteLine (fi.Name); // FileInfo.txt
Console.WriteLine (fi.FullName); // c:\temp\FileInfo.txt (Windows)
// /tmp/FileInfo.txt (Unix)
Console.WriteLine (fi.DirectoryName); // c:\temp (Windows)
// /tmp (Unix)
Console.WriteLine (fi.Directory.Name); // temp
Console.WriteLine (fi.Extension); // .txt
Console.WriteLine (fi.Length); // 9
fi.Encrypt();
fi.Attributes ^= FileAttributes.Hidden; // (Toggle hidden flag)
fi.IsReadOnly = true;
Console.WriteLine (fi.Attributes); // ReadOnly,Archive,Hidden,Encrypted
Console.WriteLine (fi.CreationTime); // 3/09/2019 1:24:05 PM
fi.MoveTo (Path.Combine (TestDirectory, "FileInfoX.txt"));
DirectoryInfo di = fi.Directory;
Console.WriteLine (di.Name); // temp or tmp
Console.WriteLine (di.FullName); // c:\temp or /tmp
Console.WriteLine (di.Parent.FullName); // c:\ or /
di.CreateSubdirectory ("SubFolder");
روش استفاده از DirectoryInfo برای Enumerate کردن Fileها و Subdirectoryها:
DirectoryInfo di = new DirectoryInfo (@"e:\photos");
foreach (FileInfo fi in di.GetFiles ("*.jpg"))
Console.WriteLine (fi.Name);
foreach (DirectoryInfo subDir in di.GetDirectories())
Console.WriteLine (subDir.FullName);
Path
Class استاتیک Path Methodها و Fieldهایی برای کار با Path و Filename تعریف میکند.
با فرض کد Setup زیر:
string dir = @"c:\mydir"; // or /mydir
string file = "myfile.txt";
string path = @"c:\mydir\myfile.txt"; // or /mydir/myfile.txt
Directory.SetCurrentDirectory (@"k:\demo"); // or /demo
میتوان Methodها و Fieldهای Path را با Expressionهای زیر نشان داد:
Expressionهای Path و Result در Windows سپس Unix| Expression | Result |
Directory.GetCurrentDirectory() | k:\demo\ یا /demo |
Path.IsPathRooted (file) | False |
Path.IsPathRooted (path) | True |
Path.GetPathRoot (path) | c:\ یا / |
Path.GetDirectoryName (path) | c:\mydir یا /mydir |
Path.GetFileName (path) | myfile.txt |
Path.GetFullPath (file) | k:\demo\myfile.txt یا /demo/myfile.txt |
Path.Combine (dir, file) | c:\mydir\myfile.txt یا /mydir/myfile.txt |
| File extensions |
Path.HasExtension (file) | True |
Path.GetExtension (file) | .txt |
Path.GetFileNameWithoutExtension (file) | myfile |
Path.ChangeExtension (file, ".log") | myfile.log |
| Separators and characters |
Path.DirectorySeparatorChar | \ یا / |
ادامهٔ Expressionهای Path و Temporary Fileها| Expression | Result در Windows سپس Unix |
Path.AltDirectorySeparatorChar | / |
Path.PathSeparator | ; یا : |
Path.VolumeSeparatorChar | : یا / |
Path.GetInvalidPathChars() | Characterهای 0 تا 31 و Characterهای نامعتبر مخصوص OS |
Path.GetInvalidFileNameChars() | Characterهای 0 تا 31 و Characterهای نامعتبر Filename مخصوص OS |
| Temporary files |
Path.GetTempPath() | <local user folder>\Temp یا /tmp/ |
Path.GetRandomFileName() | d2dwuzjf.dnp |
Path.GetTempFileName() | <local user folder>\Temp\tmp14B.tmp یا /tmp/tmpubSUYO.tmp |
Combine بسیار مفید است: اجازه میدهد Directory و Filename یا دو Directory را بدون بررسی دستی وجود Trailing Path Separator ترکیب کنید و Separator صحیح OS را خودکار به کار میبرد. Overloadهایی دارد که تا چهار Directory و/یا Filename میپذیرند.
GetFullPath Path Relative به Current Directory را به Absolute Path تبدیل میکند و Valueهایی مانند ..\..\file.txt را میپذیرد.
GetRandomFileName یک Filename واقعاً Unique با Format 8.3 برمیگرداند بدون اینکه Fileای بسازد. GetTempFileName Temporary Filename را با Counter افزایشی تولید میکند که هر 65,000 File تکرار میشود؛ سپس File صفر Byte با همان Name در Local Temporary Directory میسازد.
Folderهای ویژه (Special Folders)
چیزی که در Path و Directory نیست راهی برای پیدا کردن Folderهایی مثل My Documents، Program Files و Application Data است. این قابلیت توسط Method با نام GetFolderPath در Class با نام System.Environment ارائه میشود:
string myDocPath = Environment.GetFolderPath
(Environment.SpecialFolder.MyDocuments);
Environment.SpecialFolder یک Enum است که Valueهای آن همهٔ Directoryهای ویژه Windows را پوشش میدهد؛ مانند AdminTools، ApplicationData، Fonts، History، SendTo، StartMenu و غیره. تنها Directory مربوط به .NET Runtime اینجا نیست که میتوانید چنین بگیرید:
System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()
روی Windows، ApplicationData ارزش ویژهای دارد؛ Settingsی که در صورت فعال بودن Roaming Profile روی Network Domain همراه User جابهجا میشوند را میتوانید آنجا Store کنید. LocalApplicationData برای Data غیر Roaming و مخصوص User Loginشده است و CommonApplicationData بین همهٔ Userهای Computer Shared میشود. نوشتن Application Data در این Folderها نسبت به استفاده از Windows Registry ترجیح داده میشود. Protocol استاندارد این است که Subdirectoryای با Name Application خود بسازید:
string localAppDataPath = Path.Combine (
Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData),
"MyCoolApplication");
if (!Directory.Exists (localAppDataPath))
Directory.CreateDirectory (localAppDataPath);
در استفاده از CommonApplicationData یک دام بد وجود دارد: اگر User برنامه را با Administrative Elevation Start کند و Program Folder و File در CommonApplicationData بسازد، همان User ممکن است بعداً هنگام اجرا با Login محدود Windows Permission لازم برای Replace کردن آن Fileها را نداشته باشد. مشکل مشابه هنگام جابهجایی بین Accountهای دارای Permission محدود رخ میدهد. Workaround این است که Folder مورد نظر را با Permission برای Everyone در مرحله Setup بسازید.
محل دیگر برای Configuration و Log File، Base Directory Application است که با AppDomain.CurrentDomain.BaseDirectory میگیرید. با این حال توصیه نمیشود، چون پس از Installation اولیه احتمال دارد OS بدون Administrative Elevation، Permission Write به این Folder را به Application ندهد.
Query کردن اطلاعات Volume
با Class با نام DriveInfo میتوانید Driveهای Computer را Query کنید:
DriveInfo c = new DriveInfo ("C"); // Query the C: drive.
// On Unix: /
long totalSize = c.TotalSize; // Size in bytes.
long freeBytes = c.TotalFreeSpace; // Ignores disk quotas.
long freeToMe = c.AvailableFreeSpace; // Takes quotas into account.
foreach (DriveInfo d in DriveInfo.GetDrives()) // All defined drives.
// On Unix: mount points
{
Console.WriteLine (d.Name); // C: Console.WriteLine (d.DriveType); // Fixed
Console.WriteLine (d.RootDirectory); // C: if (d.IsReady) // If the drive is not ready, the following two
// properties will throw exceptions:
{
Console.WriteLine (d.VolumeLabel); // The Sea Drive
Console.WriteLine (d.DriveFormat); // NTFS
}
}
Method استاتیک GetDrives همهٔ Driveهای Mapشده را برمیگرداند، شامل CD-ROM، Media Card و Network Connection. DriveType یک Enum با Valueهای زیر است:
Unknown, NoRootDirectory, Removable, Fixed, Network, CDRom, Ram
گرفتن Eventهای Filesystem
Class با نام FileSystemWatcher اجازه میدهد یک Directory و در صورت نیاز Subdirectoryهایش را برای Activity Monitor کنید. Eventهایی دارد که هنگام Create، Modify، Rename یا Delete شدن Fileها یا Subdirectoryها و نیز تغییر Attributeهای آنها Fire میشوند. این Eventها بدون توجه به User یا Process انجامدهندهٔ Change Fire میشوند.
Watch (GetTestDirectory(), "*.txt", true);
void Watch (string path, string filter, bool includeSubDirs)
{
using (var watcher = new FileSystemWatcher (path, filter))
{
watcher.Created += FileCreatedChangedDeleted;
watcher.Changed += FileCreatedChangedDeleted;
watcher.Deleted += FileCreatedChangedDeleted;
watcher.Renamed += FileRenamed;
watcher.Error += FileError;
watcher.IncludeSubdirectories = includeSubDirs;
watcher.EnableRaisingEvents = true;
Console.WriteLine ("Listening for events - press <enter> to end");
Console.ReadLine();
}
// Disposing the FileSystemWatcher stops further events from firing.
}
void FileCreatedChangedDeleted (object o, FileSystemEventArgs e)
=> Console.WriteLine ("File {0} has been {1}", e.FullPath, e.ChangeType);
void FileRenamed (object o, RenamedEventArgs e)
=> Console.WriteLine ("Renamed: {0}->{1}", e.OldFullPath, e.FullPath);
void FileError (object o, ErrorEventArgs e)
=> Console.WriteLine ("Error: " + e.GetException().Message);
string GetTestDirectory() =>
RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
? @"C:\Temp"
: "/tmp";
Event با نام Error دربارهٔ Filesystem Error اطلاع نمیدهد؛ در عوض نشان میدهد Event Buffer مربوط به FileSystemWatcher بهخاطر حجم بیش از حد Eventهای Changed، Created، Deleted یا Renamed Overflow شده است. Size Buffer را با Property با نام InternalBufferSize میتوانید تغییر دهید.
IncludeSubdirectories بهصورت Recursive اعمال میشود. پس اگر روی C:\ یک FileSystemWatcher با IncludeSubdirectories=true بسازید، Eventهای آن با Change شدن File یا Directory در هر جای Hard Drive Fire میشوند.
امنیت سیستمعامل (OS Security)
همهٔ Applicationها براساس Login Privilege کاربر تحت Restrictionهای OS هستند. این Restrictionها علاوه بر File I/O روی Capabilityهای دیگر، مانند دسترسی به Windows Registry، اثر میگذارند.
در Windows و Unix دو نوع Account وجود دارد:
- Administrative/Superuser Account که برای Access به Computer محلی Restriction اعمال نمیکند.
- Account با Permission محدود که Functionهای Administrative و Visibility به Data سایر Userها را محدود میکند.
در Windows، Feature با نام User Account Control یا UAC باعث میشود Administrator هنگام Login دو Token یا «Hat» بگیرد: Hat اداری و Hat User معمولی. بهطور پیشفرض Programها با Hat User معمولی و Permission محدود اجرا میشوند، مگر اینکه Program Administrative Elevation درخواست کند. سپس User باید Request را در Dialog Box تأیید کند.
در Unix، Userها معمولاً با Account محدود Login میکنند. Administratorها نیز برای کاهش احتمال Damage ناخواسته System همین کار را میکنند. وقتی User نیاز دارد Commandی با Permission بالاتر اجرا کند، پیش از Command از sudo ــ مخفف «super-user do» ــ استفاده میکند.
بهطور پیشفرض Application شما با Restricted User Privilege اجرا میشود. بنابراین باید یکی از این دو راه را انتخاب کنید:
- Application را طوری بنویسید که بدون Administrative Privilege اجرا شود.
- در Application Manifest درخواست Administrative Elevation کنید ــ فقط Windows ــ یا نبود Privilege لازم را Detect و به User اعلام کنید Application را بهعنوان Administrator/Superuser Restart کند.
Option اول امنتر و برای User راحتتر است. طراحی Program برای اجرا بدون Administrative Privilege در بیشتر موارد آسان است.
میتوانید بفهمید تحت Administrative Account اجرا میشوید یا نه:
[DllImport("libc")]
public static extern uint getuid();
static bool IsRunningAsAdmin()
{
if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal (identity);
return principal.IsInRole (WindowsBuiltInRole.Administrator);
}
return getuid() == 0;
}
با فعال بودن UAC در Windows، فقط وقتی Process فعلی Administrative Elevation دارد مقدار true برمیگردد. در Linux فقط وقتی Process فعلی بهعنوان Superuser، مثلاً sudo myapp، اجرا میشود true میدهد.
اجرا در Standard User Account
کارهای کلیدیای که در Standard User Account نمیتوانید انجام دهید:
- Write در Directoryهای زیر:
- OS Folder ــ معمولاً
\Windows یا /bin، /sbin و ... ــ و Subdirectoryهای آن. - Program Files Folder ــ
\Program Files یا /usr/bin، /opt ــ و Subdirectoryها. - Root مربوط به OS Drive، مانند
C:\ یا /.
- Write در Branch با نام
HKEY_LOCAL_MACHINE از Registry در Windows. - Read کردن Performance Monitoring Data با WMI در Windows.
علاوه بر این، بهعنوان Windows User معمولی ــ یا حتی Administrator ــ ممکن است Access به File یا Resource متعلق به Userهای دیگر رد شود. Windows برای محافظت از این Resourceها از Access Control List یا ACL استفاده میکند؛ میتوانید با Typeهای System.Security.AccessControl Rights خود را در ACL Query و Assert کنید.
ACLها همچنین میتوانند روی Cross-process Wait Handleها اعمال شوند که در فصل 21 توضیح داده میشوند.
اگر Access بهدلیل OS Security رد شود، CLR Failure را Detect و بهجای Fail خاموش، UnauthorizedAccessException پرتاب میکند.
در بیشتر موارد میتوانید Restrictionهای Standard User را چنین مدیریت کنید:
- Fileها را در Locationهای توصیهشده بنویسید.
- برای Informationای که میتوان در File ذخیره کرد از Registry استفاده نکنید؛ بهجز Hive با نام
HKEY_CURRENT_USER که فقط در Windows Access Read/Write به آن دارید. - ActiveX یا COM Componentها را هنگام Setup Register کنید ــ فقط Windows.
Location توصیهشده برای User Documentها، SpecialFolder.MyDocuments است:
string docsFolder = Environment.GetFolderPath
(Environment.SpecialFolder.MyDocuments);
string path = Path.Combine (docsFolder, "test.txt");
Location توصیهشده برای Configuration Fileهایی که User ممکن است بیرون از Application تغییر دهد، SpecialFolder.ApplicationData فقط برای Current User یا SpecialFolder.CommonApplicationData برای همهٔ Userهاست. معمولاً بر اساس Organization و Product Name خود داخل این Folderها Subdirectory میسازید.
Administrative Elevation و Virtualization
با Application Manifest میتوانید از Windows بخواهید هر بار Program اجرا شد از User درخواست Administrative Elevation کند؛ Linux این Request را Ignore میکند:
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
Application Manifest را در فصل 17 با جزئیات بیشتر توضیح میدهیم.
اگر requireAdministrator را با asInvoker جایگزین کنید، به Windows میگوید Administrative Elevation لازم نیست. Effect تقریباً مانند نداشتن Application Manifest است، جز اینکه Virtualization Disable میشود. Virtualization یک Measure موقت بود که با Windows Vista معرفی شد تا Applicationهای قدیمی بدون Administrative Privilege درست اجرا شوند. نبود Application Manifest دارای Element با نام requestedExecutionLevel این Feature Backward Compatibility را فعال میکند.
Virtualization زمانی وارد عمل میشود که Application در Directory با نام Program Files یا Windows، یا Area با نام HKEY_LOCAL_MACHINE در Registry Write کند. بهجای پرتاب Exception، Changeها به Location جداگانه روی Hard Disk Redirect میشوند؛ جایی که نمیتوانند روی Data اصلی اثر بگذارند. این کار مانع دخالت Application در OS یا Applicationهای خوشرفتار دیگر میشود.
Memory-Mapped Fileها
Memory-mapped Fileها دو Feature اصلی فراهم میکنند:
- Random Access کارآمد به File Data.
- قابلیت Share کردن Memory بین Processهای مختلف روی همان Computer.
Typeهای Memory-mapped File در Namespace با نام System.IO.MemoryMappedFiles قرار دارند. در داخل، API مربوط به Memory-mapped File در Operating System را Wrap میکنند.
Memory-Mapped File و Random File I/O
هرچند FileStream معمولی Random File I/O را با تنظیم Property با نام Position ممکن میکند، برای Sequential I/O Optimize شده است. بهعنوان Rule of Thumb تقریبی:
FileStream برای Sequential I/O تقریباً 10 برابر سریعتر از Memory-mapped File است.- Memory-mapped File برای Random I/O تقریباً 10 برابر سریعتر از
FileStream است.
تغییر Position در FileStream میتواند چند Microsecond هزینه داشته باشد که اگر در Loop انجام شود جمع میشود. FileStream برای Multithreaded Access نیز مناسب نیست، چون Position آن با Read یا Write تغییر میکند.
برای ساخت Memory-mapped File:
- مثل حالت عادی یک
FileStream بگیرید. - یک
MemoryMappedFile Instantiate و File Stream را به آن بدهید. - روی Object مربوط به Memory-mapped File،
CreateViewAccessor را Call کنید.
Step آخر Object از نوع MemoryMappedViewAccessor میدهد که Methodهایی برای Read و Write تصادفی Typeهای ساده، Structها و Arrayها دارد؛ در «Working with View Accessors» صفحهٔ 738 بیشتر میبینیم.
کد زیر File یکمیلیون Byte میسازد و سپس با Memory-mapped File API یک Byte را در Position برابر 500,000 Write و Read میکند:
File.WriteAllBytes ("long.bin", new byte [1000000]);
using MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile ("long.bin");
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
accessor.Write (500000, (byte) 77);
Console.WriteLine (accessor.ReadByte (500000)); // 77
هنگام Call کردن CreateFromFile میتوانید Map Name و Capacity هم مشخص کنید. Map Name غیر null اجازه میدهد Memory Block با Processهای دیگر Share شود؛ بخش بعد را ببینید. تعیین Capacity نیز File را خودکار به همان Value بزرگ میکند. کد زیر File هزار Byte میسازد:
File.WriteAllBytes ("short.bin", new byte [1]);
using (var mmf = MemoryMappedFile.CreateFromFile
("short.bin", FileMode.Create, null, 1000))
...
Memory-Mapped File و Shared Memory در Windows
در Windows میتوانید از Memory-mapped File بهعنوان وسیلهای برای Share کردن Memory بین Processهای روی همان Computer استفاده کنید. یک Process با MemoryMappedFile.CreateNew Shared Memory Block میسازد و Processهای دیگر با MemoryMappedFile.OpenExisting و همان Name به همان Block Subscribe میکنند. با اینکه هنوز Memory-mapped «File» نامیده میشود، کاملاً داخل Memory قرار دارد و Presence روی Disk ندارد.
کد زیر Shared Memory-mapped File با Size برابر 500 Byte میسازد و Integer با Value برابر 12345 را در Position صفر Write میکند:
using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew ("Demo", 500))
using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
{
accessor.Write (0, 12345);
Console.ReadLine(); // Keep shared memory alive until user hits Enter.
}
کد زیر همان Memory-mapped File را باز و Integer را Read میکند:
// This can run in a separate executable:
using (MemoryMappedFile mmFile = MemoryMappedFile.OpenExisting ("Demo"))
using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
Console.WriteLine (accessor.ReadInt32 (0)); // 12345
Shared Memory بین Processها بهشکل Cross-Platform
هم Windows و هم Unix اجازه میدهند چند Process همان File را Memory-map کنند. باید Care داشته باشید File Sharing Setting مناسب باشد:
static void Writer()
{
var file = Path.Combine (TestDirectory, "interprocess.bin");
File.WriteAllBytes (file, new byte [100]);
using FileStream fs =
new FileStream (file, FileMode.Open, FileAccess.ReadWrite,
FileShare.ReadWrite);
using MemoryMappedFile mmf = MemoryMappedFile
.CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite,
HandleInheritability.None, true);
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
accessor.Write (0, 12345);
Console.ReadLine(); // Keep shared memory alive until user hits Enter.
File.Delete (file);
}
static void Reader()
{
// This can run in a separate executable:
var file = Path.Combine (TestDirectory, "interprocess.bin");
using FileStream fs =
new FileStream (file, FileMode.Open, FileAccess.ReadWrite,
FileShare.ReadWrite);
using MemoryMappedFile mmf = MemoryMappedFile
.CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite,
HandleInheritability.None, true);
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
Console.WriteLine (accessor.ReadInt32 (0)); // 12345
}
static string TestDirectory =>
RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
? @"C:\Test"
: "/tmp";
کار با View Accessorها
فراخوانی CreateViewAccessor روی MemoryMappedFile یک View Accessor میدهد که با آن میتوانید Valueها را در Positionهای تصادفی Read/Write کنید.
Methodهای Read*/Write* Typeهای Numeric، bool و char و همچنین Array و Structهایی که Element یا Field از نوع Value Type دارند میپذیرند. Reference Typeها و Array/Structهایی که Reference Type دارند ممنوعاند، چون نمیتوانند به Unmanaged Memory Map شوند. پس اگر String میخواهید بنویسید باید آن را به Byte Array Encode کنید:
byte[] data = Encoding.UTF8.GetBytes ("This is a test");
accessor.Write (0, data.Length);
accessor.WriteArray (4, data, 0, data.Length);
توجه کنید ابتدا Length را نوشتیم. در نتیجه بعداً میدانیم چند Byte باید Read کنیم:
byte[] data = new byte [accessor.ReadInt32 (0)];
accessor.ReadArray (4, data, 0, data.Length);
Console.WriteLine (Encoding.UTF8.GetString (data)); // This is a test
مثال Read/Write کردن Struct:
struct Data { public int X, Y; }
...
var data = new Data { X = 123, Y = 456 };
accessor.Write (0, ref data);
accessor.Read (0, out data);
Console.WriteLine (data.X + " " + data.Y); // 123 456
Methodهای Read و Write بهطرز شگفتآوری کندند. با دسترسی مستقیم به Unmanaged Memory زیربنایی از طریق Pointer میتوانید Performance بسیار بهتری بگیرید. ادامهٔ مثال:
unsafe
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer (ref pointer);
int* intPointer = (int*) pointer;
Console.WriteLine (*intPointer); // 123
}
finally
{
if (pointer != null)
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
Project باید برای اجازهٔ Unsafe Code تنظیم شود. میتوانید File با نام .csproj را چنین Edit کنید:
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
مزیت Performance در Pointerها هنگام کار با Structureهای بزرگ حتی بیشتر است، چون اجازه میدهد مستقیم با Raw Data کار کنید، نه اینکه با Read/Write Data بین Managed و Unmanaged Memory Copy شود. این موضوع را در فصل 24 بیشتر بررسی میکنیم.