Amin'sTechLab
Open in Telegram
🔬 Embedded Systems | AI | FPGA | PCB Design 🚀 Exploring tech at the edge of innovation 🧠 Founder of AminTechLab 📍Engineering the future – one bit at a time
Show more1 970
Subscribers
-124 hours
No data7 days
+1430 days
Data loading in progress...
Similar Channels
Tags Cloud
Incoming and Outgoing Mentions
---
---
---
---
---
---
Attracting Subscribers
July '26
July '26
+19
in 0 channels
June '26
+27
in 0 channels
Get PRO
May '26
+10
in 0 channels
Get PRO
April '26
+7
in 0 channels
Get PRO
March '26
+1
in 0 channels
Get PRO
February '26
+12
in 0 channels
Get PRO
January '26
+4
in 0 channels
Get PRO
December '25
+28
in 1 channels
Get PRO
November '25
+36
in 1 channels
Get PRO
October '25
+41
in 1 channels
Get PRO
September '25
+56
in 1 channels
Get PRO
August '25
+44
in 0 channels
Get PRO
July '25
+20
in 0 channels
Get PRO
June '25
+15
in 0 channels
Get PRO
May '25
+25
in 1 channels
Get PRO
April '25
+18
in 0 channels
Get PRO
March '25
+30
in 0 channels
Get PRO
February '25
+19
in 0 channels
Get PRO
January '25
+42
in 0 channels
Get PRO
December '24
+34
in 0 channels
Get PRO
November '24
+36
in 0 channels
Get PRO
October '24
+27
in 0 channels
Get PRO
September '24
+17
in 0 channels
Get PRO
August '24
+23
in 0 channels
Get PRO
July '24
+48
in 0 channels
Get PRO
June '24
+39
in 0 channels
Get PRO
May '24
+44
in 0 channels
Get PRO
April '24
+32
in 0 channels
Get PRO
March '24
+60
in 0 channels
Get PRO
February '24
+82
in 1 channels
Get PRO
January '24
+48
in 2 channels
Get PRO
December '23
+30
in 0 channels
Get PRO
November '23
+7
in 0 channels
Get PRO
October '23
+10
in 0 channels
Get PRO
September '23
+7
in 0 channels
Get PRO
August '23
+5
in 0 channels
Get PRO
July '23
+10
in 0 channels
Get PRO
June '23
+7
in 0 channels
Get PRO
May '23
+1 042
in 0 channels
Get PRO
April '23
+5
in 0 channels
Get PRO
March '23
+9
in 0 channels
Get PRO
February '23
+15
in 0 channels
Get PRO
January '23
+14
in 0 channels
Get PRO
December '22
+1
in 0 channels
Get PRO
November '22
+8
in 0 channels
Get PRO
October '22
+11
in 0 channels
Get PRO
September '22
+7
in 0 channels
Get PRO
August '22
+13
in 0 channels
Get PRO
July '22
+14
in 0 channels
Get PRO
June '22
+11
in 0 channels
Get PRO
May '22
+18
in 0 channels
Get PRO
April '22
+14
in 0 channels
Get PRO
March '22
+8
in 0 channels
Get PRO
February '22
+12
in 0 channels
Get PRO
January '22
+12
in 0 channels
Get PRO
December '21
+12
in 0 channels
Get PRO
November '21
+38
in 0 channels
Get PRO
October '21
+5
in 0 channels
Get PRO
September '21
+25
in 0 channels
Get PRO
August '21
+27
in 0 channels
Get PRO
July '21
+554
in 0 channels
Get PRO
June '21
+14
in 0 channels
Get PRO
May '21
+39
in 0 channels
Get PRO
April '21
+58
in 0 channels
Get PRO
March '21
+88
in 0 channels
Get PRO
February '21
+112
in 0 channels
Get PRO
January '21
+692
in 0 channels
Get PRO
December '20
+2 352
in 0 channels
| Date | Subscriber Growth | Mentions | Channels | |
| 12 July | +1 | |||
| 11 July | +2 | |||
| 10 July | 0 | |||
| 09 July | +2 | |||
| 08 July | +2 | |||
| 07 July | 0 | |||
| 06 July | +3 | |||
| 05 July | +1 | |||
| 04 July | +2 | |||
| 03 July | +3 | |||
| 02 July | +2 | |||
| 01 July | +1 |
Channel Posts
💻 یکی از خطرناکترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش دوم)
در بخش قبل دیدیم که اگر یک متغیر در یک عبارت چند بار تغییر کند و ترتیب اجرای این تغییرات مشخص نباشد، برنامه وارد Undefined Behavior میشود.
حالا چند مثال دیگر را بررسی کنیم.
مثال اول:
int i = 0;
i = i++;
بسیاری تصور میکنند مقدار i برابر ۱ خواهد شد.
اما این کد نیز Undefined Behavior است.
مثال دوم:
int i = 3;
printf("%d %d\n", i++, i++);
به نظر شما خروجی چیست؟
3 4یا
4 3یا حتی
4 4واقعیت این است که استاندارد C هیچکدام از این خروجیها را تضمین نمیکند. چون ترتیب ارزیابی آرگومانهای یک تابع در زبان C مشخص نشده است. مثال سوم:
int a = 10; a = a++ + 5;در این مثال نیز مقدار
a همزمان خوانده و تغییر داده میشود، بدون اینکه ترتیب اجرای این عملیات مشخص باشد.
نتیجه؟
باز هم Undefined Behavior.
چرا کامپایلر این کدها را خطا نمیگیرد؟
زیرا استاندارد C فقط مشخص میکند که رفتار این برنامه تعریفنشده است؛ اما کامپایلر را مجبور نمیکند که حتماً برای آن خطا یا حتی Warning صادر کند.
کامپایلر فرض میکند شما قوانین زبان را رعایت کردهاید و بر همان اساس کد را بهینه میکند.
به همین دلیل ممکن است:
gcc main.cیک خروجی مشخص تولید کند. اما همان برنامه با:
gcc main.c -O2یا حتی روی کامپایلر دیگری، رفتار کاملاً متفاوتی داشته باشد. چگونه از این مشکل جلوگیری کنیم؟ به جای نوشتن عبارتهای پیچیده مانند:
printf("%d\n", i++ + ++i);
کد را به چند مرحله ساده تقسیم کنید:
int a = i;
i++;
int b = i;
i++;
printf("%d\n", a + b);
نوشتن چند خط کد بیشتر، همیشه بهتر از تکیه بر رفتاری است که استاندارد C آن را تضمین نمیکند.
جمعبندی
یکی از مهمترین قوانین زبان C این است:
هرگز یک متغیر را بیش از یک بار در یک عبارت تغییر ندهید، مگر اینکه ترتیب اجرای تمام آن عملیات توسط استاندارد C تضمین شده باشد.شاید این نوع کدها امروز روی سیستم شما درست اجرا شوند، اما هیچ تضمینی وجود ندارد که فردا، با یک نسخه جدید از کامپایلر یا فقط با فعال کردن Optimization، همان رفتار را حفظ کنند. به همین دلیل برنامهنویسان حرفهای C و Embedded از نوشتن عبارتهای پیچیدهای که همزمان یک متغیر را چند بار تغییر میدهند، خودداری میکنند. در زبان C، ساده نوشتن فقط خوانایی را افزایش نمیدهد؛ بلکه از ورود برنامه به یکی از خطرناکترین انواع Undefined Behavior نیز جلوگیری میکند. ⚠️ @Amin_TechLab
| 2 | 💻 یکی از خطرناکترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش اول)
اگر سالها با زبان C کار کرده باشید، احتمالاً حداقل یک بار با کدی مواجه شدهاید که روی یک کامپایلر درست کار میکند، اما روی کامپایلر یا سطح Optimization دیگر، نتیجه متفاوتی میدهد.
در بسیاری از موارد، دلیل این رفتار یک مفهوم مهم در استاندارد C است:
Unsequenced Modifications
این مفهوم یکی از رایجترین دلایل Undefined Behavior در زبان C محسوب میشود.
فرض کنید کد زیر را بنویسید:
#include <stdio.h>
int main()
{
int i = 5;
printf("%d\n", i++ + ++i);
return 0;
}
به نظر شما خروجی چیست؟
12 ؟
13 ؟
14 ؟
پاسخ صحیح این است:
هیچ پاسخ درستی وجود ندارد.
طبق استاندارد C، این کد Undefined Behavior است.
چرا؟
بیایید عبارت زیر را بررسی کنیم:
i++ + ++i
در این عبارت، متغیر i دو بار تغییر میکند:
i++
و
++i
اما استاندارد C مشخص نمیکند که کدامیک باید ابتدا اجرا شود.
کامپایلر میتواند:
ابتدا i++ را اجرا کند.
ابتدا ++i را اجرا کند.
حتی بخشی از هر دو را همزمان بهینهسازی کند.
هیچ ترتیب مشخصی وجود ندارد.
Sequence Point چیست؟
در زبان C، بعضی نقاط وجود دارند که استاندارد تضمین میکند تمام عملیات قبلی کامل شدهاند و سپس اجرای بخش بعدی آغاز میشود.
به این نقاط میگویند:
Sequence Point
در استانداردهای جدید C بیشتر از اصطلاحهای Sequenced Before و Unsequenced استفاده میشود، اما هنوز هم بسیاری از منابع آموزشی از عبارت Sequence Point استفاده میکنند.
چه زمانی مشکل ایجاد میشود؟
اگر در یک عبارت:
یک متغیر را بیش از یک بار تغییر دهید.
یا آن را همزمان بخوانید و تغییر دهید.
بدون اینکه ترتیب اجرای این عملیات مشخص باشد.
برنامه وارد Undefined Behavior میشود.
این یعنی استاندارد C دیگر هیچ تضمینی درباره نتیجه اجرای برنامه نمیدهد.
در بخش دوم، چند مثال واقعی دیگر را بررسی میکنیم و خواهیم دید چرا این دسته از باگها فقط با تغییر کامپایلر یا فعال کردن Optimization ظاهر میشوند.
@Amin_TechLab | 91 |
| 3 | 💻حالا همان Mini Shell که در پست قبل با زبان C نوشتیم را با Rust بازنویسی میکنیم.
جالب است بدانید که در Rust دیگر خبری از fork() و execvp() نیست. کتابخانهی استاندارد Rust این جزئیات سطح پایین را پشت یک API ساده مخفی کرده و ما فقط با std::process::Command کار میکنیم.
کد زیر دقیقاً همان Shell سادهی پست قبل را پیادهسازی میکند:
use std::io::{self, Write};
use std::process::Command;
fn main() {
loop {
print!("mini-shell> ");
io::stdout().flush().unwrap();
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
let line = line.trim();
if line.is_empty() {
continue;
}
if line == "exit" {
break;
}
let args: Vec<&str> = line.split_whitespace().collect();
let status = Command::new(args[0])
.args(&args[1..])
.status();
match status {
Ok(status) => {
println!("Exit Status: {}", status);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
}
این برنامه چگونه کار میکند؟
🔹 دریافت ورودی
ابتدا یک خط از کاربر خوانده میشود:
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
🔹 تجزیه دستور
مانند نسخهی C، ورودی بر اساس فاصله جدا میشود:
let args: Vec<&str> =
line.split_whitespace().collect();
اگر کاربر بنویسد:
ls -l /home
بردار args به این شکل خواهد بود:
["ls", "-l", "/home"]
🔹 اجرای برنامه
در این قسمت تفاوت اصلی Rust و C دیده میشود:
Command::new(args[0])
.args(&args[1..])
.status();
کافی است نام برنامه و آرگومانها را مشخص کنیم.
کتابخانهی استاندارد Rust در سیستمهای شبهیونیکس خودش در پشت صحنه عملیات fork → exec → wait را انجام میدهد و نتیجهی اجرای برنامه را به ما برمیگرداند.
به عبارت دیگر، ما همان رفتار Shell را داریم، اما بدون اینکه مستقیماً با fork() یا execvp() کار کنیم.
چرا این کد اینقدر کوتاهتر است؟
در نسخهی C مجبور بودیم:
حافظه را مدیریت کنیم.
رشته را Tokenize کنیم.
fork() را صدا بزنیم.
execvp() را اجرا کنیم.
خطاها را مدیریت کنیم.
با waitpid() منتظر پایان فرآیند بمانیم.
اما در Rust، بیشتر این پیچیدگیها داخل std::process::Command پنهان شدهاند و API سادهتری در اختیار برنامهنویس قرار میگیرد.
آیا Rust واقعاً از fork استفاده نمیکند؟
یکی از سؤالات رایج همین است.
خیر؛ این فقط ظاهر ماجراست.
در لینوکس، زمانی که از Command::status() یا Command::spawn() استفاده میکنید، Rust در نهایت از مکانیزمهای سیستمعامل برای ایجاد فرآیند جدید استفاده میکند. بسته به سیستمعامل و نسخهی آن، این کار ممکن است با fork()، vfork()، posix_spawn() یا مکانیزمهای مشابه انجام شود.
بنابراین Rust چیزی را جایگزین سیستمعامل نکرده است؛ فقط یک Abstraction سطح بالا روی همان APIهای قدیمی یونیکس ساخته تا توسعهدهنده با کد کمتر، خواناتر و ایمنتر همان نتیجه را به دست آورد.
به همین دلیل اگر مفاهیم fork، exec و wait را در C یاد بگیرید، درک رفتار Command در Rust نیز بسیار سادهتر خواهد بود.
@Amin_TechLab | 116 |
| 4 | 💻 ساخت یک Mini Shell در کمتر از ۵۰ خط کد!
اگر بخواهیم سادهترین نسخهی یک Shell را بنویسیم، فقط به چند قابلیت نیاز داریم:
دریافت یک دستور از کاربر
ساخت یک Process جدید (fork)
اجرای برنامه (execvp)
منتظر ماندن تا پایان اجرای برنامه (waitpid)
کد زیر دقیقاً همین کار را انجام میدهد.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
char line[256];
while (1)
{
printf("mini-shell> ");
if (!fgets(line, sizeof(line), stdin))
break;
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "exit") == 0)
break;
char *argv[32];
int argc = 0;
char *token = strtok(line, " ");
while (token && argc < 31)
{
argv[argc++] = token;
token = strtok(NULL, " ");
}
argv[argc] = NULL;
pid_t pid = fork();
if (pid == 0)
{
execvp(argv[0], argv);
perror("execvp");
exit(EXIT_FAILURE);
}
else
{
waitpid(pid, NULL, 0);
}
}
return 0;
}
این کد چگونه کار میکند؟
🔹 fgets()
یک خط از کاربر دریافت میکند.
🔹 strtok()
رشته را بر اساس فاصله به آرگومانهای مختلف تقسیم میکند.
برای مثال:
ls -l /home
به این تبدیل میشود:
argv[0] = "ls"
argv[1] = "-l"
argv[2] = "/home"
🔹 fork()
از فرآیند فعلی یک نسخهی جدید ایجاد میکند.
بعد از این تابع دو Process وجود دارد:
Parent (شل)
Child (برنامهای که قرار است اجرا شود)
🔹 execvp()
فرآیند فرزند را با برنامهی موردنظر جایگزین میکند.
اگر بنویسید:
ls -l
در واقع Child دیگر Mini Shell نیست؛ تبدیل به برنامهی ls میشود.
🔹 waitpid()
شل اصلی منتظر میماند تا اجرای برنامه تمام شود و سپس دوباره Prompt را نمایش میدهد.
این دقیقاً همان چرخهای است که تقریباً تمام Shellهای لینوکس (مانند Bash و Zsh) برای اجرای برنامههای خارجی انجام میدهند:
Read Command
│
▼
Parse Arguments
│
▼
fork()
│
▼
execvp()
│
▼
Program Runs
│
▼
waitpid()
│
▼
Show Prompt Again
این نمونه فقط یک Shell آموزشی است و قابلیتهایی مثل Pipeline (|)، Redirect (>)، متغیرهای محیطی، Wildcard (*)، Job Control، Builtins و Quote Parsing را پیادهسازی نمیکند؛ اما برای درک نحوهی اجرای دستورات در لینوکس، یکی از بهترین نقطههای شروع محسوب میشود.
@Amin_TechLab | 101 |
| 5 | 💻حالا همان Mini Shell که در پست قبل با زبان C نوشتیم را با Rust بازنویسی میکنیم.
جالب است بدانید که در Rust دیگر خبری از fork() و execvp() نیست. کتابخانهی استاندارد Rust این جزئیات سطح پایین را پشت یک API ساده مخفی کرده و ما فقط با std::process::Command کار میکنیم.
کد زیر دقیقاً همان Shell سادهی پست قبل را پیادهسازی میکند:
use std::io::{self, Write};
use std::process::Command;
fn main() {
loop {
print!("mini-shell> ");
io::stdout().flush().unwrap();
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
let line = line.trim();
if line.is_empty() {
continue;
}
if line == "exit" {
break;
}
let args: Vec<&str> = line.split_whitespace().collect();
let status = Command::new(args[0])
.args(&args[1..])
.status();
match status {
Ok(status) => {
println!("Exit Status: {}", status);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
}
این برنامه چگونه کار میکند؟
🔹 دریافت ورودی
ابتدا یک خط از کاربر خوانده میشود:
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
🔹 تجزیه دستور
مانند نسخهی C، ورودی بر اساس فاصله جدا میشود:
let args: Vec<&str> =
line.split_whitespace().collect();
اگر کاربر بنویسد:
ls -l /home
بردار args به این شکل خواهد بود:
["ls", "-l", "/home"]
🔹 اجرای برنامه
در این قسمت تفاوت اصلی Rust و C دیده میشود:
Command::new(args[0])
.args(&args[1..])
.status();
کافی است نام برنامه و آرگومانها را مشخص کنیم.
کتابخانهی استاندارد Rust در سیستمهای شبهیونیکس خودش در پشت صحنه عملیات fork → exec → wait را انجام میدهد و نتیجهی اجرای برنامه را به ما برمیگرداند.
به عبارت دیگر، ما همان رفتار Shell را داریم، اما بدون اینکه مستقیماً با fork() یا execvp() کار کنیم.
چرا این کد اینقدر کوتاهتر است؟
در نسخهی C مجبور بودیم:
حافظه را مدیریت کنیم.
رشته را Tokenize کنیم.
fork() را صدا بزنیم.
execvp() را اجرا کنیم.
خطاها را مدیریت کنیم.
با waitpid() منتظر پایان فرآیند بمانیم.
اما در Rust، بیشتر این پیچیدگیها داخل std::process::Command پنهان شدهاند و API سادهتری در اختیار برنامهنویس قرار میگیرد.
آیا Rust واقعاً از fork استفاده نمیکند؟
یکی از سؤالات رایج همین است.
خیر؛ این فقط ظاهر ماجراست.
در لینوکس، زمانی که از Command::status() یا Command::spawn() استفاده میکنید، Rust در نهایت از مکانیزمهای سیستمعامل برای ایجاد فرآیند جدید استفاده میکند. بسته به سیستمعامل و نسخهی آن، این کار ممکن است با fork()، vfork()، posix_spawn() یا مکانیزمهای مشابه انجام شود.
بنابراین Rust چیزی را جایگزین سیستمعامل نکرده است؛ فقط یک Abstraction سطح بالا روی همان APIهای قدیمی یونیکس ساخته تا توسعهدهنده با کد کمتر، خواناتر و ایمنتر همان نتیجه را به دست آورد.
به همین دلیل اگر مفاهیم fork، exec و wait را در C یاد بگیرید، درک رفتار Command در Rust نیز بسیار سادهتر خواهد بود.
@Amin_TechLab | 8 |
| 6 | 💻 ساخت یک Mini Shell در کمتر از ۵۰ خط کد!
اگر بخواهیم سادهترین نسخهی یک Shell را بنویسیم، فقط به چند قابلیت نیاز داریم:
دریافت یک دستور از کاربر
ساخت یک Process جدید (fork)
اجرای برنامه (execvp)
منتظر ماندن تا پایان اجرای برنامه (waitpid)
کد زیر دقیقاً همین کار را انجام میدهد.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
char line[256];
while (1)
{
printf("mini-shell> ");
if (!fgets(line, sizeof(line), stdin))
break;
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "exit") == 0)
break;
char *argv[32];
int argc = 0;
char *token = strtok(line, " ");
while (token && argc < 31)
{
argv[argc++] = token;
token = strtok(NULL, " ");
}
argv[argc] = NULL;
pid_t pid = fork();
if (pid == 0)
{
execvp(argv[0], argv);
perror("execvp");
exit(EXIT_FAILURE);
}
else
{
waitpid(pid, NULL, 0);
}
}
return 0;
}
این کد چگونه کار میکند؟
🔹 fgets()
یک خط از کاربر دریافت میکند.
🔹 strtok()
رشته را بر اساس فاصله به آرگومانهای مختلف تقسیم میکند.
برای مثال:
ls -l /home
به این تبدیل میشود:
argv[0] = "ls"
argv[1] = "-l"
argv[2] = "/home"
🔹 fork()
از فرآیند فعلی یک نسخهی جدید ایجاد میکند.
بعد از این تابع دو Process وجود دارد:
Parent (شل)
Child (برنامهای که قرار است اجرا شود)
🔹 execvp()
فرآیند فرزند را با برنامهی موردنظر جایگزین میکند.
اگر بنویسید:
ls -l
در واقع Child دیگر Mini Shell نیست؛ تبدیل به برنامهی ls میشود.
🔹 waitpid()
شل اصلی منتظر میماند تا اجرای برنامه تمام شود و سپس دوباره Prompt را نمایش میدهد.
این دقیقاً همان چرخهای است که تقریباً تمام Shellهای لینوکس (مانند Bash و Zsh) برای اجرای برنامههای خارجی انجام میدهند:
Read Command
│
▼
Parse Arguments
│
▼
fork()
│
▼
execvp()
│
▼
Program Runs
│
▼
waitpid()
│
▼
Show Prompt Again
این نمونه فقط یک Shell آموزشی است و قابلیتهایی مثل Pipeline (|)، Redirect (>)، متغیرهای محیطی، Wildcard (*)، Job Control، Builtins و Quote Parsing را پیادهسازی نمیکند؛ اما برای درک نحوهی اجرای دستورات در لینوکس، یکی از بهترین نقطههای شروع محسوب میشود.
@Amin_TechLab | 7 |
| 7 | خب حالا که ساختار Shell در لینوکس رو باهاش آشنا شدیم .
وقتی خوبیه که Mini-Shell بنویسیم تا دقیقا عملکردش رو درک کنیم .
در ادامه ابتدا این Mini-Shell را به زبان C مینویسیم و بعد همان را با زبان Rust بازنویسی میکنیم .
@Amin_TechLab | 89 |
| 8 | پشت صحنهی یک دستور در لینوکس! (۱۰/۱۰)
اگر تمام این مسیر را مرور کنیم، میبینیم پشت یک خط فرمان ساده، دنیای بزرگی پنهان شده است.
ابتدا ترمینال ورودی ما را دریافت میکند.
شل آن را میخواند، تجزیه و تحلیل میکند، متغیرها را گسترش میدهد، مسیر برنامه را پیدا میکند و تصمیم میگیرد چه چیزی باید اجرا شود.
در ادامه، با استفاده از مفاهیمی مانند fork و exec فرآیندهای جدید ایجاد میشوند و پس از پایان اجرا، شل دوباره کنترل ترمینال را در اختیار میگیرد.
از طرف دیگر، شل خودش نیز یک زبان برنامهنویسی کامل است؛ زبانی که امکان تعریف متغیر، شرط، حلقه، توابع و خودکارسازی بسیاری از کارها را فراهم میکند.
شاید به همین دلیل است که با وجود دهها محیط گرافیکی مدرن، هنوز هم خط فرمان یکی از قدرتمندترین ابزارهای تعامل با سیستمعامل محسوب میشود.
شل یک ابزار قدیمی نیست؛ بلکه یکی از بنیادیترین بخشهای دنیای یونیکس است که بعد از چند دهه، همچنان همان سادگی، انعطاف و قدرت روز اول را حفظ کرده است.
پایان.
@Amin_TechLab | 121 |
| 9 | پشت صحنهی یک دستور در لینوکس! (۹/۱۰)
کار شل با اجرای یک برنامه تمام نمیشود.
اگر برنامه در Foreground اجرا شده باشد، شل تا پایان اجرای آن منتظر میماند و کنترل ترمینال را دوباره پس میگیرد.
به همین دلیل هنگام اجرای برنامههای زمانبر، تا پایان کار خبری از Prompt جدید نیست.
اما وظایف شل فقط به انتظار کشیدن محدود نمیشود.
اگر کاربر کلیدهای Ctrl+C یا Ctrl+Z را فشار دهد، این شل است که باید سیگنال مناسب را مدیریت کند.
همچنین اگر برنامهای را با & در پسزمینه اجرا کنیم، باز هم شل مسئول مدیریت آن خواهد بود.
او باید:
وضعیت Jobها را نگه دارد.
پایان اجرای آنها را تشخیص دهد.
در صورت نیاز به کاربر اطلاع دهد.
و همزمان Prompt را برای اجرای دستورات بعدی نمایش دهد.
به همین دلیل شل صرفاً یک اجراکنندهی برنامه نیست؛ بلکه مدیر چرخهی اجرای آنها نیز محسوب میشود.
ادامه دارد...
@Amin_TechLab | 116 |
| 10 | پشت صحنهی یک دستور در لینوکس! (۸/۱۰)
شل فقط مسئول اجرای برنامهها نیست؛ خودش یک زبان برنامهنویسی کامل است.
برای مثال:
name="Ali"
echo "$name"
یا:
if [ -f /etc/passwd ]; then
echo "exists"
fi
و حتی:
for file in *.txt; do
echo "$file"
done
در این مثالها دیگر فقط در حال اجرای یک برنامه نیستیم.
شل متغیر تعریف میکند، شرط اجرا میکند، حلقه میسازد و بر اساس قواعد زبانی خودش تصمیم میگیرد چه اتفاقی بیفتد.
به همین دلیل Shell Script فقط مجموعهای از دستورات پشت سر هم نیست؛ بلکه یک زبان اسکریپتنویسی واقعی است.
با استفاده از آن میتوان برنامههای مختلف را به هم متصل کرد، روی خروجی آنها تصمیم گرفت، عملیات تکراری را خودکار کرد و حتی ابزارهای نسبتاً پیچیده ساخت.
همین ویژگی است که شل را از یک Command Runner ساده فراتر میبرد.
ادامه دارد...
@Amin_TechLab | 104 |
| 11 | پشت صحنهی یک دستور در لینوکس! (۷/۱۰)
بعد از اینکه شل تصمیم گرفت چه برنامهای باید اجرا شود، نوبت به دو مفهوم بسیار مهم در سیستمعاملهای شبهیونیکس میرسد:
fork و exec
فرض کنید دستور زیر را اجرا میکنید:
ls
اولین کاری که شل انجام میدهد، ایجاد یک فرآیند جدید است.
این کار با سیستمکال fork() انجام میشود.
بعد از آن، فرآیند جدید با استفاده از exec() خودش را کاملاً با برنامهی ls جایگزین میکند.
در همین زمان، شل اصلی منتظر میماند تا اجرای برنامه تمام شود و سپس دوباره کنترل ترمینال را در اختیار بگیرد.
اما همهی دستورات این مسیر را طی نمیکنند.
برخی دستورات اصلاً فایل اجرایی مستقلی ندارند و داخل خود شل پیادهسازی شدهاند؛ به اینها Shell Builtins گفته میشود.
یکی از بهترین مثالها دستور cd است.
اگر cd یک برنامهی مستقل بود، فقط مسیر کاری همان فرآیند فرزند تغییر میکرد و بعد از پایان برنامه، شل همچنان در همان پوشهی قبلی باقی میماند.
به همین دلیل دستوراتی مانند:
cd
pwd
alias
export
توسط خود شل اجرا میشوند و نیازی به جستجو در PATH ندارند.
ادامه دارد...
@Amin_TechLab | 91 |
| 12 | پشت صحنهی یک دستور در لینوکس! (۶/۱۰)
قبل از اجرای هر دستور، شل یک مرحلهی بسیار مهم را انجام میدهد:
Parsing و Expansion
برای مثال این چهار دستور را ببینید:
echo $HOME
echo "$HOME"
echo '$HOME'
echo *.txt
ظاهر آنها تقریباً مشابه است، اما شل آنها را کاملاً متفاوت تفسیر میکند.
شل باید تشخیص دهد:
آیا متغیر محیطی باید جایگزین شود؟
آیا نقلقولها مانع Expansion هستند؟
آیا * باید به لیست فایلها تبدیل شود؟
در واقع برنامهی echo هیچ شناختی از $HOME یا *.txt ندارد.
تمام این تبدیلها قبل از اجرای برنامه توسط خود شل انجام میشود.
به همین دلیل چیزی که برنامه دریافت میکند، الزاماً همان متنی نیست که شما تایپ کردهاید.
در حقیقت، شل ابتدا متن خام را به یک دستور قابل فهم تبدیل میکند و بعد اجرای آن را آغاز میکند.
ادامه دارد...
@Amin_TechLab | 89 |
| 13 | پشت صحنهی یک دستور در لینوکس! (۵/۱۰)
وقتی Enter را فشار میدهید، اولین چیزی که به شل میرسد چیست؟
از دید ما فقط یک خط متن تایپ شده است؛ اما پشت پرده چندین مرحله اتفاق میافتد.
ابتدا ترمینال کلیدهایی که فشار دادهایم را دریافت میکند.
این دادهها از طریق TTY به برنامهای ارسال میشوند که در Foreground ترمینال قرار دارد؛ معمولاً همان شل.
در نتیجه شل مستقیماً از صفحهکلید چیزی دریافت نمیکند، بلکه اطلاعات را از Standard Input خودش میخواند.
فرض کنید این دستور را وارد میکنیم:
echo myfile
در این لحظه هنوز هیچ برنامهای اجرا نشده است.
شل فقط یک رشتهی متنی دریافت کرده است.
حالا باید تصمیم بگیرد:
دستور چیست؟
آرگومانها کداماند؟
آیا این دستور داخلی است یا باید فایل اجرایی پیدا شود؟
به همین دلیل شل فقط یک اجراکنندهی برنامه نیست؛ بلکه قبل از هر چیز، یک مفسر (Interpreter) است که باید معنی متن واردشده را درک کند.
ادامه دارد ...
@Amin_TechLab | 91 |
| 14 | پشت صحنهی یک دستور در لینوکس! (۴/۱۰)
یکی از زیباترین ایدههایی که فلسفهی یونیکس بر پایهی آن شکل گرفته، مفهوم Pipeline است.
اصل ماجرا ساده است:
هر برنامه فقط یک کار را انجام دهد، اما همان یک کار را به بهترین شکل ممکن انجام دهد.
حالا سؤال اینجاست؛ اگر هر برنامه فقط یک وظیفه داشته باشد، چطور کارهای پیچیده انجام میشوند؟
پاسخ را شل میدهد.
هر برنامه در لینوکس سه کانال استاندارد دارد:
stdin → ورودی برنامه
stdout → خروجی عادی
stderr → خروجی خطا
شل میتواند خروجی یک برنامه را مستقیماً به ورودی برنامهی بعدی متصل کند.
مثلاً:
cat /var/log/mylog | grep "error"
در اینجا خروجی دستور cat مستقیماً وارد grep میشود و grep فقط خطوطی را نمایش میدهد که شامل کلمهی error هستند.
بدون اینکه فایل موقتی ساخته شود یا برنامهی بزرگی نوشته شود.
به همین دلیل ابزارهای یونیکس مثل قطعات لگو هستند؛ هر کدام یک کار انجام میدهند و شل آنها را کنار هم قرار میدهد تا یک کار بزرگتر ساخته شود.
همین سادگی، یکی از مهمترین دلایل ماندگاری فلسفهی یونیکس است.
ادامه دارد....
@Amin_TechLab | 105 |
| 15 | پشت صحنهی یک دستور در لینوکس! (۳/۱۰)
ممکن است این سؤال برایتان پیش بیاید که آیا فقط یک شل وجود دارد؟
از آنجایی که شل خودش فقط یک برنامه است، هر توسعهدهندهای میتواند نسخهی مخصوص خودش را بنویسد. به همین دلیل طی سالها شلهای مختلفی با امکانات متفاوت به وجود آمدهاند.
• sh (Bourne Shell)
قدیمیترین شل استاندارد یونیکس که در سال ۱۹۷۷ معرفی شد. اگرچه امکاناتش محدود است، اما هنوز هم پایهی بسیاری از اسکریپتهای سیستمی محسوب میشود.
• Bash (Bourne Again Shell)
محبوبترین شل دنیای لینوکس و انتخاب پیشفرض بسیاری از توزیعها. قابلیتهایی مثل History، تکمیل خودکار با کلید Tab و امکانات پیشرفتهی اسکریپتنویسی باعث محبوبیت آن شده است.
• Zsh (Z Shell)
شل مدرنی که این روزها در macOS و بسیاری از سیستمهای توسعه استفاده میشود. اگر نام Oh My Zsh را شنیده باشید، با یکی از معروفترین مجموعه افزونههای همین شل آشنا هستید.
• Fish (Friendly Interactive Shell)
اگر به دنبال تجربهای سادهتر و کاربرپسندتر باشید، Fish یکی از بهترین انتخابهاست. پیشنهاد خودکار دستورات، رنگبندی مناسب و تنظیمات پیشفرض خوب، آن را برای کاربران تازهکار و حرفهای جذاب کرده است.
اگر این شلها روی سیستم نصب باشند، کافی است نام آنها را در ترمینال اجرا کنید:
fish
یا
zsh
برای تغییر دائمی شل پیشفرض نیز میتوانید از دستور زیر استفاده کنید:
chsh
در نهایت، همهی این شلها یک هدف مشترک دارند؛ برقراری ارتباط بین شما و سیستمعامل، فقط با روشها و امکانات متفاوت.
ادامه دارد...
@Amin_TechLab | 115 |
| 16 | پشت صحنهی یک دستور در لینوکس!! (۲/۱۰)
امروزه دیگر آن دستگاههای بزرگ وجود ندارند، اما مفهوم آنها همچنان زنده است.
وقتی برنامهای مثل GNOME Terminal، Konsole یا Windows Terminal را باز میکنید، در واقع با یک Terminal Emulator کار میکنید؛ نرمافزاری که رفتار همان ترمینالهای قدیمی را شبیهسازی میکند.
به همین دلیل هنوز هم اصطلاحاتی مانند TTY و PTS را در لینوکس میبینیم؛ یادگار روزهایی که ترمینال یک سختافزار واقعی بود.
اما یک نکتهی مهم وجود دارد:
ترمینال هیچ دستوری را اجرا نمیکند.
وظیفهی آن فقط دریافت ورودی و نمایش خروجی است.
اجرای دستورات بر عهدهی برنامهای دیگر به نام Shell است.
شل نه سیستمعامل است و نه کرنل؛ فقط یک برنامه است، درست مثل هر برنامهی دیگری.
کلمهی Shell به معنی «پوسته» است؛ لایهای که بین کاربر و کرنل قرار میگیرد و ارتباط میان آنها را برقرار میکند.
مثلاً وقتی دستور زیر را اجرا میکنید:
ls
شل ابتدا بررسی میکند که فایل اجرایی ls کجا قرار دارد.
برای این کار از متغیر محیطی PATH استفاده میکند:
echo $PATH
خروجی چیزی شبیه این خواهد بود:
/usr/local/bin:/usr/bin:/bin
شل این مسیرها را بهترتیب جستجو میکند تا فایل اجرایی موردنظر را پیدا کند.
به همین دلیل اگر یک فایل اجرایی داخل پوشهی فعلی داشته باشید، فقط نوشتن نام آن کافی نیست و باید مسیرش را هم مشخص کنید:
./my_app
همین طراحی باعث میشود فایلهای ناشناس داخل پوشهی فعلی بهصورت تصادفی اجرا نشوند و امنیت سیستم حفظ شود.
ادامه دارد ....
@Amin_TechLab | 135 |
| 17 | پشت صحنهی یک دستور در لینوکس! (۱/۱۰)
وقتی در لینوکس ترمینال را باز میکنید و یک دستور مثل ls یا pwd را اجرا میکنید، واقعاً چه اتفاقی میافتد؟
قبل از اینکه سراغ شل برویم، باید کمی به گذشته برگردیم.
دهههای ۱۹۵۰ و ۱۹۶۰ چیزی به اسم «ترمینال» اصلاً یک پنجرهی نرمافزاری نبود؛ بلکه یک دستگاه فیزیکی بود که تنها وظیفهاش دریافت ورودی از کاربر و نمایش خروجی کامپیوتر بود.
در آن زمان کاربر متن را با صفحهکلید وارد میکرد، دادهها از طریق سیگنالهای الکتریکی به کامپیوتر مرکزی ارسال میشدند و پس از پردازش، پاسخ دوباره به دستگاه برمیگشت تا روی رولهای کاغذ چاپ شود.
این دستگاهها که با نام Teletype (TTY) شناخته میشدند، سرعت بالایی هم نداشتند؛ معمولاً چیزی حدود ۱۰ تا ۲۰ کاراکتر در ثانیه چاپ میکردند.
چند سال بعد ترمینالهای ویدیویی معرفی شدند. دیگر خبری از رولهای کاغذی نبود و خروجی مستقیماً روی نمایشگرهای CRT نمایش داده میشد. دستگاههایی مانند DEC VT100 یکی از معروفترین نمونههای این نسل بودند و تجربهی کار با کامپیوتر را متحول کردند.
ادامه دارد....
@Amin_Techlab | 158 |
| 18 | 💻 روش صحیح مدیریت طول عمر حافظه در زبان C (بخش سوم)
اگر میخواهید مقداری بعد از خروج از تابع باقی بماند، نباید آدرس یک متغیر Local را برگردانید.
چند روش صحیح وجود دارد:
✅ روش اول: استفاده از static
char *getName()
{
static char name[] = "Embedded";
return name;
}
متغیرهای static تا پایان اجرای برنامه در حافظه باقی میمانند.
✅ روش دوم: استفاده از حافظه Dynamic
#include <stdlib.h>
#include <string.h>
char *getName()
{
char *name = malloc(20);
strcpy(name, "Embedded");
return name;
}
اما مسئولیت آزاد کردن حافظه با شماست:
free(name);
اگر این کار انجام نشود، مشکل دیگری به نام:
Memory Leak
ایجاد میشود.
✅ روش سوم: ارسال Buffer توسط Caller
این روش در برنامههای Embedded بسیار رایج است:
void getName(char *buffer)
{
strcpy(buffer, "Embedded");
}
در این حالت کسی که حافظه را ساخته، طول عمر آن را هم کنترل میکند.
در زبان C همیشه یک سؤال مهم وجود دارد:
این حافظه متعلق به چه کسی است و تا چه زمانی معتبر است؟
بسیاری از خطاهای سخت در Embedded و سیستمهای Low-Level به دلیل پاسخ اشتباه به همین سؤال ایجاد میشوند.
یک Pointer فقط یک آدرس نیست؛
یک قرارداد درباره مالکیت و طول عمر حافظه است. ⚠️
@Amin_TechLab | 260 |
| 19 | 💻 چرا Dangling Pointerها در C خیلی خطرناک هستند؟ (بخش دوم)
یکی از دلایلی که این نوع خطاها سخت پیدا میشوند این است که برنامه همیشه Crash نمیکند.
مثلاً:
char *getName()
{
char name[] = "Embedded";
return name;
}
این کد اشتباه است.
چون آرایه:
char name[]
داخل Stack ساخته میشود.
بعد از خروج از تابع:
getName()
|
v
+---------------+
| name[] |
+---------------+
این حافظه دیگر معتبر نیست.
اما اشارهگر هنوز همان آدرس قبلی را نگه داشته است.
حالا ممکن است:
printf("%s", getName());
گاهی درست کار کند.
گاهی خروجی خراب بدهد:
Embe@#12
و گاهی برنامه Crash کند.
حتی تغییرهای ساده میتوانند رفتار برنامه را تغییر دهند:
void test()
{
int buffer[100];
}
اضافه کردن همین تابع ممکن است باعث شود حافظه قبلی Stack دوباره استفاده شود و مقدار اشارهگر شما تغییر کند.
مشکل از کامپایلر نیست؛
برنامه شما دارد از حافظهای استفاده میکند که دیگر وجود ندارد.
در بخش آخر روشهای صحیح مدیریت این شرایط را بررسی میکنیم.
@Amin_TechLab | 264 |
| 20 | 💻 یکی از اشتباهات رایج در زبان C: برگرداندن آدرس یک متغیر Local از تابع (بخش اول)
یکی از باگهای خطرناک در زبان C، استفاده از اشارهگری است که به یک متغیر محلی (Local Variable) اشاره میکند.
مثلاً:
#include <stdio.h>
int *getNumber()
{
int x = 42;
return &x;
}
int main()
{
int *ptr = getNumber();
printf("%d\n", *ptr);
return 0;
}
در نگاه اول شاید تصور کنیم تابع:
return &x;
آدرس متغیر x را برمیگرداند و در main میتوانیم مقدار آن را بخوانیم.
اما یک مشکل مهم وجود دارد.
متغیر:
int x = 42;
یک متغیر Local است.
یعنی فقط در زمان اجرای تابع:
getNumber()
وجود دارد.
وقتی تابع تمام میشود، فضای مربوط به آن در Stack دیگر معتبر نیست.
پس اشارهگری که برگردانده شده:
ptr
به حافظهای اشاره میکند که دیگر مالکیتی روی آن نداریم.
به این حالت میگویند:
Dangling Pointer
ممکن است این کد در بعضی سیستمها خروجی زیر را بدهد:
42
و فکر کنید مشکلی وجود ندارد.
اما این فقط یک اتفاق تصادفی است.
در واقع برنامه شما وارد محدودهای شده که استاندارد C هیچ رفتاری را تضمین نمیکند.
در بخش بعدی میبینیم چرا این نوع باگها گاهی بدون خطا اجرا میشوند و بعد از تغییر یک خط ساده یا فعال کردن Optimization خراب میشوند.
@Amin_TechLab | 251 |
