Professional Documents
Culture Documents
Thực hành lập trình C Linux
Thực hành lập trình C Linux
I. Lý Thuyết
1. Các khái niệm cơ bản
- Users (Người dùng): Để có thể sử dụng được Linux, bạn phải được cấp tài khoản (account) đăng nhập vào máy
Linux. Thông tin về tài khoản bao gồm tên đăng nhập (username), mật khẩu đăng nhập (password), và các quyền
truy xuất tập tin và thư mục mà bạn có được dựa vào tài khoản mà bạn đăng nhập và máy.
- Group (Nhóm): Các người dùng làm việc trên cùng một bộ phận hoặc đang làm việc chung trên cùng một dự án
(project) có thể được đưa vào cùng một nhóm. Đây là một cách đơn giản của việc tổ chức để quản lý người dùng.
- File (Tập tin): Tất cả các thông tin trên Linux được lưu giữ trong các tập tin. Các tập tin được tạo ra bởi người
dùng và người chủ tập tin có quyền truy xuất, tạo, sửa đổi, thiết lập kích thước của tập tin và phân phối quyền để cho
phép người dùng khác có thể truy xuất tập tin.
- Directory (Thư mục): Thư mục giống như Folder trong Windows. Nó được dùng để chứa các tập tin và thư mục
khác, và tạo ra cấu trúc cho hệ thống tập tin. Dưới Linux, chỉ có một cây thư mục và gốc của nó là /. Giống như tập
tin, mỗi thư mục có thông tin kết hợp với nó, kích thước tối đa và những người dùng được quyền truy xuất thư mục
này, …
- Path (Đường dẫn): Đường dẫn là 1 chuỗi các thư mục và có thể kết thúc bằng tên của một tập tin. Các thư mục và
tên tập tin được phân cách bởi ký tự /. Ví dụ : /dir1/dir2/file là một đường dẫn tuyệt đối tới file được
chứa trong dir2, với dir2 được chứa trong dir1, và dir1 nằm trong thư mục gốc. Ví dụ khác: ~/homework
là một đường dẫn tương đối, tính từ thư mục đăng nhập của người dùng, vào thư mục homework.
- Permissions (Quyền): Quyền là một đặc tính quan trọng của Linux. Chúng tạo ra sự bảo mật bằng cách giới hạn
các hành động mà người dùng có thể thực hiện đối với tập tin và thư mục. Các quyền đọc (read), ghi (write) và thực
thi (execute) điều khiển việc truy xuất tới việc truy xuất tập tin của người tạo ra nó, nhóm và các người dùng khác.
Một người dùng sẽ không thể truy xuất tới tập tin của người dùng khác nếu không có đủ quyền truy xuất.
- Process (Tiến trình): Khi người dùng thực thi một lệnh, Linux tạo ra một tiến trình chứa các chỉ thị lệnh. Một tiến
trình còn chứa các thông tin điều khiển như thông tin người dùng thực thi lệnh, định danh duy nhất của tiến trình
(PID – process id). Việc quản lý của tiến trình dựa trên PID này.
- Shell: Trong chế độ console, người dùng giao tiếp với máy thông qua shell (hệ vỏ). Một shell là một chương trình
thường được dùng để bắt đầu một chương trình khác từ dấu nhắc của shell. Một shell được cấu hình bằng việc thiết
lập các biến môi trường cho nó. Khi đăng nhập vào Linux, một shell sẽ được tự động tạo ra, và các biến môi trường
mặc nhiên (default) sẽ được thiết lập. Ở đây, ta sẽ sử dụng shell BASH (Bourne Again SHell), là shell thông dụng
của hầu hết các hệ thống Linux.
2. Thực thi Lệnh
- Nhập lệnh: Để nhập lệnh, đơn giản bạn chỉ đánh vào tên của lệnh sau dấu nhắc của shell rồi nhấn Enter. Dấu nhắc
của shell thường có dạng [user@host directory]$, nó có thể được thiết lập lại, và có thể khác nhau đối với
các máy khác nhau. Hầu hết các lệnh thường chấp nhận nhiều đối số (argument) hoặc lựa chọn (option) (thường
được gọi là flag – cờ). Thông thường các đối số được đưa vào bằng cách sử dụng 1 hoặc 2 dấu -. Nếu một lệnh yêu
cầu đối số và chúng ta không đưa vào, lệnh sẽ tự động hiển thị một mô tả ngắn về cách sử dụng các đối số kết hợp
với nó. Một lệnh và các đối số thường có dạng như sau:
command –a1 –a2
command --long_argument_name
- Biến môi trường PATH: Đây là biến môi trường của shell mà cho phép các thư mục mà Linux có thể nhìn thấy
được khi thực thi lệnh nếu đường dẫn đầy đủ của lệnh không được chỉ định rõ ràng. Biến môi trường PATH bao gồm
1 chuỗi tên các đường dẫn thư mục, phân cách bởi dấu ‘:’. Hầu hết các lệnh mà chúng ta sẽ thực hành đều nằm
trong các thư mục mà đã được đưa vào biến môi trường PATH và có thể thực hiện đơn giản bằng cách nhập tên của
nó tại dấu nhắc lệnh. Vì lý do bảo mật, thư mục hiện hành sẽ không được đưa vào biến môi trường PATH, do đó, để
chạy một chương trình nằm trong thư mục hiện hành, chúng ta phải thêm ‘./’ vào trước tên chương trình:
./command
ls –a Liệt kê tất cả tập tin, kể cả các tập tin có thuộc tính ẩn.
ls –l Hiển thị đầy đủ các thông tin (quyền truy cập, chủ, kích thước,
…)
ls | less
Thay đổi thư mục cd path Chuyển đến thư mục được chỉ định bởi path.
cd ~ Chuyển về thư mục nhà.
cd - Chuyển về thư mục trước của bạn.
cd .. Chuyển về thư mục cha của thư mục hiện hành.
Quản lý tập tin và thư cp Cho phép tạo ra một bản sao (copy) của một tập tin hoặc thư
mục mục: cp source_path destination_path
mkdir Cho phép tạo ra một thư mục mới (make directory), rỗng, tại vị
trí được chỉ định: mkdir directoryname
mv Cho phép di chuyển (move) một tập tin từ thư mục này tới thư
mục khác, có thể thực hiện việc đổi tên tập tin:
mv source_path destination_path
rm Cho phép xóa (remove) các tập tin, dùng lệnh ‘rm – R’ để xóa
một thư mục và tất cả những gì nằm trong nó: rm filename
rmdir Dùng để xóa thư mục: rmdir directoryname
touch Tạo tập tin trống: touch filename
Xác định vị trí của tập find Tìm tập tin filename bắt đầu từ thư mục path: find
tin path –name filename
locate Tìm tập tin trong cơ sở dữ liệu của nó có tên là filename:
locate filename
Làm việc với tập tin cat Để xem nội dung của một tập tin văn bản ngắn, chúng ta dùng
văn bản lệnh ‘cat’ để in nó ra màn hình: cat filename
less Cho phép xem một tập tin dài bằng cách cuộn lên xuống bằng
các phím mũi tên và các phím pageUp, pageDown. Dùng phím
q để thoát chế độ xem: less filename
grep Một công cụ mạnh để tìm một chuỗi trong một tập tin văn bản.
Khi lệnh ‘grep’ tìm thấy chuỗi, nó sẽ in ra cả dòng đó lên màn
hình: grep string filename
sort Sắp xếp các dòng trong tập tin theo thứ tự alphabet và in nội
dung ra màn hình: sort filename
Giải nén bunzip2 Giải nén một tập tin bzip2 (*.bz2). Thường dùng cho các
tập tin lớn: bunzip2 filename.bz2
gunzip Giải nén một tập tin gzipped (*.gz): gunzip
filename.gz
unzip Giải nén một tập tin PkZip hoặc WinZip (*.zip): unzip
filename.zip
tar Nén và giải nén các tập tin .tar, .tar.gz: Ví dụ: tar –
xvf filename.tar và tar –xvzf
filename.tar.gz
Xem thông tin hệ date In ngày giờ hệ thống.
thống df –h In thông tin không gian đĩa được dùng.
free In thông tin bộ nhớ được dùng.
history Hiển thị các lệnh được thực hiện bởi tài khoản hiện tại.
hostname In tên của máy cục bộ (host).
pwd In đường dẫn đến thư mục làm việc hiện hành.
rwho -a Liệt kê tất cả người dùng đã đăng nhập vào network.
uptime In thời gian kể từ lần reboot gần nhất.
who Liệt kê tất cả người dùng đã đăng nhập vào máy.
whoami In tên người dùng hiện hành.
Các lệnh dùng theo dõi ps Liệt kê các tiến trình đang kích hoạt bởi người dùng và PID của
tiến trình các tiến trình đó.
ps –aux Liệt kê các tiến trình đang kích hoạt cùng với tên của người
dùng là chủ tiến trình.
top Hiển thị danh sách các tiến trình đang kích hoạt, danh sách này
được cập nhật liên tục.
command & Chạy command trong nền.
fg Đẩy một tiến trình nền hoặc bị dừng lên bề mặt trở lại.
bg Chuyển một tiến trình vào nền. Có thể thực hiện tương tự với
Ctrl-z.
kill pid Thúc đẩy tiến trình kết thúc. Đầu tiên phải xác định pid của
tiến trình cần hủy với lệnh ps.
killall -9 name Hủy tiến trình với name chỉ định.
nice program Chạy program với cấp ưu tiên ngược level. Cấp nice càng
level cao, chương trình càng có mức ưu tiên thấp.
Lệnh cat chuyển hướng cho phép bạn nhập vào nội dung cho file và kết thúc khi bạn nhấn phím Ctrl+D
3. Sao chép tập tin và thư mục
- Sao chép tập tin từ thư test3.c mục user2 sang user1
- Kiểm tra tập tin trong user1 và user2
Muốn sao chép nhiều file bạn có thể dùng các kí tự đại diện *,? hay liệt kê một danh sách các file cần sao chép. Ví
dụ, lệnh sau đây sẽ chép file test3.c, test4.c vào thư mục /user1
$cp test3.c test4.c ../user1
Nếu dùng kí tự đại diện bạn có thể sao chép như sau:
$cp *.c /user1
Nếu muốn sao chép toàn bộ cây thư mục (bao gồm file và thư mục con) bạn sử dụng tùy chọn –R. Ví dụ để sao chép
toàn bộ thư mục /mydata vào thư mục /tmp bạn gọi cp như sau:
$cp –R /mydata /tmp
Ngoài ra các bạn cũng có thể nén thư mục với cách thức tương tự như trên.
5. Xóa tập tin, thư mục
Lệnh rm, rmdir để xóa tập tin hoặc thư mục
* Chú ý: lệnh rmdir dùng để xóa thư mục rỗng, nếu muốn xóa thư mục có chứa thư mục con hoặc tập tin thì thêm
tùy chọn –r sau lệnh rm.Ví dụ:
- Xóa tập tin test1.c trong thư mục user1
- Xóa tập tin test4.c trong thư mục user2
- Xóa thư mục user2 (rỗng)
- Xóa thư mục user1 (không rỗng)
Bài 2: LẬP TRÌNH C TRÊN LINUX
I. Lý thuyết
1. Chương trình trên Linux
- Để có thể viết chương trình trên Linux, chúng ta cần phải nắm rõ 1 số vị trí tài nguyên để xây dựng
chương trình như trình biên dịch, tập tin thư viện, các tập tin tiêu đề (header), các tập tin chương
trình sau khi biên dịch, …
- Trình biên dịch gcc thường được đặt trong thư mục /usr/bin hoặc /usr/local/bin (kiểm
tra bằng lệnh which gcc). Tuy nhiên, khi biên dịch, gcc cần đến rất nhiều tập tin hỗ trợ nằm
trong những thư mục khác nhau như những tập tin tiêu đề (header) của C thường nằm trong thư mục
/usr/include hay /usr/local/include. Các tập tin thư viện liên kết thường được gcc
tìm trong thư mục /lib hoặc /usr/local/lib. Các thư viện chuẩn của gcc thường đặt trong
thư mục /usr/lib/gcc-lib.
Chương trình sau khi biên dịch ra tập tin thực thi (dạng nhị phân) có thể đặt bất cứ vị trí nào trong
hệ thống.
/* nhan.c */
long nhan( int a, int b )
{
return a * b;
}
Thực hiện biên dịch để tạo ra hai tập tin thư viện đối tượng .o
$ gcc –c cong.c nhan.c
Để một chương trình nào đó gọi được các hàm trong thư viện trên, chúng ta cần tạo một tập tin
header .h khai báo các nguyên mẫu hàm để người sử dụng triệu gọi:
/* lib.h */
int cong( int a, int b );
long nhan( int a, int b );
Cuối cùng, tạo ra chương trình chính program.c triệu gọi hai hàm này.
/* program.c */
2
#include <stdio.h>
#include "lib.h"
int main ()
{
int a, b;
printf( "Nhap vào a : " );
scanf( "%d", &a );
printf("Nhap vào b : " );
scanf( "%d", &b );
printf( "Tổng %d + %d = %d\n", a, b, cong( a, b ) );
printf( "Tich %d * %d = %ld\n", a, b, nhan( a, b ) );
return ( 0 );
}
- Chúng ta biên dịch và liên kết với chương trình chính như sau:
$ gcc –c program.c
$ gcc program.o cong.o nhan.o -oprogram
Sau đó thực thi chương trình
$ ./program
Ở đây .o là các tập tin thư viện đối tượng. Các tập tin thư viện .a là chứa một tập hợp các tập tin
.o. Tập tin thư viện .a thực ra là 1 dạng tập tin nén được tạo ra bởi chương trình ar. Chúng ta hãy
yêu cầu ar đóng cong.o và nhan.o vào libfoo.a
$ ar cvr libfoo.a cong.o nhan.o
Sau khi đã có được thư viện libfoo.a, chúng ta liên kết lại với chương trình theo cách sau:
$ gcc program.o –oprogram libfoo.a
Chúng ta có thể sử dụng tùy chọn –l để chỉ định thư viện khi biên dịch thay cho cách trên. Tuy nhiên
libfoo.a không nằm trong thư mục thư viện chuẩn, cần phải kết hợp với tùy chọn –L để chỉ định
đường dẫn tìm kiếm thư viện trong thư mục hiện hành. Dưới đây là cách biên dịch:
$ gcc program.c –oprogram –L –lfoo
Chúng ta có thể sử dụng lệnh nm để xem các hàm đã biên dịch sử dụng trong tập tin chương trình,
tập tin đối tượng .o hoặc tập tin thư viện .a. Ví dụ:
$ nm cong.o
3
Để tạo ra thư viện liên kết động, chúng ta không sử dụng trình ar như với thư viện liên kết tĩnh mà
dùng lại gcc với tùy chọn –shared.
$ gcc –shared cong.o nhan.o -olibfoo.so
Nếu tập tin libfoo.so đã có sẵn trước thì không cần dùng đến tùy chọn –o
$ gcc –shared cong.o nhan.o libfoo.so
Bây giờ chúng ta đã có thư viện liên kết động libfoo.so. Biên dịch lại chương trình như sau:
$ gcc program.c –oprogram –L. –lfoo
- Sử dụng thư viện liên kết động:
Khi Hệ Điều Hành nạp chương trình program, nó cần tìm thư viện libfoo.so ở đâu đó trong hệ
thống. Ngoài các thư mục chuẩn, Linux còn tìm thư viện liên kết động trong đường dẫn của biến
môi trường LD_LIBRARY_PATH. Do libfoo.so đặt trong thư mục hiện hành, không nằm trong
các thư mục chuẩn nên ta cần đưa thư mục hiện hành vào biến môi trường LD_LIBRARY_PATH:
$ LD_LIBRARY_PATH=.:
$ export LD_LIBRARY_PATH
Kiểm tra xem Hệ Điều Hành có thể tìm ra tất cả các thư viện liên kết động mà chương trình sử dụng
hay không:
$ ldd program
rồi chạy chương trình sử dụng thư viện liên kết động này:
$./program
- Một khuyết điểm của việc sử dụng thư viện liên kết động đó là thư viện phải tồn tại trong đường
dẫn để Hệ Điều Hành tìm ra khi chương trình được triệu gọi. Nếu không tìm thấy thư viện, Hệ Điều
Hành sẽ chấm dứt ngay chương trình cho dù các hàm trong thư viện chưa được sử dụng. Ta có thể
chủ động nạp và gọi các hàm trong thư viện liên kết động mà không cần nhờ vào Hệ Điều Hành
bằng cách gọi hàm liên kết muộn.
II. Nội dung bài thực hành số 2
1.Viết chương trình C in ra màn hình các các số nguyên từ 0 đến 9
Bước 1: Mở chương trình soạn thảo: $vi thuchanh.c
Bước 2: Viết chương trình:
- Khởi đầu vào màn hình vi bạn đang ở chế độ xem (view). Muốn chỉnh sửa nội dung
file bạn nhấn phím Insert. Dòng trạng thái cuối màn hình đổi thành --INSERT--
cho biết bạn đang trong chế độ soạn thảo (bạn cũng có thể nhấn phím i hoặc a thay
cho phím Insert).
- Nhấn Enter nếu bạn muốn sang dòng mới. Nhấn các phím mũi tên để di chuyển con
trỏ và thay đổi nội dung file. Muốn ghi lại nội dung file sau khi soạn thảo, bạn nhấn
Esc để trở về chế độ lệnh và nhấn :w. Muốn thoát khỏi vi bạn nhấn :q (hoặc :wq để
lưu và thoát).
4
Bước 3: Biên dịch chương trình thành tập tin đối tượng: $gcc –c thuchanh.c
Bước 4: Biên dịch tập tin đối tượng thành tập tin thực thi: $gcc thuchanh.o –o thuchanh
->Lưu ý: Có thể gom bước 3 và 4 bằng câu lệnh: $gcc thuchanh.c –o thuchanh
Bước 5: Thực thi chương trình bằng lệnh: $./thuchanh
2. Viết chương trình cộng và nhân 2 số nguyên sử dụng thư viện liên kết tĩnh:
$ vi cong.c
int cong(int a, int b)
{
return a + b;
}
$ vi nhan.c
int nhan(int a, int b)
{
return a * b;
}
$ vi program.c
#include <stdio.h>
int main()
{
int a, b;
printf(“\nNhap a:”);
scanf(“%d”,&a);
printf(“Nhap b:”);
scanf(“%d”,&b);
printf(“\nTong cua hai so la: %d”,cong(a,b));
printf(“\nTich cua hai so la: %d\n”,nhan(a,b));
return 0;
}
3. Viết chương trình cộng và nhân 2 số nguyên sử dụng thư viện liên kết động:
$ vi cong.c
int cong(int a, int b)
{
return a + b;
}
$ vi nhan.c
int nhan(int a, int b)
{
return a * b;
}
5
$ vi program.c
#include <stdio.h>
int main()
{
int a, b;
printf(“\nNhap a:”);
scanf(“%d”,&a);
printf(“Nhap b:”);
scanf(“%d”,&b);
printf(“\nTong cua hai so la: %d”,cong(a,b));
printf(“\nTich cua hai so la: %d\n”,nhan(a,b));
return 0;
}
6
Bài 3: XỬ LÝ TIẾN TRÌNH TRONG LINUX
I. Lý Thuyết
1. Khái quát
- Một trong những đặc điểm nổi bật của Linux là khả năng chạy đồng thời nhiều chương trình. Hệ Điều Hành
xem mỗi đơn thể mã lệnh mà nó điều khiển là tiến trình (process). Một chương trình có thể bao gồm nhiều
tiến trình kết hợp với nhau.
- Đối với Hệ Điều Hành, các tiến trình cùng hoạt động chia sẻ tốc độ xử lý của CPU, cùng dùng chung vùng
nhớ và tài nguyên hệ thống khác. Các tiến trình được điều phối xoay vòng bởi Hệ Điều Hành. Một chương
trình của chúng ta nếu mở rộng dần ra, sẽ có lúc cần phải tách ra thành nhiều tiến trình để xử lý những công
việc độc lập với nhau. Các lệnh của Linux thực tế là những lệnh riêng lẻ có khả năng kết hợp và truyền dữ liệu
cho nhau thông qua các cơ chế như : đường ống pipe, chuyển hướng xuất nhập (redirect), phát sinh tín hiệu
(signal), … Chúng được gọi là cơ chế giao tiếp liên tiến trình (IPC – Inter Process Comunication). Đối với
tiến trình, chúng ta sẽ tìm hiểu cách tạo, hủy, tạm dừng tiến trình, đồng bộ hóa tiến trình và giao tiếp giữa các
tiến trình với nhau.
- Xây dựng ứng dụng trong môi trường đa tiến trình như Linux là công việc khó khăn. Không như môi trường
đơn nhiệm, trong môi trường đa nhiệm tiến trình có tài nguyên rất hạn hẹp. Tiến trình của chúng ta khi hoạt
động phải luôn ở trạng thái tôn trọng và sẵn sàng nhường quyền xử lý CPU cho các tiến trình khác ở bất kỳ
thời điểm nào, khi hệ thống có yêu cầu. Nếu tiến trình của chúng ta được xây dựng không tốt, khi đổ vỡ và
gây ra lỗi, nó có thể làm treo các tiến trình khác trong hệ thống hay thậm chí phá vỡ (crash) Hệ Điều Hành.
- Định nghĩa của tiến trình: là một thực thể điều khiển đoạn mã lệnh có riêng một không gian địa chỉ, có
ngăn xếp stack riêng rẽ, có bảng chứa các thông số mô tả file được mở cùng tiến trình và đặc biệt có một định
danh PID (Process Identify) duy nhất trong toàn bộ hệ thống vào thời điểm tiến trình đang chạy.
Như chúng ta đã thấy, tiến trình không phải là một chương trình (tuy đôi lúc một chương trình đơn giản chỉ
cấn một tiến trình duy nhất để hoàn thành tác vụ, trong trường hợp này thì chúng ta có thể xem tiến trình và
chương trình là một). Rất nhiều tiến trình có thể thực thi trên cùng một máy với cùng một Hệ Điều Hành,
cùng một người dùng hoặc nhiều người dùng đăng nhập khác nhau. Ví dụ shell bash là một tiến trình có thể
thực thi lệnh ls hay cp. Bản thân ls, cp lại là những tiến trình có thể hoạt động tách biệt khác.
- Trong Linux, tiến trình được cấp không gian địa chỉ bộ nhớ phẳng là 4GB. Dữ liệu của tiến trình này không
thể đọc và truy xuất được bởi các tiến trình khác. Hai tiến trình khác nhau không thể xâm phạm biến của nhau.
Tuy nhiên, nếu chúng ta muốn chia sẻ dữ liệu giữa hai tiến trình, Linux có thể cung cấp cho chúng ta một
vùng không gian địa chỉ chung để làm điều này.
2
với việc bạn gọi shell thực thi lệnh của hệ thống: $sh –c cmdstr
system() sẽ trả về mã lỗi 127 nếu như không khởi động được shell để gọi lệnh cmdstr. Mã lỗi -1 nếu gặp
các lỗi khác. Còn lại, mã trả về của system() là mã lỗi do cmdstr sau khi lệnh được gọi trả về.
Ví dụ sử dụng hàm system(), system.c
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf( "Thuc thi lenh ps voi system\n" );
system( "ps –ax" );
system(“mkdir daihoc”);
system(“mkdir caodang”);
3
- Sau khi tách tiến trình, mã lệnh thực thi ở cả hai tiến trình được sao chép là hoàn toàn giống nhau. Chỉ có
một dấu hiệu để chúng ta có thể nhận dạng tiến trình cha và tiến trình con, đó là trị trả về của hàm fork().
Bên trong tiến trình con, hàm fork() sẽ trả về trị 0. Trong khi bên trong tiến trình cha, hàm fork() sẽ trả
về trị số nguyên chỉ là PID của tiến trình con vừa tạo. Trường hợp không tách được tiến trình, fork() sẽ trả
về trị -1. Kiểu pid_t được khai báo và định nghĩa trong uinstd.h là kiểu số nguyên (int).
- Đoạn mã điều khiển và sử dụng hàm fork() thường có dạng chuẩn sau:
pid_t new_pid;
new_pid = fork(); // tách tiế n trình
switch (new_pid)
{
case -1: printf( "Khong the tao tien trinh moi" ); break;
case 0: printf( "Day la tien trinh con" );
// mã lệnh dành cho tiến trình con đặt ở đây
break;
default: printf( "Day la tien trinh cha" );
// mã lệnh dành cho tiến trình cha đặt ở đây
break;
}
d) Kiểm soát và đợi tiến trình con
- Khi fork() tách tiến trình chính thành hai tiến trình cha và con, trên thực tế cả hai tiến trình cha lẫn tiến
trình con đều hoạt động độc lập. Đôi lúc tiến trình cha cần phải đợi tiến trình con thực hiện xong tác vụ thì
mới tiếp tục thực thi. Ở ví dụ trên, khi thực thi, chúng ta sẽ thấy rằng tiến trình cha đã kết thúc mà tiến trình
con vẫn in thông báo và cả tiến trình cha và tiến trình con đều tranh nhau gởi kết quả ra màn hình. Chúng ta
không muốn điều này, chúng ta muốn rằng khi tiến trình cha kết thúc thì tiến trình con cũng hoàn tất thao tác
của nó. Hơn nữa, chương trình con cần thực hiện xong tác vụ của nó thì mới đến chương trình cha. Để làm
được việc này, chúng ta hãy sử dụng hàm wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int &stat_loc);
Hàm wait khi được gọi sẽ yêu cầu tiến trình cha dừng lại chờ tiến trình con kết thúc trước khi thực hiện tiếp
các lệnh điều khiển trong tiến trình cha. wait() làm cho sự liên hệ giữa tiến trình cha và tiến trình con trở
nên tuần tự. Khi tiến trình con kết thúc, hàm sẽ trả về số PID tương ứng của tiến trình con. Nếu chúng ta
truyền thêm đối số stat_loc khác NULL cho hàm thì wait() cũng sẽ trả về trạng thái mà tiến trình con
kết thúc trong biến stat_loc. Chúng ta có thể sử dụng các macro khai báo sẵn trong sys/wait.h như
sau:
WIFEXITED (stat_loc) Trả về trị khác 0 nếu tiến trình con kết thúc bình thường.
WEXITSTATUS (stat_loc) Nếu WIFEXITED trả về trị khác 0, macro này sẽ trả về mã lỗi của tiến
trình con.
WIFSIGNALED (stat_loc) Trả về trị khác 0 nếu tiến trình con kết thúc bởi một tín hiệu gửi đến.
WTERMSIG(stat_loc) Nếu WIFSIGNALED khác 0, macro này sẽ cho biết số tín hiệu đã hủy tiến
trình con.
WIFSTOPPED(stat_loc) Trả về trị khác 0 nếu tiến trình con đã dừng.
WSTOPSIG(stat_loc) Nếu WIFSTOPPED trả về trị khác 0, macro này trả về số hiệu của signal.
4
II. Thực Hành
Bài 1. Sử dụng hàm system(), system_demo.c tạo các tiến trình sau:
Tạo thư mục ThucHanh1 và ThucHanh2
Tạo tập tin Tho.c trong thư mục ThucHanh1 và ghi chuỗi “troi hom nay that dep !” vào tập
tin vừa tạo (sử dụng lệnh echo để ghi chuỗi vào tập tin: echo noi_dung_chuoi >ten_tap_tin).
Sao chép tập tin vừa tạo sang thư mục ThucHanh2 và hiển thị lên màn hình.
Bài 2. Sử dụng hàm execlp để thay thế tiến trình hiện tại bằng tiến trình ps –af của Hệ Điều Hành.
#include <unistd.h>
#include <stdio.h>
int main()
{
printf( "Thuc thi lenh ps voi execlp\n" );
execlp( "ps", "ps", "–ax", 0 );
printf( "Thuc hien xong! Nhung chung ta se khong thay duoc dong nay.\n"
);
exit( 0 );
}
Bài 3. Tạo tập tin fork_demo.c sử dụng hàm fork() trong đó:
In ra câu thông báo: “Khong the tao tien trinh con !” nếu hàm fork() trả về giá trị -1.
Ngược lại:
- In ra 5 lần câu thông báo: “Day la tien trinh con !” nếu mã trả về là 0.
- In ra 3 lần câu thông báo: “Day la tien trinh cha !” nếu mã trả về là PID của tiến
trình con.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
char * message;
int n;
pid = fork();
switch ( pid )
{
case -1:
printf( "Khong the tao tien trinh con !" );
exit(1);
case 0:
message = "Day la tien trinh con !";
n = 0;
for ( ; n < 5; n++ ) {
printf( "%s", message );
sleep( 1 );
}
break;
default:
message = "Day la tien trinh cha !";
n = 0;
for ( ; n < 3; n++ ) {
printf( "%s", message );
sleep( 1 );
}
break;
}
exit( 0 );
}
Biên dịch và thực thi chương trình này, chúng ta sẽ thấy rằng cả 2 tiến trình hoạt động đồng thời và in ra kết
quả đan xen nhau. Nếu muốn xem sự liên quan về PID và PPID của cả 2 tiến trình cha và con khi lệnh
fork() phát sinh, chúng ta có thể thực hiện chương trình như sau:
$./fork_demo & ps – af
5
Bài 4. Sử dụng hàm wait() để chờ tiến trình con kết thúc sau khi gọi fork(), wait_child.c
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t pid;
int child_status;
int n;
// nhân bản tiến trình, tạo bản sao mới
pid = fork();
switch ( pid ) {
case -1: // fork không tạo được tiến trình mới
printf("Khong the tao tien trinh moi");
exit( 1 );
case 0: // fork thành công, chúng ta đang ở trong tiến trình con
n = 0;
for ( ; n < 5; n++ ) {
printf( "Tien trinh con" );
sleep( 1 );
}
exit( 0 ); // Mã lỗi trả về của tiến trình con
default: // fork thành công, chúng ta đang ở trong tiến trình cha
printf("Tien trinh cha, cho tien trinh con hoan thanh.\n”);
// Chờ tiến trình con kết thúc
wait( &child_status );
printf("Tien trinh cha – tien trinh con hoan thanh.\n");
}
return ( 0 );
}
Bài 5. Sử dụng hàm wait() để chờ tiến trình con kết thúc sau khi gọi fork(), wait_child2.c, kiểm
tra mã lỗi trả về từ tiến trình con.
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t pid;
int child_status;
int n;
// nhân bản tiến trình, tạo bản sao mới
pid = fork();
switch ( pid ) {
case -1: // fork không tạo được tiến trình mới
printf("Khong the tao tien trinh moi");
exit( 1 );
case 0: // fork thành công, chúng ta đang ở trong tiến trình con
n = 0;
for ( ; n < 5; n++ ) {
printf( "Tien trinh con" );
sleep( 1 );
}
exit( 37 ); // Mã lỗi trả về của tiến trình con
default: // fork thành công, chúng ta đang ở trong tiến trình cha
n = 3;
for ( ; n > 0; n-- ) {
printf( "Tien trinh cha" );
sleep( 1 );
}
6
// Chờ tiến trình con kế t thúc
wait( &child_status );
// Kiểm tra và in mã lỗi trả về của tiến trình con
printf( "Tien trinh con hoan thanh: PID = %d\n", pid );
if ( WIFEXITED( child_status ))
printf( "Tien trinh con thoat ra voi ma %d\n",
WEXITSTATUS( child_status ) );
else
printf( "Tien trinh con ket thuc binh thuong\n" );
break;
}
exit( 0 );
}
7
BÀI 4
GIAO TIẾP GIỮA CÁC TIẾN TRÌNH TRONG LINUX
I. Khái quát
Linux cung cấp một số cơ chế giao tiếp giữa các tiến trình gọi là IPC (Inter-Process Communication):
Trao đổi bằng tín hiệu (signals handling)
Trao đổi bằng cơ chế đường ống (pipe)
Trao đổi thông qua hàng đợi thông điệp (message queue)
Trao đổi bằng phân đoạn nhớ chung (shared memory segment)
Giao tiếp đồng bộ dùng semaphore
Giao tiếp thông qua socket
1. Khái niệm
- Tín hiệu là các thông điệp khác nhau được gởi đến tiến trình nhằm thông báo cho tiến trình một tình huống. Mỗi tín hiệu có thể kết
hợp hoặc có sẵn bộ xử lý tín hiệu (signal handler). Tín hiệu sẽ ngắt ngang quá trình xử lý của tiến trình, bắt hệ thống chuyển sang
gọi bộ xử lý tín hiệu ngay tức khắc. Khi kết thúc xử lý tín hiệu, tiến trình lại tiếp tục thực thi.
- Mỗi tín hiệu được định nghĩa bằng một số nguyên trong /urs/include/signal.h. Danh sách các hằng tín hiệu của hệ thống
có thể xem bằng lệnh kill –l.
a) Từ bàn phím
Ctrl+C: gởi tín hiệu INT( SIGINT ) đến tiến trình, ngắt ngay tiến trình (interrupt).
Ctrl+Z: gởi tín hiệu TSTP( SIGTSTP ) đến tiến trình, dừng tiến trình (suspend).
Ctrl+\: gởi tín hiệu ABRT( SIGABRT ) đến tiến trình, kết thúc ngay tiến trình (abort).
b) Từ dòng lệnh
- Lệnh kill -<signal> <PID>
Ví dụ: kill -INT 1234 dùng gởi tín hiệu INT ngắt tiến trình có PID 1234.
Nếu không chỉ định tên tín hiệu, tín hiệu TERM được gởi để kết thúc tiến trình.
- Lệnh fg: gởi tín hiệu CONT đến tiến trình, dùng đánh thức các tiến trình tạm dừng do tín hiệu TSTP trước đó.
2
IV. Thực hành
Bài 1: Chương trình đặt bẫy tín hiệu (hay thiết lập bộ xử lý) tín hiệu INT. Đây là tín hiệu gửi đến tiến trình khi người dùng nhấn
Ctrl + C. Chúng ta không muốn chương trình bị ngắt ngang do người dùng vô tình (hay cố ý) nhấn tổ hợp phím này.
#include <stdio.h> /*Hàm nhập xuất chuẩn*/
#include <unistd.h> /*các hàm chuẩn của UNIX như getpid()*/
#include <signal.h> /*các hàm xử lý tín hiệu()*/
Bài 2: Tạo đường ống, gọi hàm fork() để tạo ra tiến trình con. Tiến trình cha sẽ đọc dữ liệu nhập vào từ phía người dùng và ghi vào
đường ống trong khi tiến trình con phía bên kia đường ống tiếp nhận dữ liệu bằng cách đọc từ đường ống và in ra màn hình.
#include <stdio.h>
#include <unistd.h>
/*Cài đặt hàm dùng thực thi tiến trình con*/
void do_child( int data_pipes[] )
{
int c; /*Chứa dữ liệu từ tiến trình cha*/
int rc; /*Lưu trạng thái trả về của read()*/
/*Tiến trình con chỉ đọc đường ống nên đóng đầu ghi do không cần*/
close( data_pipes[1] );
/*Tiến trình con đọc dữ liệu từ đầu đọc */
while ( ( rc = read( data_pipes[0], &c, 1 ) ) > 0 )
{
putchar( c );
}
exit( 0 );
}
3
{
perror( "Error: pipe not created" );
exit( 1 );
}
/*Tạo tiến trình con*/
pid = fork();
switch ( pid )
{
case -1: /*Không tạo được tiến trình con*/
perror( "Child process not create" );
exit( 1 );
case 0: /*Tiến trình con*/
do_child( data_pipes );
default: /*Tiến trình cha*/
do_parent( data_pipes );
}
return 0;
}
Bài 3: Chương trình sử dụng cơ chế đường ống giao tiếp hai chiều, dùng hàm fork() để nhân bản tiến trình. Tiến trình thứ nhất
(tiến trình cha) sẽ đọc nhập liệu từ phía người dùng và chuyển vào đường ống đến tiến trình thứ hai (tiến trình con). Tiến trình thứ
hai xử lý dữ liệu bằng cách chuyển tất cả ký tự thành chữ hoa sau đó gửi về tiến trình cha qua một đường ống khác. Cuối cùng tiến
trình cha sẽ đọc từ đường ống và in kết quả của tiến trình con ra màn hình (Sinh viên tự làm).
Bài 4: Tạo hai tiến trình tách biệt: producer.c là tiến trình sản xuất, liên tục ghi dữ liệu vào đường ống mang tên
/tmp/my_fifo trong khi consumer.c là tiến trình tiêu thụ liên tục đọc dữ liệu từ đường ống /tmp/my_fifo cho đến khi
nào hết dữ liệu trong đường ống thì thôi. Khi hoàn tất quá trình nhận dữ liệu, tiến trình consumer sẽ in ra thông báo kết thúc.
/* producer.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "my_fifo" /*Tạo đường ống*/
#define BUFFER_SIZE PIPE_BUF /*Vùng đệm dùng cho đường ống*/
#define TEN_MEG ( 1024 * 1024 * 10 ) /*Dữ liệu*/
int main() {
int pipe_fd;
int res;
int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[BUFFER_SIZE + 1];
4
exit( EXIT_FAILURE );
}
printf( "Process %d finished, %d bytes sent\n", getpid(), bytes_sent );
exit( EXIT_SUCCESS );
}
/* consumer.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "my_fifo"
#define BUFFER_SIZE PIPE_BUF
int main() {
int pipe_fd;
int res;
int open_mode = O_RDONLY;
int bytes_read = 0;
char buffer[BUFFER_SIZE + 1];
/* Mở đường ống để đọc */
printf( "Process %d starting to read on pipe\n", getpid() );
pipe_fd = open( FIFO_NAME, open_mode);
if ( pipe_fd != -1 )
{
do
{
res = read( pipe_fd, buffer, BUFFER_SIZE );
bytes_read += res;
} while ( res > 0 );
( void ) close( pipe_fd ); /Kết thúc đọc*/
}
else
{
exit( EXIT_FAILURE );
}
printf( "Process %d finished, %d bytes read\n", getpid(), bytes_read );
exit( EXIT_SUCCESS );
}
Chạy producer dưới nền, tiếp đến là consumer: ./producer & ./consumer
5
BÀI 5
CÁC TẬP LỆNH LINUX
I. Quyền sử dụng tập tin và thư mục
- Tất cả các tập tin và thư mục của Linux đều có người sở hữu và quyền truy nhập. Có thể đổi các tính chất này cho phép nhiều
hay ít quyền truy nhập hơn đối với một tập tin hay thư mục. Quyền của tập tin còn cho phép xác định tập tin có là một chương
trình (application) hay không (khác với Windows xác định tính chất này qua phần mở rộng của tên tập tin).
Kiểu Quyền Số Chủ Tên Kích thước Thời điểm sửa Tên
tập tin tập tin liên kết nhân nhóm (bytes) đổi sau cùng tập tin
Có 3 đối tượng chính là {owner, group, other} và mỗi đối tượng ứng với 3 quyền cụ thể {read, write, execute}.
- Quyền tập tin được thay đổi bằng lệnh chmod: chú ý không thay đổi được quyền của symbolic link.
chmod [-R] mode file
Có hai phương pháp dùng chmod: phương pháp tượng trưng và phương pháp tuyệt đối.
- Phương pháp tượng trưng:
Đối tượng truy nhập (Who) Gán/thu hồi quyền (Operation) Quyền (Permission)
4 4 0
2 0 0
1 1 1
7 5 1
Thư mục được gán sticky bit thì chỉ owner của tập tin và root mới được phép sửa, xóa file. Nhận diện thư mục có sticky bit
drwxr-xr-t: quyền thực thi của người dùng khác là t.
Gán bit sticky cho thư mục theo mẫu 0xxx như sau:
$ mkdir test
$ chmod -R 1777 test
$ ls –l
drwxwrxrwt 2 s01 student 40 Apr 12 16:00 test
- Các quyền là mặc định khi tạo tập tin. Khi một tập tin hay thư mục được tạo ra, quyền mặc định sẽ được xác định bởi các quyền
666 trừ bớt các quyền hiển thị bằng lệnh umask. Quyền cao nhất ở đây là 666 do quyền thực thi chương trình cần được gán cố ý
bởi người sử dụng hay các chương trình biên dịch.
$ umask 002
$ echo tao mot file > tmp
$ ls -l
total 5472
-rw-rw-r-- 1 s01 student 12 Apr 3 21:55 tmp //Quyền 664
$ umask 022
$ echo tao mot file khac > tmp1
$ ls -l
-rw-rw-r-- 1 s01 student 12 Apr 3 21:55 tmp
-rw-r--r-- 1 s01 student 12 Apr 3 21:59 tmp1 //Quyền 644
Giá trị mặc định của các quyền thường được gán mỗi khi người sử dụng login vào hệ thống thông qua các tập tin ẩn khởi tạo biến
môi trường như .profile, .bashrc. Đứng trên quan điểm bảo mật hệ thống, giá trị 027 là tốt nhất, nó cho người cùng nhóm
có quyền đọc và không cho quyền nào với những người khác.
3. Số liên kết
Linux và UNIX cho phép bạn tạo ra một file liên kết tắt (symbol link) đến một file vật lý khác. File liên kết tắt có thể trỏ đến một
file hoặc thư mục. Có 2 loại liên kết:
- Liên kết tắt cứng (hard link): tạo ra một file trỏ đến cùng mục nhập i-node của file vật lý trên đĩa. Và do đó file vật lý trên đĩa
chỉ thật sự bị xóa khi tất cả các liên kết cứng đã bị xóa cùng với bản thân file (không tạo được hard link cho thư mục).
- Liên kết tắt mềm (soft link): chỉ chứa các thông tin trỏ đến file vật lý. File liên kết mềm không tham chiếu trực tiếp đến điểm
nhập i-node của file vật lý mà nó trỏ đến. Nếu bạn xóa file vật lý ban đầu đi thì file liên kết mềm sẽ không biết đường tham chiếu
đến file gốc ban đầu nữa. Tuy nhiên một khi bạn tạo lại file gốc vật lý thì file liên kết mềm vẫn tiếp tục có hiệu lực.
$ps -af
PID TTY TIME CMD
128 tty1 00:00:00 bash
235 pts/0 00:00:00 bash
…
Có một số tiến trình có độ ưu tiên cao và không thể loại bỏ theo cách thông thường. Lúc này ta sử dụng kill ở cấp độ -9. Ví dụ:
$kill -9 137
III. Luyện tập
1. Thay đổi quyền truy xuất
- Truy cập bằng quyền root
- Tạo 2 người dùng: user1 và user2
- Đặt password đăng nhập cho user1 và user2 và root
- Tạo nhóm người dùng: group1
- Chuyển user1 và user2 vào nhóm group1
- Tạo thư mục /baitap1 với quyền 770
- Đăng nhập vào tài khoản user1
- Viết chương trình c: program1.c đặt trong thư mục /baitap1 in ra câu thông báo: “Hello world”
a.Mutex là gì
Mutex thực sự là một cờ hiệu, hay đối với hệ thống, mutex là một đối tượng mang hai trạng thái: đang
được sử dụng và chưa sử dụng (trạng thái sẵn sàng).
Khi mutex bật, một tuyến sẽ bước vào sử dụng tài nguyên và tắt mutex. Tuyến khác sẽ không sử dụng
được tài nguyên cho đến khi tuyến trước đó bật lại mutex ở trạng thái sẵn sàng.
rc = pthread_mutex_unlock (&a_mutex);
if (rc)
{
perror (“pthread_mutex_unlock error”);
pthread_exit (NULL);
}
d. Hủy mutex
Sau khi sử dụng xong mutex bạn nên hủy nó. Sử dụng xong có nghĩa là không còn tuyến nào cần chiếm
giữ mutex cho các cho tác khóa/tháo khóa nữa. Hàm pthread_mutex_destroy () được dùng để hủy
mutex.
rc = pthread_mutex_destroy (&a_mutex);
Sau khi gọi hàm hủy mutex, bạn không còn sử dụng được biến mutex được nữa. Để sử dụng lại biến
mutex bạn cần thực hiện lại bước khởi tạo.
/*Tạo tuyến*/
thr_id = pthread_create (&p_thread, NULL, do_loop, (void*) a);
/*Hàm xử lý tuyến*/
void* do_thread (void* data)
{
printf ("Thread function is executing ... \n");
printf ("Thread data is %s\n", (char*) message);
sleep (3);
strcpy (message, "Bye !");
pthread_exit ("Thank you for using my thread");
}
if (res != 0)
{
perror ("Thread created error\n");
exit (EXIT_FAILURE);
}
if (res != 0)
{
perror ("Thread wait error\n");
exit(EXIT_FAILURE);
}
Bài 3: Chờ đồng thời nhiều tuyến: dùng mảng để lưu thông tin về danh sách các tuyến. Sau đó chương trình
chính sẽ gọi pthread_join () để chờ các tuyến trong danh sách kết thúc.
thread_multiwait.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define MAX_THREADS 6
int main ()
{
int res;
int thread_num;
pthread_t a_thread [MAX_THREADS];
void* thread_result;
if (res != 0)
{
perror ("Thread created error");
exit (EXIT_FAILURE);
}
/*Dừng 1 giây*/
sleep (1);
}
printf ("Waiting for threads to finish ...\n");
/*Biến dữ liệu toàn cục có thể truy xuất bởi cả hai tuyến*/
int global_var;
if (res != 0)
{
perror ("Mutex create error");
exit (EXIT_FAILURE);
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
int main ()
{
int res, i;
pthread_t a_thread;
void* thread_result;
/*Khởi tạo đối tượng semaphore - Ở đây ta đặt giá trị cho semaphore là 2*/
res = sem_init (&semaphore, 0, 2);
if (res != 0)
{
perror ("Semaphore init error");
exit (EXIT_FAILURE);
}
/*Khởi tạo tuyến đóng vai trò người tiêu thụ - consumer*/
res = pthread_create (&a_thread, NULL, do_thread, NULL);
if (res != 0)
{
perror ("Thread create error");
exit (EXIT_FAILURE);
}
Consumer product_val = 1
Consumer product_val = 0
Producer product_val = 1
Consumer product_val = 0
Producer product_val = 1
Consumer product_val = 0
Producer product_val = 1
Consumer product_val = 0
thread_cancel.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int main ()
{
int res, i;
pthread_t a_thread;
void* thread_result;
if (res != 0)
{
perror ("Thread cancel error");
exit (EXIT_FAILURE);
}
/*
Do mặc định tuyến tạo ra với trạng thái PTHREAD_CANCEL_DEFERRED nên
tuyến chỉ thực sự chấm dứt khi bạn gọi hàm pthread_join ()
*/
printf ("Waiting for thread to finish ...\n");
res = pthread_join (a_thread, &thread_result);
if (res != 0)
{
perror ("Thread waiting error");
exit (EXIT_FAILURE);
}
Chú ý tùy chọn –f có dữ liệu kết hợp kèm theo là đối số tiếp theo ngay sau nó: file.c
- Hàm getopt() giúp ta phân tích đối số dòng lệnh tuân theo một khuôn dạng chỉ định.
Hàm getopt() nhận argc và argv làm đối số thứ nhất và thứ hai. Đối số thứ ba là chuỗi xác định nội dung tùy chọn, chuỗi này sẽ thông báo cho
getopt() biết tùy chọn được định nghĩa như thế nào trong chương trình, có hay không dữ liệu kết hợp với tùy chọn.
Ví dụ: lới gọi hàm: getopt( argc, argv, "if:lr" );
sẽ phân tích và xem –i, -f, -l và –r là các tùy chọn, tuy nhiên –f sẽ có dữ liệu kèm theo (sau f có ký tự :) là đối số đi liền sau –f.
Kết quả trả về của getopt() là ký tự tùy chọn kế tiếp được tìm thấy trong mảng argv. Vì vậy cần gọi getopt() nhiều lần trong vòng lặp để lần
lượt lấy ra các tùy chọn mong muốn.
- Hoạt động của getopt() như sau:
+ Nếu tùy chọn có kèm theo giá trị dữ liệu, giá trị này sẽ được lấy ra và trỏ đến bởi biến toàn cục optarg.
+ getopt() trả về mã -1 nếu không có tùy chọn nào có thể lấy ra được
+ getopt() trả về ký tự ? nếu không nhận dạng được tùy chọn, giá trị không nhận được này sẽ được lấy ra và trỏ đến bởi biến toàn cục optopt.
+ Nếu tùy chọn yêu cầu dữ liệu kèm theo mà trong danh sách đối số phân tích không tìm thấy dữ liệu, hàm getopt() trả về ký tự :.
+ Biến toàn cục optind dùng làm chỉ số cho biết vị trí của đối số tiếp theo cần xử lý.
1
case ':' :
printf( "option needs a value\n" );
break;
case '?' :
printf( "unknown option: %c\n", optopt );
break;
}
}
for ( ; optind < argc; optind++ )
printf( "argument: %s\n", argv[optind] );
return 0;
}
$ ./argopt –i –lr 'Hello World' –f file.c -q
option: i
option: l
option: r
filename: file.c
./argopt: invalid option –-q
invalid option: q
argument: HelloWorld
2
if ( value )
pritnf( "New value of %s is %s\n", var, value );
else
pritnf( "New value of %s is null?\n", var );
}
return 0;
}
Test chương trình:
$ get_setenv HOME
Variable HOME has value /home/dsl
$ get_setenv BOOK
Variable BOOK has no value
$ get_setenv BOOK Linux_Programming
Variable BOOK has no value
Calling putenv with: BOOK = Linux_Programming
New value of BOOK is Linux_Programming
$get_setenv BOOK
Variable BOOK has no value
3. Biến environ
- Danh sách tất cả các biến môi trường mà chương trình có thể đọc được chứa trong biến environ, là mảng các chuỗi theo dạng varname=value:
#include <stdlib.h>
extern char** environ;
Ví dụ showenv.c cho thấy cách đọc nội dung biến environ:
Bài 4: showenv.c
#include <stdlib.h>
#include <stdio.h>
extern char** environ;
int main()
{
char** env = environ;
while ( *env ) {
pritnf( "%s\n", *env );
env++;
}
return 0;
}
Kết quả khi chạy chương trình:
3
III. Thông tin về người dùng
- Người dùng thường triệu gọi chương trình (lệnh) từ dấu nhắc của shell, ta sẽ tìm hiểu thông tin người dùng đang sử dụng chương trình.
- Khi người dùng đăng nhập, hệ thống sẽ kiểm tra username và password. Nếu hợp lệ, hệ thống cho phép tiếp cận dấu nhắc của shell và cấp cho
mỗi người dùng một số định danh UID dùng trong hệ thống. Chương trình do người dùng gọi sẽ mang số UID cho biết người dùng đã gọi nó.
Linux cung cấp các hàm sau để lấy định danh UID cũng như thông tin người dùng đã đăng nhập triệu gọi chương trình:
#include <unistd.h>
#include <sys/types.h>
uid = getuid();
gid = getgid();
printf( "User is %s\n", getlogin() );
printf( "User IDs: uid=%d, gid=%d\n", uid, gid );
pw = getpwuid( uid );
printf( "UID passwd entry:\n name=%s, uid=%d, gid=%d, home=%s, shell=%s\n",
pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell );
pw = getpwnam( "root" );
printf( "root passwd entry:\n" );
printf( "name=%s, uid=%d, gid=%d, home=%s, shell=%s\n",
pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell );
return 0;
}
Biên dịch và chạy chương trình với kết quả kết xuất như sau:
4
Bài 8:
LẬP TRÌNH MẠNG BẰNG SOCKET
I. Khái niệm về socket
1. Socket
- Khi bạn viết ứng dụng và có yêu cầu tương tác với một ứng dụng khác, chúng ta thường dựa vào mô hình khách/chủ (client/server):
+ Ứng dụng chủ (trình chủ hay server): ứng dụng có khả năng phục vụ hoặc cung cấp cho bạn thông tin nào đó.
+ Ứng dụng khách (trình khách hay client): ứng dụng gửi yêu cầu đến trình chủ.
- Trước khi yêu cầu một dịch vụ của trình chủ thực hiện điều gì đó, trình khách (client) phải có khả năng kết nối được với trình chủ. Quá trình kết
nối này được thực hiện thông qua một cơ chế trừu tượng hóa gọi là socket. Kết nối giữa trình khách và trình chủ tương tự như việc cắm phích điện
vào ổ cắm điện .Trình khách thường được coi như phích cắm điện, còn trình chủ được xem như ổ cắm điện, một ổ cắm có thể cắm vào đó nhiều
phích điện khác nhau cũng như một máy chủ có thể kết nối và phục vụ cho rất nhiều máy khách.
Nếu kết nối socket thành công thì trình khách và trình chủ có thể thực hiện các yêu cầu về trao đổi dữ liệu với nhau.
- Dưới đây là một ví dụ đơn giản về trình khách client1.c. Trình khách kết nối với trình chủ thông qua socket mang tên server_socket và gửi
ký tự A xem như lời chào bắt tay đến server.
client1.c
/* 1. Tạo các #include cần thiết để gọi hàm socket */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
int main()
{
int sockfd; /* số mô tả socket – socket handle */
int len;
struct sockaddr_un address; /* structure quan trọng, chứa các thông tin về socket */
int result;
char ch = 'A';
/* 2. Tạo socket cho trình khách. Lưu lại số mô tả socket */
sockfd = socket( AF_UNIX, SOCK_STREAM, 0 );
address.sun_family = AF_UNIX;
/* 3. Gán tên của socket trên máy chủ cần kết nối */
strcpy( address.sun_path, "server_socket" );
len = sizeof( address );
/* 4. Thực hiện kết nối */
result = connect( sockfd, (struct sockaddr*)&address, len );
if ( result == -1 ) {
perror( "Oops: client1 problem" );
exit( 1 );
}
/* 5. Sau khi socket kết nối, chúng ta có thể đọc ghi dữ liệu của socket tương tự đọc ghi trên file */
write( sockfd, &ch, 1 );
read ( sockfd, &ch, 1 );
printf( "char from server: %c\n", ch );
close( sockfd );
exit( 0 );
}
1
- Chương trình chưa chạy được do phần server (chính xác hơn là socket tên server-socket mà trình khách yêu cầu kết nối) chưa được thiếp lập.
- Dưới đây là trình chủ server1.c thực hiện mở socket, đặt tên cho socket là server_socket, mở hàng đợi lắng nghe kết nối của trình khách
bằng listen(), chấp nhận kết nối bằng accept(). Sau cùng nhận/gửi dữ liệu về trình khách và đóng kết nối.
server1.c
/* 1.Tạo các #include cần thiết */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
/* 2. Loại bỏ các tên hay liên kết socket khác trước đó nếu có. Đồng thời thực hiện khởi tạo socket mới cho trình chủ */
unlink( "server_socket" );
server_sockfd = socket( AF_UNIX, SOCK_STREAM, 0 );
/* 3. Đặt tên cho socket của trình chủ */
server_address.sun_family = AF_UNIX;
strcpy( server_address.sun_path, "server_socket" );
server_len = sizeof( server_address );
/* 4. Ràng buộc tên với socket */
bind( server_sockfd, (struct sockaddr *)&server_address, server_len );
/* 5. Mở hàng đợi nhận kết nối - cho phép đặt hàng vào hàng đợi tối đa 5 kết nối */
listen( server_sockfd, 5 );
/* 6. Lặp vĩnh viễn để chờ và xử lý kết nối của trình khách */
while ( 1 ) {
char ch;
printf( "server waiting...\n" );
/* Chờ và chấp nhận kết nối */
client_sockfd = accept( server_sockfd, (struct sockaddr*)&client_address, &client_len );
/* Đọc dữ liệu do trình khách gửi đến */
read( client_sockfd, &ch, 1 );
ch++;
/* Gửi trả dữ liệu về cho trình khách */
write( client_sockfd, &ch,1 );
/* Đóng kết nối */
close( client_sockfd );
}
}
- Dịch và chạy server dưới nền bằng:
$ gcc server1.c –o server1
$ ./server1 &
- Dùng lệnh ls để thấy socket được tạo ra (chú ý ký tự kiểu tập tin là s – socket):
$ ls –lF
srwx-xr-r 1 s01 users 0 Apr 16:10 server_socket
- Dịch và chạy client:
$ gcc client1.c –o client1
$ client1
server waiting...
char from server: B
- Do server và client dùng chung màn hình nên sẽ thấy hai thông điệp.
2
chính xác dữ liệu đến nơi ứng dụng cần. Điều này là do trên một máy có thể có nhiều ứng dụng cùng chạy và cùng sử dụng socket để giao tiếp.
Các ứng dụng trên cùng một máy không được sử dụng trùng số cổng. Do cổng là một giá trị nguyên 2 bytes nên bạn có thể sử dụng khoảng 65535
cổng để tự do đặt cho socket. Trừ các số hiệu cổng quen thuộc như FTP, Web, …, bạn có thể chọn số cổng >1024 để mở cho socket của ứng dụng.
Mở socket theo AF_INET không khác mấy so với AF_UNIX ngoài việc đặt tên và chỉ định số hiệu cổng. Ví dụ:
/* Mở socket theo kết nối IP */
server_sockfd = socket ( AF_INET, SOCK_STREAM, 0 );
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr( "127.0.0.1" );
server_address.sin_port = 1234;
2. Tạo socket
- Hệ thống cung cấp cho bạn hàm socket() để tạo mới một socket. Hàm socket() trả về số nguyên int cho biết số mô tả hay định danh dùng
để truy cập socket sau này, còn gõi là socket handle.
#include <sys/types.h>
#include <sys/socket.h>
int socket( inte domain, int type, int protocol );
+ Tham số domain chỉ định vùng hay họ địa chỉ áp đặt cho socket. domain có thể nhận một trong các giá trị sau:
AF_UNIX Mở socket kết nối theo giao thức tập tin (xuất nhập socket dựa trên xuất nhập tập tin) của UNIX/Linux.
AF_INET Mở socket theo giao thức Internet (sử dụng địa chỉ IP để kết nối).
AF_IPX Vùng giao thức IPX (Mạng Novell).
AF_ISO Chuẩn giao thức ISO.
AF_NS Giao thức Xerox Network System.
Hầu như bạn chỉ sử dụng AF_UNIX và AF_INET là chính. Các vùng giao tiếp khác đã lỗi thời và hiện nay ít còn được sử dụng.
+ Tham số type trong hàm socket() dùng chỉ định kiểu giao tiếp hay truyền dữ liệu của socket. Bạn có thể chỉ định hằng SOCK_STREAM dùng
cho truyền dữ liệu bảo đảm hoặc SOCK_DGRAM dùng cho kiểu truyền không bảo đảm.
+ Tham số protocol dùng để chọn giao thức áp dụng cho kiểu socket (trong trường hợp có nhiều giao thức áp dụng cho một kiểu truyền). Tuy
nhiên bạn chỉ cần đặt giá trị 0 (lấy giao thức mặc định). AF_INET chỉ cài đặt một giao thức duy nhất cho các kiểu truyền SOCK_STREAM và
SOCK_DGRAM, đó là TCP và UDP.
Nếu tạo socket thành công, hàm sẽ trả về số định danh socket (socket handle). Bạn sử dụng số định danh này trong tất cả các lời gọi truy xuất
socket khác như read/write. Đọc/ghi vào socket cũng đồng nghĩa với gửi và nhận dữ liệu giữa trình khách và trình chủ.
Để đóng socket đã mở trước đó, bạn có thể gọi hàm close().
3
};
Trong đó:
struct in_addr {
unsigned long int s_addr;
};
int main()
{
int sockfd; /* số mô tả socket – socket handle */
int len;
struct sockaddr_in address; /* structure sockaddr_in, chứa các thông tin về socket AF_INET */
int result;
char ch = 'A';
/* 2. Tạo socket cho trình khách. Lưu lại số mô tả socket */
sockfd = socket( AF_INET, SOCK_STREAM, 0 );
/* 3. Đặt tên và gán địa chỉ kết nối cho socket theo giao thức Internet */
address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr( "127.0.0.1" );
address.sin_port = htons( 9734 );
len = sizeof( address );
/* 4. Thực hiện kết nối */
4
result = connect( sockfd, (struct sockaddr*)&address, len );
if ( result == -1 ) {
perror( "Oops: client1 problem" );
exit( 1 );
}
/* 5. Sau khi socket kết nối, chúng ta có thể đọc ghi dữ liệu của socket tương tự đọc ghi trên file */
write( sockfd, &ch, 1 );
read ( sockfd, &ch, 1 );
printf( "char from server: %c\n", ch );
close( sockfd );
exit( 0 );
}
server2.c
/* 1.Tạo các #include cần thiết */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
/* dành riêng cho AF_INET */
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
/* 2. Thực hiện khởi tạo socket mới cho trình chủ */
server_sockfd = socket( AF_INET, SOCK_STREAM, 0 );
/* 3. Đặt tên và gán địa chỉ kết nối cho socket theo giao thức Internet */
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr( "127.0.0.1" );
server_address.sin_port = htons( 9734 );
server_len = sizeof( server_address );
/* 4. Ràng buộc tên với socket */
bind( server_sockfd, (struct sockaddr *)&server_address, server_len );
/* 5. Mở hàng đợi nhận kết nối - cho phép đặt hàng vào hàng đợi tối đa 5 kết nối */
listen( server_sockfd, 5 );
/* 6. Lặp vĩnh viễn để chờ và xử lý kết nối của trình khách */
While ( 1 ) {
char ch;
printf( "server waiting...\n" );
/* Chờ và chấp nhận kết nối */
client_sockfd = accept( server_scockfd, (struct sockaddr*)&client_address, &client_len );
/* Đọc dữ liệu do trình khách gửi đến */
read( client_sockfd, &ch, 1 );
ch++;
/* Gửi trả dữ liệu về cho trình khách */
write( client_sockdf, ch,1 );
/* Đóng kết nối */
close( client_sockfd );
}
}
- Tập tin văn bản /etc/host chứa các cặp địa chỉ IP - tên gợi nhớ. Trong chương trình ta có thể dùng tên gợi nhớ (ví dụ “localhost” ) hoặc địa
chỉ IP (ví dụ “127.0.0.1”). Nếu dùng địa chỉ IP khác, cần kiểm tra trước xem địa chỉ đó có tồn tại không:
$ ping 192.168.2.250
PING 192.168.2.250 (192.168.2.250) from 192.168.2.250: 56(84) bytes of data.
64 bytes from 192.168.2.250: icmp_seq=1 ttl=255 time=0.083ms
64 bytes from 192.168.2.250: icmp_seq=2 ttl=255 time=0.089ms
64 bytes from 192.168.2.250: icmp_seq=3 ttl=255 time=0.103ms
64 bytes from 192.168.2.250: icmp_seq=4 ttl=255 time=0.088ms
5
III. Xử lý kết nối đồng thời của nhiều trình khách
- Trong mô hình client/server một trình chủ có thể phục vụ đồng thời cho nhiều trình khách. Ở ví dụ trên, trình chủ gọi hàm accept() chờ kết nối
đến, xử lý xong kết nối rồi mới quay lại nhận kết nối tiếp theo. Đây là cách xử lý tuần tự và thường không phù hợp với việc nhiều trình khách yêu
cầu phục vụ cùng lúc.
- Bạn có thể sử dụng lệnh fork () để kiến tạo tiến trình con mới. Tiến trình con mới này hoạt động độc lập với trình chủ và chịu trách nhiệm phục
vụ trình khách theo cách riêng của nó. Trình chủ hoàn toàn tự do để tiếp nhận ngay kết nối khác. Ngoài cách tạo lập tiến trình con mới bạn có thể
sử dụng cách tạo tuyến (thread). Tuy nhiên tuyến không thường được sử dụng trong UNIX và LINUX bằng tiến trình (process).
- Dưới đây là chương trình cho thấy cách sử dụng mô hình client/server phục vụ kết nối đồng thời từ nhiều trình khách.
server3.c
/*
Như thường lệ, phần đầu đầu là nơi khai báo các tập tin header cần thiết, đồng thời koiwr tạo các biến dùng
cho chương trình. Bạn lưu ý, ta thêm vào signal.h để sử dụng các hằng khai báo xử lý tín hiệu.
*/
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
/*
Tạo hàng đợi để nhận kết nối, yêu cầu bỏ qua tín hiệu kết thúc của các tiến trình con gởi đến tiến trình cha.
Tạo vòng lặp chờ kết nối từ trình khách.
*/
listen (server_sockfd, 5);
signal (SIGCHLD, SIG_IGN);
while (1)
{
char ch;
printf("Server waiting...\n");
/*
Chờ kết nói và chấp nhận kết nối từ trình khách.
*/
client_len = sizeof (client_address);
client_sockfd = accept (server_sockfd, (struct sockaddr*) &client_address, &client_len);
/*
Gọi hàm fork () tạo tiến trình con để xử lý kết nối, kiểm tra xem hiện ta đang là tiến trình cha hay tiến
trình con.
*/
if (fork() == 0)
{
/*
Nếu hiện là tiến trình con, ta hoàn toàn có thể đọc và ghi vào socket client_sockfd. Chúng ta gọi hàm sleep()
dể dừng lại 3 giây để mô phỏng quá trình xử lý thực tế của tiến trình con như kết nối cơ sở dữ liệu, xử lý
nhập xuất…
*/
read (client_sockfd, &ch, 1);
sleep (3);
ch++;
write (client_sockfd, &ch, 1);
close (client_sockfd);
exit (0);
}
else
{
/*
Nếu không, hiện chúng ta đã ở tiến tình cha, quá trình xử lý kết nối đã hoàn tất. socket dành cho client có
thể đóng lại.
*/
close (client_sockfd);
6
}
}
}
- Sử dụng client2 để thực hiện kết nối và tương tác với server3 trên đây. Dưới đây là kết quả xuất khi ta cho chạy ngầm server3 ở hậu cảnh và liên tục thực hiện kết
nối đến server3 bằng client2.
$./server3
[7] 1571
Server waiting…
$ps –ax
PID TTY STAT TIME COMMAND
1577 ppo S 0:00:00 server3
1580 ppo S 0:00:00 ps –ax
[8] Done client2
[9] –Done client2
[10] +Done client2
7
#include <stdio.h>
#include <unistd.h>
#include <ctype.h> /*khai báo các hàm isascii(), toupper()..*/
/*Cài đặt hàm điều khiển tiến trình cha nhận nhập liệu từ phía người dùng*/
void user_handler(int input_pipe[], int output_pipe[])
{
char c; /*Lưu ký tự nhập của người dùng đưa vào*/
int rc; /*Lưu mã trả về của hàm*/
close(input_pipe[1]); /*Tiến trình cha không ghi vào pipe đọc*/
close(output_pipe[0]); /*Tiến trình cha không đọc từ pipe ghi*/
/*Lặp yêu cầu người dùng nhập dữ liệu – nhận dữ liệu và chuyển vào đường ống cho tiến trình
con*/
while((c = getchar())>0)
{
/*Ghi vào đường ống*/
rc = write(output_pipe[1], &c, 1);
if(rc == -1) /*Lỗi ghi*/
{
perror("user_handler: pipe write error");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
/*Tạo đường ống thứ nhất dùng gởi dữ liệu từ tiến trình cha đến tiến trình con*/
rc=pipe(user_to_translator);
if(rc == -1)
{
perror("main: pipe user_to_translator error");
exit(1);
}
/*Tạo đường ống thứ hai dùng gửi dữ liệu từ tiến trình con trở lại tiến trình cha*/
rc=pipe(translator_to_user);
if(rc == -1)
{
perror("main: pipe translator_to_user error");
exit(1);
}
switch(pid)
{
case -1: /*Không thể tạo tiến trình con*/
perror("main: fork error");
exit(1);
case 0: /*Mã lệnh xử lý bên trong tiến trình con*/
translator(user_to_translator,translator_to_user);
default: /*Mã lệnh xử lý bên trong tiến trình cha*/
user_handler(translator_to_user,user_to_translator);
}
return 0;
}