You are on page 1of 56

Bài 1: LINUX VÀ CÁC LỆNH CƠ BẢN

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

3. Một số lệnh cơ bản


Nhóm lệnh Lệnh Mục đích
Gọi sự trợ giúp command –h Hiển thị thông tin trợ giúp ngắn gọn về lệnh.
command -–help Hiển thị thông tin trợ giúp ngắn gọn về lệnh.
man command Hiển thị trang trợ giúp đầy đủ của lệnh.
Liệt kê tập tin (file) ls Liệt kê nội dung của thư mục hiện hành.

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.

II. Nội dung bài thực hành số 1


1. Tạo cây thư mục
Tạo cây thư mục như sau:
home
dsl
CTH
user1
user2
Sử dụng lệnh mkdir để tạo thư mục con:

2.Tạo tập tin


Lần lượt tạo các tập tin test1.c, test2.c nằm trong thư mục user1 - tập tin test3.c, test4.c nằm
trong thư mục user2
Để tạo file bạn có 2 cách , cách thứ nhất là tạo file rỗng bằng lệnh touch:
$touch test1.c
Tương tự ta tạo các file: test2.c, test3.c, test4.c
Như bạn thấy kích thước các file được tạo ra bởi lệnh touch là 0 bytes. Bạn có thể dùng trình soạn thảo vi để bổ
sung cho file sau này.
Cách thứ 2 là dùng lệnh cat với định hướng đầu ra là tên file như ví dụ sau:

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

4. Di chuyển file và thư mục


Bạn dùng lệnh mv để di chuyển hoặc đổi tên file. Trong Linux đổi tên file cũng tương tự như di chuyển file. Ví dụ:
-Di chuyển test4.c từ user2 sang user1
-Kiểm tra những tập tin trong user1 và user2
Để đổi tên test4.c trong thư mục hiện hành thành test4.doc
$mv test4.c test4.doc
Để di chuyển các file .doc và .c vào thư mục /tmp
$mv *.doc *.c /tmp
Nếu bạn muốn chuyển user1 trong thư mục hiện hành vào user2 với tên tên mới là NewDir bạn gọi mv
$mv user1/ user2/NewDir

4. Nén, giải nén


Nén thành tập tin .tar:
- Nén tập tin test4.c trong thư mục user2 thành tập tin test4.c.tar
- Liệt kê danh sách các file trong thư mục user2
- Xóa tập tin test4.c
- Giải nén tập tin test4.c.tar

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.

2. Các tập tin tiêu đề (header)


- Các tập tin tiêu đề trong C thường định nghĩa hàm và khai báo cần thiết cho quá trình biên dịch.
Hầu hết các chương trình trên Linux khi biên dịch sử dụng các tập tin tiêu đề trong thư mục
/usr/include hoặc các thư mục con bên trong thư mục này, ví dụ: /usr/include/sys. Một
số khác được trình biên dịch dò tìm mặc định như /usr/include/X11 đối với các khai báo hàm
lập trình đồ họa X-Window, hoặc /usr/include/g++-2 đối với trình biên dịch GNU g++.
Tuy nhiên, nếu chúng ta có các tập tin tiêu đề của riêng mình trong một thư mục khác thư mục mặc
định của hệ thống thì chúng ta có thể chỉ rõ tường minh đường dẫn đến thư mục khi biên dịch bằng
tùy chọn –I, ví dụ:
$ gcc –I/usr/mypro/include test.c –otest
- Khi chúng ta sử dụng một hàm nào đó của thư viện hệ thống trong chương trình C, ngoài việc phải
biết khai báo nguyên mẫu của hàm, chúng ta cần phải biết hàm này được định nghĩa trong tập tin
tiêu đề nào. Trình man sẽ cung cấp cho chúng ta các thông tin này rất chi tiết. Ví dụ, khi dùng man
để tham khảo thông tin về hàm kill(), chúng ta sẽ thấy rằng cần phải khai báo 2 tập tin tiêu đề là
types.h và signal.h.

3. Các tập tin thư viện


- Các tập tin tiêu đề của C chỉ cần thiết để trình biên dịch bắt lỗi cú pháp, kiểm tra kiểu dữ liệu của
chương trình và tạo ra các tập tin đối tượng. Muốn tạo ra chương trình thực thi, chúng ta cần phải có
các tập tin thư viện. Trong Linux, các tập tin thư viện tĩnh của C có phần mở rộng là .a, .so, .sa
và bắt đầu bằng tiếp đầu ngữ lib. Ví dụ libutil.a hay libc.so là tên các thư viện liên kết
trong Linux.
- Linux có hai loại liên kết là liên kết tĩnh (static) và liên kết động (dynamic). Thư viện liên kết động
trên Linux thường có phần mở rộng là .so, chúng ta có thể dùng lệnh ls /usr/lib hoặc ls
/lib để xem các thư viện hệ thống đang sử dụng. Khi biên dịch, thông thường trình liên kết (ld)
sẽ tìm thư viện trong 2 thư viện chuẩn /usr/lib và /lib. Để chỉ định tường minh một thư viện
nào đó, chúng ta làm như sau:
$ gcc test.c –otest /usr/lib/libm.a
Bởi vì thư viện bắt buộc phải có tiếp đầu ngữ lib và có phần mở rộng là .a hoặc .so, trình biên
dịch cho phép chúng ta sử dụng tùy chọn –l ngắn gọn như sau:
$ gcc test.c –otest -lm
1
chúng ta sẽ thấy rằng gcc sẽ mở rộng –l thành tiếp đầu ngữ lib và tìm libm.a hoặc libm.so
trong thư mục chuẩn để liên kết.
- Mặc dù vậy, không phải lúc nào thư viện của chúng ta cũng phải nằm trong thư viện của Linux.
Nếu thư viện của chúng ta nằm ở một thư mục khác, chúng ta có thể chỉ định gcc tìm kiếm trực tiếp
với tùy chọn –L như sau:
$ gcc test.c –otest -L/usr/myproj/lib -ltool
Lệnh trên cho phép liên kết với thư viện libtool.a hoặc libtool.so trong thư mục
/usr/myproj/lib.

4. Thư viện liên kết trên Linux


- Hình thức đơn giản nhất của thư viện là tập hợp các tập tin .o do trình biên dịch tạo ra ở bước
biên dịch với tùy chọn –c. Ví dụ
$gcc –c helloworld.c
trình biên dịch chưa tạo ra tập tin thực thi mà tạo ra tập tin đối tượng helloworld.o. Tập tin này
chứa các mã máy của chương trình đã được sắp xếp lại. Nếu muốn tạo ra tập tin thực thi, chúng ta
gọi trình biên dịch thực hiện bước liên kết:
$gcc helloworld.o –o helloworld
Trình biên dịch sẽ gọi tiếp trình liên kết ld tạo ra định dạng tập tin thực thi cuối cùng. Ở đây, nếu
chúng ta không sử dụng tùy chọn –c, trình biên dịch sẽ thực hiện cả hai bước đồng thời.

a) Thư viện liên kết tĩnh


- Thư viện liên kết tĩnh là các thư viện khi liên kết trình biên dịch sẽ lấy toàn bộ mã thực thi của hàm
trong thư viện đưa vào chương trình chính. Chương trình sử dụng thư viện liên kết tĩnh chạy độc lập
với thư viện sau khi biên dịch xong. Nhưng khi nâng cấp và sửa đổi, muốn tận dụng những chức
năng mới của thư viện thì chúng ta phải biên dịch lại chương trình.
Ví dụ sử dụng liên kết tĩnh:
/* cong.c */
int cong( int a, int b )
{
return a + b;
}

/* 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

b) Thư viện liên kết động


- Khuyết điểm của thư viện liên kết tĩnh là nhúng mã nhị phân kèm theo chương trình khi biên dịch,
do đó tốn không gian đĩa và khó nâng cấp. Thư viện liên kết động được dùng để giải quyết vấn đề
này. Các hàm trong thư viện liên kết động không trực tiếp đưa vào chương trình lúc biên dịch và
liên kết, trình liên kết chỉ lưu thông tin tham chiếu đến các hàm trong thư viện liên kết động. Vào
lúc chương trình nhị phân thực thi, Hệ Điều Hành sẽ nạp các chương trình liên kết cần tham chiếu
vào bộ nhớ. Như vậy, nhiều chương trình có thể sử dụng chung các hàm trong một thư viện duy
nhất.
- Tạo thư viện liên kết động:
Khi biên dịch tập tin đối tượng để đưa vào thư viện liên kết động, chúng ta phải thêm tùy chọn –
fpic (PIC- Position Independence Code – mã lệnh vị trí độc lập).
Ví dụ: biên dịch lại 2 tập tin cong.c và nhan.c
$ gcc –c –fpic cong.c nhan.c

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;
}

$ gcc –c cong.c nhan.c


$ ar cvr libfoo.a cong.o nhan.o
$ gcc program.c –o program –L. –lfoo
$ ./program

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;
}

$ gcc –c –fpic cong.c nhan.c


$ gcc –shared cong.o nhan.o –o libfoo.so
$ gcc program.c –o program –L. –lfoo
$ LD_LIBRARY_PATH=.:
$ export LD_LIBRARY_PATH
$ ./program

4. Bài tập thêm


4.1. Viết chương trình nhập, xuất mảng số nguyên(sử dụng thư viện liên kết động).
4.2. Tạo thư mục /home/dsl/lib
- Chép thư viện libfoo.so tạo được ở câu 4.1 vào thư mục vừa tạo.
- Biên dịch và chạy lại chương trình.

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. Cách hoạt động của tiến trình


- Khi 1 chương trình đang chạy từ dòng lệnh, chúng ta có thể nhấn phím Ctrl+z để tạm dùng chương trình
và đưa nó vào hoạt động phía hậu trường (background). Tiến trình của Linux có các trạng thái:
+ Đang chạy (running) : đây là lúc tiến trình chiếm quyền xử lý CPU dùng tính toán hay thực các công việc
của mình.
+ Chờ (waiting) : tiến trình bị Hệ Điều Hành tước quyền xử lý CPU, và chờ đến lược cấp phát khác.
+ Tạm dừng (suspend) : Hệ Điều Hành tạm dừng tiến trình. Tiến trình được đưa vào trạng thái ngủ (sleep).
Khi cần thiết và có nhu cầu, Hệ Điều Hành sẽ đánh thức (wake up) hay nạp lại mã lệnh của tiến trình vào bộ
nhớ. Cấp phát tài nguyên CPU để tiến trình tiếp tục hoạt động.
- Trên dòng lệnh, thay vì dùng lệnh Ctrl+z, chúng ta có thể sử dụng lệnh bg để đưa một tiến trình vào hoạt
động phía hậu trường. Chúng ta cũng có thể yêu cầu 1 tiến trình chạy nền bằng cú pháp &. Ví dụ: $ls –R &
Lệnh fg sẽ đem tiến trình trở về hoạt động ưu tiên phía trước. Thực tế khi chúng ta đăng nhập vào hệ thống
và tương tác trên dòng lệnh, cũng là lúc chúng ta đang ở trong tiến trình shell của bash. Khi gọi một lệnh có
nghĩa là chúng ta đã yêu cầu bash tạo thêm một tiến trình con thực thi khác. Về mặt lập trình, chúng ta có thể
dùng lệnh fork() để nhân bản tiến trình mới từ tiến trình cũ. Hoặc dùng lệnh system() để triệu gọi một
tiến trình của Hệ Điều Hành. Hàm exec() cũng có khả năng tạo ra tiến trình mới khác.

3. Cấu trúc tiến trình


- Chúng ta hãy xem Hệ Điều Hành quản lý tiến trình như thế nào? user1 user2
$grep abc file1 $grep cde file2
Nếu có hai người dùng: user1 và user2 cùng đăng nhập vào
chạy chương trình grep đồng thời, thực tế, Hệ Điều Hành sẽ PID 101 PID 102
quản lý và nạp mã của chương trình grep vào hai vùng nhớ khác Code mã lệnh grep Code
nhau và gọi mỗi phân vùng như vậy là tiến trình. Hình sau cho
thấy cách phân chia chương trình grep thành hai tiến trình cho Data Data
s=abc s=cde
hai người khác nhau sử dụng
Trong hình này, user1 chạy chương trình grep tìm chuỗi abc
Library Thư viện C Library
trong tập tin file1.
$grep abc file1 filede Tiến trình quản lý filede
user2 chạy chương trình grep và tìm chuỗi cde trong tập tin bởi Hệ Điều Hành
file1 file2
1
file2.
$grep cde file2
Chúng ta cần ta cần nhớ là hai người dùng user1 và user2 có thể ở hai máy tính khác nhau đăng nhập vào
máy chủ Linux và gọi grep chạy đồng thời. Hình trên là hiện trạng không gian bộ nhớ Hệ Điều Hành Linux
khi chương trình grep phục vụ người dùng.
- Nếu dùng lệnh ps, hệ thống sẽ liệt kê cho chúng ta thông tin về các tiến trình mà Hệ Điều Hành đang kiểm
soát, Ví dụ: $ps –af
Mỗi tiến trình được gán cho một định danh để nhận dạng gọi là PID (process identify). PID thường là số
nguyên dương có giá trị từ 2-32768. Khi một tiến trình mới yêu cầu khởi động, Hệ Điều Hành sẽ chọn lấy một
số (chưa bị tiến trình nào đang chạy chiếm giữ) trong khoảng số nguyên trên và cấp phát cho tiến trình mới.
Khi tiến trình chấm dứt, hệ thống sẽ thu hồi số PID để cấp phát cho tiến trình khác trong lần sau. PID bắt đầu
từ giá trị 2 bởi vì giá trị 1 được dành cho tiến trình đầu tiên gọi là init. Tiến trình init được và chạy ngay
khi chúng ta khởi động Hệ Điều Hành. init là tiến trình quản lý và tạo ra mọi tiến trình con khác. Ở ví dụ
trên, chúng ta thấy lệnh ps –af sẽ hiển thị 2 tiến trình grep chạy bởi user1 và user2 với số PID lần
lượt là 101 và 102.
- Mã lệnh thực thi của lệnh grep chứa trong tập tin chương trình nằm trên đĩa cứng được Hệ Điều Hành nạp
vào vùng nhớ. Như chúng ta đã thấy ở lược đồ trên, mỗi tiến trình được Hệ Điều hành phân chia rõ ràng: vùng
chứa mã lệnh (code) và vùng chứa dữ liệu (data). Mã lệnh thường là giống nhau và có thể sử dụng chung.
Linux quản lý cho phép tiến trình của cùng một chương trình có thể sử dụng chung mã lệnh của nhau.
Thư viện cũng vậy. Trừ những thư viện đặc thù còn thì các thư viện chuẩn sẽ được Hệ Điều Hành cho phép
chia sẻ và dùng chung bởi mọi tiến trình trong hệ thống. Bằng cách chia sẻ thư viện, kích thước chương trình
giảm đi đáng kể. Mã lệnh của chương trình khi chạy trong hệ thống ở dạng tiến trình cũng sẽ đỡ tốn bộ nhớ
hơn.
- Trừ mã lệnh và thư viện có thể chia sẻ, còn dữ liệu thì không thể chia sẻ bởi các tiến trình. Mỗi tiến trình sở
hữu phân đoạn dữ liệu riêng. Ví dụ tiến trình grep do user1 nắm giữ lưu giữ biến s có giá trị là 'abc',
trong khi grep do user2 nắm giữ lại có biến s với giá trị là 'cde'.
Mỗi tiến trình cũng được hệ thống dành riêng cho một bảng mô tả file (file description table). Bảng này chứa
các số mô tả áp đặt cho các file đang được mở. Khi mỗi tiến trình khởi động, thường Hệ Điều Hành sẽ mở sẳn
cho chúng ta 3 file : stdin (số mô tả 0), stdout (số mô tả 1), và stderr (số mô tả 2). Các file này tượng
trưng cho các thiết bị nhập, xuất, và thông báo lỗi. Chúng ta có thể mở thêm các file khác. Ví dụ user1 mở
file file1, và user2 mở file file2. Hệ Điều Hành cấp phát số mô tả file cho mỗi tiến trình và lưu riêng
chúng trong bảng mô tả file của tiến trình đó.
- Ngoài ra, mỗi tiến trình có riêng ngăn xếp stack để lưu biến cục bộ và các giá trị trả về sau lời gọi hàm. Tiến
trình cũng được dành cho khoảng không gian riêng để lưu các biến môi trường. Chúng ta sẽ dùng lệnh
putenv và getenv để đặt riêng biến môi trường cho tiến trình.
a) Bảng thông tin tiến trình
- Hệ Điều Hành lưu giữ một cấu trúc danh sách bên trong hệ thống gọi là bảng tiến trình (process table). Bảng
tiến trình quản lý tất cả PID của hệ thống cùng với thông tin chi tiết về các tiến trình đang chạy. Ví dụng khi
chúng ta gọi lệnh ps, Linux thường đọc thông tin trong bảng tiến trình này và hiển thị những lệnh hay tên tiến
trình được gọi: thời gian chiếm giữ CPU của tiến trình, tên người sử dụng tiến trình, …
b) Xem thông tin của tiến trình
- Lệnh ps của Hệ Điều Hành dùng để hiển thị thông tin chi tiết về tiến trình. Tùy theo tham số, ps sẽ cho biết
thông tin về tiến trình người dùng, tiến trình của hệ thống hoặc tất cả các tiến trình đang chạy. Ví dụ ps sẽ
đưa ra chi tiết bằng tham số -af
- Trong các thông tin do ps trả về, UID là tên của người dùng đã gọi tiến trình, PID là số định danh mà hệ
thống cấp cho tiến trình, PPID là số định danh của tiến trình cha (parent PID). Ở đây chúng ta sẽ gặp một số
tiến trình có định danh PPID là 1, là định danh của tiến trình init, được gọi chạy khi hệ thống khởi động.
Nếu chúng ta hủy tiến trình init thì Hệ Điều Hành sẽ chấm dứt phiên làm việc. STIME là thời điểm tiến
trình được đưa vào sử dụng. TIME là thời gian chiếm dụng CPU của tiến trình. CMD là toàn bộ dòng lệnh khi
tiến trình được triệu gọi. TTY là màn hình terminal ảo nơi gọi thực thi tiến trình. Như chúng ta đã biết, người
dùng có thể đăng nhập vào hệ thống Linux từ rất nhiều terminal khác nhau để gọi tiến trình. Để liệt kê các tiến
trình hệ thống, chúng ta sử dụng lệnh: $ps –ax

4. Tạo lập tiến trình


a) Gọi tiến trình mới bằng hàm system()
- Chúng ta có thể gọi một tiến trình khác bên trong một chương trình đang thực thi bằng hàm system(). Có
nghĩa là chúng ta có thể tạo ra một tiến trình mới từ một tiến trình đang chạy. Hàm system() được khai báo
như sau:
#include <stdlib.h>
int system( const char (cmdstr) )
Hàm này gọi chuỗi lệnh cmdstr thực thi và chờ lệnh chấm dứt mới quay về nơi gọi hàm. Nó tương đương

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”);

printf( "Thuc hien xong. \n" );


exit( 0 );
}
Hàm system() của chúng ta được sử dụng để gọi lệnh “ps –ax” của Hệ Điều Hành.
b) Thay thế tiến trình hiện hành với các hàm exec
- Mỗi tiến trình được Hệ Điều Hành cấp cho 1 không gian nhớ tách biệt để tiến trình tự do hoạt động. Nếu tiến
trình A của chúng ta triệu gọi một chương trình ngoài B (bằng hàm system()chẳng hạn), Hệ Điều Hành
thường thực hiện các thao tác như: cấp phát không gian bộ nhớ cho tiến trình mới, điều chỉnh lại danh sách
các tiến trình, nạp mã lệnh của chương trình B trên đĩa cứng và không gian nhớ vừa cấp phát cho tiến trình.
Đưa tiến trình mới vào danh sách cần điều phối của Hệ Điều Hành. Những công việc này thường mất thời
gian đáng kể và chiếm giữ thêm tài nguyên của hệ thống.
Nếu tiến trình A đang chạy và nếu chúng ta muốn tiến trình B khởi động chạy trong không gian bộ nhớ đã có
sẵn của tiến trình A thì có thể sử dụng các hàm exec được cung cấp bới Linux. Các hàm exec sẽ thay thế
toàn bộ ảnh của tiến trình A (bao gồm mã lệnh, dữ liệu, bảng mô tả file) thành ảnh của một tiến trình B hoàn
toàn khác. Chỉ có số định danh PID của tiến trình A là còn giữ lại. Tập hàm exec bao gồm các hàm sau:
#include <unistd.h>
extern char **environ;
int execl( const char *path, const char *arg, ... );
int execlp( const char *file, const char *arg, ... );
int execle( const char *path, const char *arg, ..., char *const envp[] );
int exect( const char *path, char *const argv[] );
int execv( const char *path, char *const argv[] );
int execvp( const char *file, char *const argv[] );
- Đa số các hàm này đều yêu cầu chúng ta chỉ đối số path hoặc file là đường dẫn đến tên chương trình cần
thực thi trên đĩa. arg là các đối số cần truyền cho chương trình thực thi, những đối số này tương tự cách
chúng ta gọi chương trình từ dòng lệnh.
c) Nhân bản tiến trình với hàm fork()
- Thay thế tiến trình đôi khi bất lợi với chúng ta. Đó là
Khởi tạo tiến trình mới chiếm giữ toàn bộ không gian của tiến
tiến trình chính trình cũ và chúng ta sẽ không có khả năng kiểm soát
cũng như điều khiển tiếp tiến trình hiện hành của mình
sau khi gọi hàm exec nữa. Cách đơn giản mà các
chương trình Linux thường dùng đó là sử dụng hàm
Gọi fork() fork() để nhân bản hay tạo bản sao mới của tiến trình.
fork() là một hàm khá đặc biệt, khi thực thi, nó sẽ trả
về 2 giá trị khác nhau trong lần thực thi, so với hàm bình
Trả về PID của tiến trình thường chỉ trả về 1 giá trị trong lần thực thi. Khai báo
con Trả về trị 0 của hàm fork() như sau:
#include <sys/types.h>
#include <unistd.h>
pid_t fork()
- Nếu thành công, fork() sẽ tách tiến trình hiện hành 2
Mã lệnh tiếp của Mã lệnh thực thi tiến trình (dĩ nhiên Hệ Điều Hành phải cấp phát thêm
tiến trình ban đầu tiến trình mới (tiến không gian bộ nhớ để tiến trình mới hoạt động). Tiến
(tiến trình cha) trình con) trình ban đầu gọi là tiến trình cha (parent process) trong
khi tiến trình mới gọi là tiến trình con (child process).
Tiến trình con sẽ có một số định danh PID riêng biệt.
ngoài ra, tiến trình con còn mang thêm một định danh
PPID là số định danh PID của tiến trình cha.
Cơ chế phân chia tiến trình của fork()

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

II. Xử lý tín hiệu (signals handling)

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.

2. Gởi tín hiệu đến tiến trình


Tiến trình có thể nhận tín hiệu từ hệ điều hành hoặc các tiến trình khác gởi đến. Các cách gởi tín hiệu đến tiến trình:

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 đó.

c) Bằng các hàm hệ thống kill():


#include <unistd.h>
#include <sys/types.h>
#include <signal.h> /* macro xử lý tín hiệu và hàm kill() */

pid_t my_pid = getpid() /* lấy định danh tiến trình */
kill( my_pid, SIGSTOP ); /* gửi tín hiệu STOP đến tiến trình */

3. Đón bắt xử lý tín hiệu


- Một số tín hiệu hệ thống (như KILL, STOP) không thể đón bắt hay bỏ qua được.
- Tuy nhiên, có rất nhiều tín hiệu mà bạn có thể đón bắt, bao gồm cả những tín hiệu nổi tiếng như SEGV và BUS.

a) Bộ xử lý tín hiệu mặc định


Hệ thống đã dành sẵn các hàm mặc định xử lý tín hiệu cho mỗi tiến trình. Ví dụ, bộ xử lý mặc định cho tín hiệu TERM gọi là hàm
exit() chấm dứt tiến trình hiện hành. Bộ xử lý dành cho tín hiệu ABRT là gọi hàm hệ thống abort() để tạo ra file core lưu
xuống thư mục hiện hành và thoát chương trình. Mặc dù vậy đối với một số tín hiệu bạn có thể cài đặt hàm thay thế bộ xử lý tín
hiệu mặc định của hệ thống. Chúng ta sẽ xem xét vấn đề này ngay sau đây:

b) Cài đặt bộ xử lý tín hiệu


Có nhiều cách thiết lập bộ xử lý tín hiệu (signal handler) thay cho bộ xử lý tín hiệu mặc định. Ở đây ta dùng cách cơ bản nhất đó là
gọi hàm signal().
#include <signal.h>
void signal( int signum, void (*sighanldler)( int ) );

III. Đường ống (pipe)


1. Khái niệm
- Các tiến trình chạy độc lập có thể chia sẻ hoặc chuyển dữ liệu cho nhau xử lý thông qua cơ chế đường ống (pipe).
Ví dụ: ps –ax | grep ls
1
- Trên đường ống dữ liệu chỉ có thể chuyển đi theo một chiều, dữ liệu vào đường ống tương đương với thao tác ghi (pipe write), lấy
dữ liệu từ đường ống tương đương với thao tác đọc (pipe read). Dữ liệu được chuyển theo luồng (stream) theo cơ chế FIFO.

2. Tạo đường ống


Hệ thống cung cấp hàm pipe() để tạo đường ống có khả năng đọc / ghi. Sau khi tạo ra, có thể dùng đường ống để giao tiếp giữa
hai tiến trình. Đọc / ghi đường ống hoàn toàn tương đương với đọc / ghi file.
#include <unistd.h>
int pipe( int filedes[2] );
Mảng filedes gồm hai phần tử nguyên dùng lưu lại số mô tả cho đường ống trả về sau lời gọi hàm, ta dùng hai số này để thực
hiện thao tác đọc / ghi trên đường ống: phần tử thứ nhất dùng để đọc, phần tử thứ hai dùng để ghi.
int pipes[2];
int rc = pipe( pipes ); /*Tạo đường ống*/
if ( rc == -1 ) /*Có tạo đường ống được không?*/
{
perror( "Error: pipe not created" );
exit( 1 );
}

3. Đường ống hai chiều


Sử dụng cơ chế giao tiếp đường ống hai chiều dễ dàng cho cả hai phía tiến trình cha và tiến trình con. Các tiến trình dùng một
đường ống để đọc và một đường ống để ghi. Tuy nhiên cũng rất dễ gây ra tình trạng tắc nghẽn “deadlock”:
- Cả hai đường ống đều rỗng nếu đường ống rỗng hàm read() sẽ block cho đến khi có dữ liệu đổ vào hoặc khi đường ống bị
đóng bởi bên ghi.
- Cả hai tiến trình cùng ghi dữ liệu: vùng đệm của một đường ống bị đầy, hàm write() sẽ block cho đến khi dữ liệu được lấy bớt
ra từ một thao tác đọc read().

4. Đường ống có đặt tên


Đường ống được tạo ra từ hàm pipe() được gọi là đường ống vô danh (anonymouse pipe). Nó chỉ được sử dụng giữa các tiến trình
cha con do bạn chủ động điều khiển tạo ra từ hàm fork(). Một vấn đề đặt ra, nếu hai tiến trình không quan hệ gì với nhau thì có thể
sử dụng được cơ chế pipe để trao đổi dữ liệu hay không ? Câu trả lời là có. Linux cho phép bạn tạo ra các đường ống đặt tên (named
pipe). Những đường ống mang tên sẽ nhìn thấy và truy xuất bởi các tiến trình khác nhau.

a) Tạo pipe đặt tên với hàm mkfifo()


Đường ống có đặt tên gọi là đối tượng FIFO, được biểu diễn như một file trong hệ thống. Vì vậy có thể dùng lệnh mkfifo() để
tạo file đường ống với tên chỉ định.
#include <sys/types.h>
#include <sys/stat.h>
mkfifo( const char *filename, mode_t mode );
Đối số thứ nhất là tên đường ống cần tạo, đối số thứ hai là chế độ đọc ghi của đường ống. Ví dụ:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int res = mkfifo( "~/tmp/my_fifo", 0777 );
if ( res == 0 ) printf( "FIFO object created" );
exit ( EXIT_SUCCESS );
}
- Có thể xem file đường ống này trong thư mục tạo nó.
- Có thể tạo đường ống có đặt tên từ dòng lệnh, ví dụ:
mkfifo ~/tmp/my_fifo --mode=0777

b) Đọc / ghi trên đường ống có đặt tên


- Dùng dòng lệnh với > (ghi dữ liệu) hoặc < (đọc dữ liệu), ví dụ:
echo Hello world! > ~/tmp/my_fifo
cat < /tmp/my_fifo
hoặc:
echo Hello world! > ~/tmp/my_fifo & cat < ~/tmp/my_fifo
- Lập trình: thao tác trên đường ống có đặt tên giống như thao tác trên file nhưng chỉ có chế độ O_RDONLY (chỉ đọc) hoặc
O_WRONLY (chỉ ghi).

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()*/

/*Trước hết cài đặt hàm xử lý tín hiệu*/


void catch_int( int sig_num )
{
signal( SIGINT, catch_int );
/*Thực hiện công việc của bạn ở đây*/
printf( "Do not press Ctrl+C\n" );
}
/*Chương trình chính*/
int main()
{
int count = 0;
/*Thiết lập hàm xử lý cho tín hiệu INT(Ctrl + C)*/
signal( SIGINT, catch_int ); /*Đặt bẫy tín hiệu INT*/
while ( 1 )
{
printf( "Counting … %d\n", count++ );
sleep( 1 );
}
}

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 );
}

/*Cài đặt hàm xử lý công việc của tiến trình cha*/


void do_parent( int data_pipes[] )
{
int c; /*Dữ liệu đọc được do người dùng nhập vào*/
int rc; /*Lưu trạng thái trả về của write()*/
/*Tiến trình cha chỉ ghi đường ống nên đóng đầu đọc do không cần*/
close( data_pipes[0] );
/*Nhận dữ liệu do người dùng nhập vào và ghi vào đường ống */
while ( ( c = getchar() ) > 0 )
{
/*Ghi dữ liệu vào đường ống*/
rc = write( data_pipes[1], &c, 1 );
if ( rc == -1 )
{
perror( "Parent: pipe write error" );
close( data_pipes[1] );
exit( 1 );
}
}
/*Đóng đường ống phía đầu ghi để thông báo cho phía cuối đường ống dữ liệu đã hết*/
close(data_pipe[1]);
exit(0);
}

/*Chương trình chính*/


int main()
{
int data_pipes[2]; /*Mảng chứa số mô tả đọc ghi của đường ống*/
int pid; /*pid của tiến trình con*/
int rc; /*Lưu mã lỗi trả về*/
rc = pipe( data_pipes ); /*Tạo đường ống*/
if ( rc == -1 )

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];

/*Tạo pipe nếu chưa có*/


if ( access( FIFO_NAME, F_OK ) == -1 )
{
res = mkfifo( FIFO_NAME, (S_IRUSR | S_IWUSR) );
if ( res != 0 )
{
fprintf( stderr, "FIFO object not created [%s]\n", FIFO_NAME);
exit( EXIT_FAILURE );
}
}
/*Mở đường ống để ghi*/
printf( "Process %d starting to write on pipe\n", getpid() );
pipe_fd = open( FIFO_NAME, open_mode);
if ( pipe_fd != -1 )
{
/*Liên tục đổ vào đường ống*/
while ( bytes_sent < TEN_MEG )
{
res = write( pipe_fd, buffer, BUFFER_SIZE );
if ( res == -1 )
{
fprintf( stderr, "Write error on pipe\n" );
exit( EXIT_FAILURE );
}
bytes_sent += res;
}
/*Kết thúc quá trình ghi dữ liệu*/
( void ) close( pipe_fd );
}
else
{

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

-rw-r--r-- 1 bibica hacker 207 Apr 10 11.30 mydata

1. Kiểu tập tin:


Thể hiện bằng ký tự đầu tiên trong chuỗi thuộc tính của tập tin.
Ký tự đầu Giải thích
- File thông thường
- Hard link
d Thư mục
l Link mềm (symbolic link)
c/b Character (c) / Block device (b)
s Domain socket
p Name pipe

2. Quyền tập tin


rwx rwx rwx

owner group other

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}.

r: Read  Thuộc tính đọc (không có quyền ghi/xóa)


w: Write  Thuộc tính ghi (hiệu chỉnh nội dung)
x: Execute  Thuộc tính thực thi (chạy chương trình)
-: None  Không có quyền trên đối tượng

- 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)

Một bộ: Có quyền: Một bộ:


u (user, tức owner) = (gán quyền) r (read – đọc)
g (group – nhóm) + (thêm quyền) w (write - ghi)
o (other – người dùng còn lại) - (thu hồi quyền) x (execute – thực thi)
a (all –tương đương ugo) hoặc một trong các đối tượng sau để sao chép quyền:
u (user, tức owner)
g (group – nhóm)
o (other – người dùng còn lại) để thiết lập user id,
group id: s
Ví dụ:
$ chmod u+rx-w /tmp
$ chmod –R u+rwx,g+r-w,o-rwx /tmp
$ chmod a=rx abc.txt
- Phương pháp tuyệt đối:
Song song với cách ký hiệu miêu tả bằng ký tự như ở trên, quyền thao tác tập tin còn có thể cho dưới dạng 3 số, ví dụ quyền 644.
Các số có thể nhận tất cả các giá trị từ 0 đến 7. Số đầu tiên miêu tả quyền của owner (sở hữu), số thứ hai cho group (nhóm) và
số thứ ba cho other (những người còn lại).
Mỗi số là tổng của các quyền theo quy tắc sau :
Quyền đọc 4
Quyền ghi 2
Quyền thực thi 1
Kết hợp như sau (cho một đối tượng u, o hoặc g)
Số bát phân Số nhị phân Quyền Giải thích
0 000 --- không có mọi quyền
1 001 --x quyền thực thi
2 010 -w- chỉ ghi (hiếm gặp)
3 011 -wx ghi và thực thi (hiếm gặp)
4 100 r-- chỉ đọc
5 101 r-x đọc và thực thi
6 110 rw- đọc và ghi
7 111 rwx đọc, ghi và thực thi
Ví dụ:
$ chmod 751 abc.txt
có nghĩa là owner có quyền read, write, và execute (4+2+1=7), group có quyền read và execute (4+1=5), và
other chỉ có quyền execute (1). Xem cách tính toán trong bảng sau:
owner group other
r w x r w x r w x

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).

$ln filename othername

- 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.

$ln –s filename othername

4. Thay đổi chủ sở hữu


- Tạo người dùng mới tên user1: useradd user1
- Tạo một tập tin test1.txt ở thư mục gốc “/”: touch /test1.txt
- Thay đổi quyền ownership của tập tin text1.txt là user1: chown user1 /test1.txt
- Kiểm tra lại: ls –l | grep test1.txt
Lưu ý: Nếu muốn thay đổi ownership cho một thư mục và các thư mục con bên trong thì ta dùng tùy chọn (-R) cho lệnh
chown.VD: chown –R user1 /test

5. Thay đổi nhóm sở hữu


- Tạo nhóm mới tên group1: groupadd group1
- Thay đổi group sở hữu của tập tin test1.txt là group1: chgrp group1 /test1.txt
- Kiểm tra lại: ls –l /
Lưu ý: Nếu muốn thay đổi group sở hữu cho một thư mục và các thư mục con bên trong ta dùng tùy chọn (-R) cho lệnh chgrp.
VD: chgrp –R group1 /test

II. Điều khiển tiến trình


1. Định hướng nhập xuất
Các tiến trình thường nhận dữ liệu đầu vào xử lý và ghi kết xuất ra một nơi nào đó. Linux quy định cơ bản đầu vào là bàn phím
stdin (thiết bị nhập chuẩn) và đầu ra là màn hình stdout (thiết bị xuất chuẩn).
Ví dụ: lệnh ls –l sẽ lấy thông số dòng lệnh gõ vào từ bàn phím đọc duyệt thư mục và in kết quả ra màn hình.
$ls –l
-rw-rw-r-- 1 s01 student 12 Apr 3 21:55 tmp

Linux cung cấp cơ chế chuyển hướng xuất nhập. Ký hiệu lệnh > dùng để chuyển hướng kết xuất đầu ra trong khi < dùng để
chuyển hướng kết xuất đầu vào.
Ví dụ: sử dụng lệnh ls sau đó ghi kết xuất ra file data.txt
$ls –l >data.txt
Nếu muốn kết xuất của lệnh ghi nối đuôi vào file hiện có, bạn dùng chuyển hướng >>. Ví dụ:
$ls –l >>data.txt
Lệnh more của Linux cho phép hiển thị dữ liệu của đầu vào theo từng trang. Bạn có thể chuyển cho lệnh more nội dung file
bằng định hướng đầu vào như sau:
$more <bigfile.txt

2. Kiểm soát tiến trình


a. Xem thông tin về tiến trình
Muốn xem các tiến trình đang chạy trong hệ thống Linux hiện hành bạn gọi lệnh ps
$ps
PID TTY TIME CMD
128 tty1 00:00:00 bash
137 pts/9 00:00:00 mc
235 pts/0 00:00:00 bash

Lệnh ps tương tự chức năng Task list trên Window khi bạn nhấn Ctrl – Alt – Del. Lệnh ps có rất nhiều tùy chọn. Ví dụ
ta sử dụng tùy chọn –a (all) yêu cầu liệt kê tất cả các tiến trình trong Linux.

b. Tiến trình tiền cảnh


Khi đang trên dấu nhắc hệ thống (# hoặc $) và gọi một chương trình, chương trình trở thành tiến trình và đi vào hoạt động dưới
sự kiểm soát của hệ thống. Dấu nhắc hệ thống sẽ không hiển thị khi tiến trình đang chạy. Khi tiến trình hoàn thành tác vụ và chấm
dứt, hệ điều hành (chính xác hơn là hệ vỏ Shell) sẽ trả lại dấu nhắc để bạn gõ lệnh thực hiện chương trình khác. Chương trình hoạt
động theo cách này gọi là chương trình tiền cảnh (foreground). Ví dụ: lện ls –R liệt là lệnh liệt kê đệ quy tất cả các thư mục
con. Bạn hãy thực hiện lệnh này từ dấu nhắc hệ thống như sau:
$ls –R /
Dấu nhắc hệ thống trở lại khi chương trình đã thực hiện xong.

c. Tiến trình hậu cảnh


Nếu có cách nào đó yêu cầu Linux đưa các tiến trình chiếm nhiều thời gian xử lý hoặc ít tương tác với người dùng ra hoạt động
phía hậu cảnh (background) trả lại ngay dấu nhắc để các tiến trình ở tiền cảnh có thể thực thi thì tốt hơn. Linux cung cấp khả năng
này bằng lệnh & kết hợp với lệnh của chương trình mà bạn gõ từ dấu nhắc hệ thống. Tất cả những lệnh gõ kèm theo chỉ thị & đều
được hệ điều hành đưa vào hoạt động ngầm bên trong. Ví dụ:
$ls –R / &
[1] 23978
$
Khi tiến trình hậu cảnh chấm dứt, hệ thống sẽ tự động đưa ra thông báo như sau:
$
[1] Done ls –R

d. Tạm dừng tiến trình


Sử dụng phím Ctrl – Z để đưa một tiến trình đang chạy ở tiền cảnh vào chạy ở hậu cảnh. Khi một tiến trình nhận được tín
hiệu Ctrl – Z nó sẽ bị hệ thống cho tạm dừng và đưa vào hậu cảnh. Dấu nhắc hệ thống được trả lại cho người dùng. Tuy đưa
vào hậu cảnh nhưng tiến trình đang bị tạm dừng, nó chỉ thực sự chạy lại ở hậu cảnh khi bạn cho phép . Ví dụ:
$ls –R / >allfiles.txt
^Z
[1]+ Stopped ls –R / >allfiles.txt
$
Muốn xem PID của tiến trình bạn gọi lệnh ps –af

e. Đánh thức tiến trình


Sử dụng lệnh jobs để kiểm tra chương trình của ta đang dừng hay đang chạy.
$jobs
[1]+ Stopped ls –R / >allfiles.txt
Lệnh jobs hiển thị trạng thái của tất cả các tiến trình đang chạy ở hậu cảnh. Như kết quả trên: tác vụ [1] đang ở trạng thái
dừng. Để yêu cầu tiến trình của ta tiếp tục hoạt động ở hậu cảnh: sử dụng lệnh bg.
$bg 1
Ls –R / >allfiles.txt
$jobs
[1]+ Running ls –R />allfiles.txt &
Dùng lệnh fg để mang tiến trình trở lại hoạt động ở phía tiền cảnh.
$fg 1
Ls –R / >allfiles.txt

f. Hủy tiến trình


Không phải lúc nào tiến trình cũng hoạt động tốt đẹp. Có thể chúng sẽ bị treo hoặc bước vào vòng lặp vô tận và không bao giờ
chấm dứt. Trong trường hợp này, ta cần phải loại bỏ chương trình ra khỏi hệ thống. Lệnh kill của Linux thường được dùng cho
mục đích này, kill yêu cầu cung cấp mã số định danh PID của tiến trình. Lệnh kill thường dùng chung với lệnh ps –af.
Bạn dùng lệnh ps –af để xem thông tin về tiến trình đang chạy, sau đó lấy PID của tiến trình cần hủy và gọi lệnh kill.
$ls –R / >data.txt
^Z
$ps -af
PID TTY TIME CMD
128 tty1 00:00:00 bash
137 pts/9 00:00:00 ls -R
235 pts/0 00:00:00 bash

$kill 137

$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”

2. Liên kết tắt


- Viết chương trình program2.c in ra màn hình các số nguyên từ 0 đến 9 đặt trong thư mục /baitap2/
- Chuyển ra thự mục gốc “/”
- Tạo liên kết cứng với tên mới là hardlink đến tập tin thực thi
- Tạo liên kết mềm với tên mới là softlink đến tập tin thực thi
- Chạy chương trình với 2 liên kết vừa tạo ở trên
- Xóa tập tin thực thi trong thư mục /baitap2/
- Chạy lại chương trình với 2 liên kết ở trên

3. Điều khiển tiến trình


- Viết chương trình program3.c in ra các số từ nguyên 0 đến 20, mỗi giây in ra một số, ghi kết xuất ra tập tin ketqua.txt
- Cho chương trình chạy ở hậu cảnh.
- Khi chương trình hoàn tất, xem nội dung tập tin ketqua.txt
BÀI 6a
LẬP TRÌNH ĐA TUYẾN (MULTI –THREAD)
I. Lý Thuyết
1. Tuyến là gì ? Tại sao phải dùng tuyến (thread)
Tuyến là một phần của tiến trình sở hữu riêng ngăn xếp (stack) và thực thi độc lập ngay trong mã lệnh
của tiến trình. Nếu như một HĐH có nhiều tiến trình thì bên trong mỗi tiến trình lại có thể tạo ra nhiều tuyến
hoạt động song song với nhau tương tự như cách tiến trình hoạt động song song bên trong HĐH.
Ưu điểm của tuyến là chúng hoạt động trong cùng không gian địa chỉ của tiến trình. Cơ chế liên lạc giữa
các tuyến đơn giản và hiệu quả.
Đối với HĐH, chi phí chuyển đổi ngữ cảnh của tiến trình cao và chậm hơn chí phí chuyển đổi ngữ cảnh
dành cho tuyến.

2. Tạo lập và hủy tuyến


Khi chương trình chính bắt đầu, nó chính là một tuyến. Tuyến điều khiển hàm main() được gọi là tuyến
chính. Các tuyến khác do tiến trình tạo ra sau đó được gọi là tuyến phụ. Mỗi tuyến được cung cấp cho một
số định danh gọi là thread ID. Để tạo ra một tuyến mới ngoài tuyến chính, bạn gọi hàm
pthread_create(). Hàm này được khai báo như sau:
#include <pthread.h>

int pthread_create ( pthread_t * thread,


pthread_attr_t* attr,
void* (*start_routine) (void*),
void* arg);
Hàm pthread_create () nhận 4 tham số, tham số thứ nhất có kiểu cấu trúc pthread_t để lưu các
thông tin về tuyến sau khi tạo ra. Tham số thứ hai dùng để đặt thuộc tính cho tuyến (trong trường hợp ta đặt
giá trị NULL thì tuyến được tạo ra với các thuộc tính mặc định). Tham số thứ ba là địa chỉ của hàm mà tuyến
sẽ dùng để thực thi. Tham số thứ tư là địa chỉ đến vùng dữ liệu sẽ truyền cho hàm thực thi tuyến.

3. Chờ tuyến kết thúc


a. Chờ tuyến hoàn thành xong tác vụ
Tương tự như tiến trình dùng hàm wait() để đợi tiến trình con kết thúc, bạn có thể gọi hàm
pthread_join() để đợi một tuyến kết thúc.
#include <pthread.h>

int pthread_join (pthread_t th, void* thread_return);


th là tuyến mà bạn muốn chờ, thread_return là con trỏ đến vùng chưa giá trị trở về của tuyến.

b. Chờ đồng thời nhiều tuyến


Thường trong các ứng dụng dịch vụ hoạt động theo mô hình khách chủ (client/server), trình chủ (server)
của bạn phải mở nhiều tuyến để phục vụ trình khách. Hay trong các ứng dụng chò trơi bạn phải mở cùng lúc
nhiều tuyến, mỗi tuyến thực hiện thao tác điều khiển một nhân vật hoạt hình nào đó. Kiểm soát và chờ đồng
thời nhiều tuyến, bạn cũng dùng hàm pthread_join ().
4. Đồng bộ hóa tuyến với đối tượng mutex
Một trong những vấn đề quan tâm hàng đầu của việc điều khiển lập trình đa tuyến trong cùng không gian
địa chỉ của tiến trình đó là đồng bộ hóa. Bạn phải đảm bảo được nguyên tắc ‘các tuyến không dẫm chân lên
nhau’. Ví dụ một tuyến chuẩn bị để đọc dữ liệu từ đĩa, thao tác đọc chưa hoàn tất thì một tuyến khác đã ghi
đè dữ liệu mới lên dữ liệu cũ. Hay đơn giản và thường gặp hơn đó là xảy ra đụng độ khi truy cập và xử lý
biến chung.
Để giải quyết tranh chấp và xử lý đồng bộ hóa chúng ta sử dụng một khái niệm gọi là mutex.

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.

b. Tạo và khởi động mutex


Để tạo ra đối tượng mutex, trước hết bạn cần khai báo biến kiểu cấu trúc pthread_mutex_t, đồng
thời khởi tạo giá trị ban đầu cho biến này. Các đơn giản nhất để khởi tạo cấu trúc mutex là dùng hằng định
nghĩa trước PTHREAD_MUTEX_INITIALIZER. Mã khai báo mutex thường có dạng sau:
pthread_t a_mutex = PTHREAD_MUTEX_INITIALIZER;
Một điều quan trọng bạn cần lưu ý là mutex khởi tạo theo cách này gọi là “mutex cấp tốc”. Đối tượng
mutex này không thể bị khóa hai lần bởi cùng một tuyến. Trong tuyến, nếu bạn đã gọi hàm khóa mutex này
và thực hiện khóa mutex lần nữa, bạn sẽ rơi vào trạng thái khóa chết (deadlock).
Có một kiểu mutex khắc phục được nhược điểm trên, đó là mutex cho phép khóa lặp (recursive mutex).
Trong cùng một tuyến, nếu bạn khóa mutex nhiều lần thì không có vấn đề gì xảy ra, nhưng bù lại muốn giải
phóng mutex, bạn phải tháo khóa bằng đúng số lần bạn đã thực hiện gọi hàm khóa mutex. Mutex kiểu này
thường được khởi động bằng hằng PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP.
Bạn cũng có thể gọi hàm pthread_mutex_init () để thực hiện cùng chức năng khởi tạo mutex:
#include <pthread.h>
int pthread_mutex_init ( pthread_mutex_t* mutex,
const pthread_mutexattr_t* mutexattr);
mutex là con trỏ đến biến cấu trúc pthread_mutex_t mà bạn muốn khởi tạo. mutexattr là các
thuộc tính của mutex (mutex đơn hay mutex cho phép khóa lặp). Nếu bạn đặt trị NULL thì mutex với
thuộc tính mặc định sẽ được tạo ra. Cách thứ hai để khởi tạo mutex sẽ là:
int res;
pthread_mutex_t* mutex;
res = thread_mutex_init (mutex, NULL);
if (res != 0)
{
perror (“Initialize mutex fail”);
}

c. Khóa và tháo khóa cho mutex


Để khóa mutex bạn có thể sử dụng hàm pthread_mutex_lock (), nếu không khóa được (mutex
đã bị tuyến khác khóa trước đó) hàm sẽ đặt tuyến hiện hành vào trạng thái ngủ (chờ). Trong trường hợp này
khi mutex được tháo khóa, tuyến hiện hành sẽ được đánh thức dậy để tiếp tục thử khóa mutex trước khi đi
vào sử dụng tài nguyên. Dưới đây là cách khóa mutex:
pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;
int rc = pthread_mutex_lock (&a_mutex);
if (rc)/*Lỗi phát sinh*/
{
perror (“pthread_mutex_lock_error”);
pthread_exit (NULL);
}
/*Mutex đã được khóa, tuyến của bạn có thể sử dụng tài nguyên một cách
an toàn ở đây*/
...

Một khi không cần sử dụng độc quyền tài nguyên nữa, bạn nên gọi hàm pthread_mutex_unlock
() để tháo khóa mutex trả lại quyền sử dụng tài nguyên cho tuyến khác. Bạn tháo khóa mutex như sau:

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.

II. Thực hành


Bài 1: Chương trình tạo lập tuyến: chúng ta tạo hàm do_loop () để in ra các số nguyên. Hàm do_loop
() này được gọi thực thi ở hai nơi: một trong tuyến chính (hàm main) và một trong tuyến phụ tạo ra bởi
hàm pthread_create ().
thread_create.c
#include <stdio.h>
#include <pthread.h> /*Khai báo các hàm xử lý tuyến*/

/*Hàm thực thi tuyến*/


void* do_loop (void* data)
{
int i; /*Bộ đếm cho tuyến*/
/*Dữ liệu cho hàm pthread_create() truyền vào cho tuyến*/
int me = (int*) data;

for (i = 0; i < 5; i++)


{
sleep (1); /*Dừng*/
printf (" '%d' - Got '%d' \n", me, i);
}
/*Chấm dứt tuyến*/
pthread_exit (NULL);
}

/*Chương trình chính*/


int main (int agrc, char* argv[])
{
int thr_id; /*Định danh tuyến*/
pthread_t p_thread; /*Cấu trúc lưu trữ các thông tin về tuyến*/
int a = 1; /*Định danh cho tuyến thứ nhất*/
int b = 2; /*Định danh cho tuyến thứ hai*/

/*Tạo tuyến*/
thr_id = pthread_create (&p_thread, NULL, do_loop, (void*) a);

/*Chạy do_loop trong tuyến chính*/


do_loop ((void*)b);
return 0;
}
Để biên dịch chương trình này, bạn cần phải dùng đến thư viện liên kết hỗ trợ lập trình tuyến là
libpthread. Chúng ta biên dịch chương trình như sau:
$gcc thread_create.c -o thread_create -lpthread
Chạy chương trình với kết quả kết xuất
./thread_create
'2' - Got '0'
'1' - Got '0'
'2' - Got '1'
'1' - Got '1'
'2' - Got '2'
'1' - Got '3'
'2' - Got '3'
'1' - Got '3'
'2' - Got '4'
'1' - Got '4'

Bài 2: Chờ tuyến thực thi xong tác vụ


thread_wait.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

char message[] = "Hello World";

/*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");
}

/*Chương trình chính*/


int main ()
{
int res;
pthread_t a_thread;
void* thread_result;

/*Tạo và thực thi tuyến*/


res = pthread_create (&a_thread, NULL, do_thread, (void*) message);

if (res != 0)
{
perror ("Thread created error\n");
exit (EXIT_FAILURE);
}

/*Đợi tuyến kết thúc*/


printf ("Waiting for thread to finish ...\n");
res = pthread_join (a_thread, &thread_result);

if (res != 0)
{
perror ("Thread wait error\n");
exit(EXIT_FAILURE);
}

/*In kết quả trả về của tuyến*/


printf ("Thread completed, it returned %s \n", (char*) thread_result);
printf ("Message is now %s \n", message);
return 0;
}
Biên dịch và chạy chương trình từ dòng lệnh. Kết quả kết xuất sẽ như sau:
$gcc thread_wait.c -o thread_wait -lpthread
$./thread_wait
Thread function is executing ...
Thread data is Hello World
Waiting for thread to finish ...
Thread completed, it returned Thank you for using my thread
Message is now Bye !

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

void* do_thread (void* data);

int main ()
{
int res;
int thread_num;
pthread_t a_thread [MAX_THREADS];

void* thread_result;

/*Khởi tạo danh sách các tuyến*/


for (thread_num =1; thread_num < MAX_THREADS; thread_num++)
{
/*Tạo tuyến và lưu vào phần tử mảng*/
res = pthread_create (&(a_thread [thread_num]), NULL, do_thread,
(void*) thread_num);

if (res != 0)
{
perror ("Thread created error");
exit (EXIT_FAILURE);
}
/*Dừng 1 giây*/
sleep (1);
}
printf ("Waiting for threads to finish ...\n");

/*Chờ danh sách các tuyến kết thúc*/


for(thread_num = MAX_THREADS - 1; thread_num > 0; thread_num--)
{
res = pthread_join (a_thread [thread_num], &thread_result);
if (res != 0)
{
perror ("Thread exited error");
}
else
{
printf ("Pickup a thread\n");
}
}
printf ("All thread completed \n");
return 0;
}

/*Cài đặt hàm điều khiển tuyến*/


void* do_thread (void* data)
{
int my_number = (int) data;
printf ("Thread function is running. Data argument was %d\n", my_number);
sleep (3);
printf ("Finish - bye from %d\n", my_number);
}

Biên dịch chương trình:


$gcc thread_multiwait.c -o thread_multiwait -lpthread
Chạy chương trình:
$./thread_multiwait
Thread function is running. Data argument was 1
Thread function is running. Data argument was 2
Thread function is running. Data argument was 3
Thread function is running. Data argument was 4
Finish - bye from 1
Finish - bye from 2
Thread function is running. Data argument was 5
Finish - bye from 3
Waiting for threads to finish ...
Finish - bye from 4
Finish - bye from 5
Pickup a thread
Pickup a thread
Pickup a thread
Pickup a thread
Pickup a thread
All thread completed

Bài 4: Sử dụng mutex


Chương trình tạo ra hai tuyến. Tuyến thứ nhất liên tục tăng biến toàn cục global_var lên một đơn vị và
dừng chờ trong 1 giây. Tuyến thứ hai ngược lại liên tục giảm biến toàn cục global_var đi một đơn vị và
dừng chờ trong 2 giây.
thread_race.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

/*Biến dữ liệu toàn cục có thể truy xuất bởi cả hai tuyến*/
int global_var;

pthread_mutex_t a_mutex; /*Khai báo biến mutex toàn cục*/

/*Khai báo các hàm dùng thực thi tuyến*/


void* do_thread1 (void* data);
void* do_thread2 (void* data);

/*Chương trình chính*/


int main ()
{
int res;
int i;
pthread_t p_thread1;
pthread_t p_thread2;
/*Khởi tạo mutex*/
res = pthread_mutex_init (&a_mutex, NULL);
/*Bạn cũng có thể khởi tạo mutex như sau
a_mutex = PTHREAD_MUTEX_INITIALIZER;
*/

if (res != 0)
{
perror ("Mutex create error");
exit (EXIT_FAILURE);
}

/*Tạo tuyến thứ nhất*/


res = pthread_create (&p_thread1, NULL, do_thread1, NULL);
if (res != 0)
{
perrror ("Thread create error");
exit (EXIT_FAILURE);
}

/*Tạo tuyến thứ hai*/


res = pthread_create (&p_thread2, NULL, do_thread2, NULL);
if (res != 0)
{
perror ("Thread create error");
exit (EXIT_FAILURE);
}

/*Tuyến chính của chương trình*/


for (i = 1; i < 20; i++)
{
printf ("Main thread waiting %d second ... \n", i);
sleep (1);
}
return 0;
}
/*Cài đặt hàm thực thi tuyến thứ nhất*/
void* do_thread1 (void* data)
{
int i;
pthread_mutex_lock (&a_mutex); /*Khóa mutex*/

for (i=1; i <= 5; i++)


{
printf ("Thread 1 count: %d with global value %d \n", i, global_var++);
sleep(1);
}
pthread_mutex_unlock (&a_mutex); /*Tháo khóa mutex*/

printf ("Thread 1 completed !");


}

void* do_thread2 (void* data)


{
int i;
pthread_mutex_lock (&a_mutex); /*Khóa mutex*/

for (i=1; i <= 5; i++)


{
printf ("Thread 2 count: %d with global value %d \n", i, global_var--);
sleep(2);
}
pthread_mutex_unlock (&a_mutex); /*Tháo khóa mutex*/

printf ("Thread 2 completed !");


}
Biên dịch và thực thi chương trình bạn sẽ nhận được kết xuất từ dòng lệnh như sau:
$./thread_race
Thread 1 count: 1 with global value 0
Main thread waiting 1 second ...
Main thread waiting 2 second ...
Thread 1 count: 2 with global value 1
Main thread waiting 3 second ...
Thread 1 count: 3 with global value 2
Main thread waiting 4 second ...
Thread 1 count: 4 with global value 3
Main thread waiting 5 second ...
Thread 1 count: 5 with global value 4
Thread 1 completed!
Main thread waiting 6 second ...
Thread 2 count: 1 with global value 5
Main thread waiting 7 second ...
Thread 2 count: 2 with global value 4
Main thread waiting 8 second ...
Main thread waiting 9 second ...
Thread 2 count: 3 with global value 3
Main thread waiting 10 second ...
Main thread waiting 11 second ...
Thread 2 count: 4 with global value 2
Main thread waiting 12 second ...
Main thread waiting 13 second ...
Thread 2 count: 5 with global value 1
Thread 2 completed!
Main thread waiting 14 second ...
Bài 6b
LẬP TRÌNH ĐA TUYẾN (tt)
5. Đồng bộ tuyến với semaphore
a. Semaphore là gì ?
Semaphore thực sự là một cờ hiệu tương tự như mutex, nếu một tuyến cần sử dụng tài nguyên nó thông báo với
semaphore.
Muốn sử dụng đối tượng semaphore, bạn cần gọi hàm sem_init () để khởi tạo biến semaphore có kiểu cấu trúc
sem_t như sau:
#include <semaphore>
int sem_init (sem_t* sem, int pshareed, unsined int value)
Để yêu cầu sử dụng tài nguyên, tuyến thực hiện gọi hàm sem_wait (). Sau khi sử dụng xong tài nguyên tuyến
cần gọi hàm sem_post để trả về giá trị của semaphore.
#include <semaphore.h>
int sem_wait (sem_t sem);
int sem_post (sem_t sem);
Cả hai hàm này đều yêu cầu đối số là đối tượng sem đã được hàm sem_init () tạo ra trước đó.
b. Ứng dụng của semaphore
Bài toán nổi tiếng về tranh chấp tài nguyên dễ hiểu nhất đó là bài toán “sản xuất – tiêu thụ”. Bạn hình dung có hai
tuyến song song. Một tuyến chịu trách nhiệm sản xuất ra sản phẩm (producer). Một tuyến có nhiệm vụ lấy sản
phẩm để tiêu thụ (comsumer). Nếu sản xuất và tiêu thụ cùng nhịp với nhau thì không có vấn đề gì xảy ra. Tuy nhiên
cung, cầu ít bao giờ cần đối và gặp nhau tại một điểm.
Ví dụ dưới dây giúp giải quyết vấn đề này.
pro_consumer.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

int product_val = 2; /*Sản phẩm ban đầu trong kho*/


sem_t semaphore; /*Khai báo đối tượng semaphore*/

/*Hàm thực thi tuyến*/


void * do_thread (void* data);

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);
}

/*Tuyến chính đóng vai trò người sản xuất*/


for (i = 0; i < 5; i++)
{
product_val++;
printf ("Producer product_val = %d \n\n", product_val);
/*Tăng giá trị semaphore - thông báo sản phầm đã được đưa thêm vào kho*/
sem_post (&semaphore);
sleep (2);
}

printf ("All done\n");


exit (EXIT_SUCCESS);
}

/*Cài đặt hàm thực thi tuyến*/


void* do_thread (void* data)
{
printf ("Consumer thread function is running ...\n");
while (1)
{
/*Yêu cầu semaphore cho biết có được phép lấy sản phẩm khỏi kho hay không*/
sem_wait (&semaphore);
product_val--;
printf ("Consumer product_val = %d \n", product_val);
sleep (1);
}
pthread_exit(NULL);
}
Biên dịch chương trình sẽ thu được kết quả như sau:
$./prod_consumer
Producer product_val = 3

Cosumer thread function is running ...


Consumer product_val = 2
Consumer product_val = 1
Producer product_val = 2

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

6. Hủy bỏ và chấm dứt tuyến


Đôi khi chúng ta muốn một tuyến có thể yêu cầu tuyến khác chấm dứt khi đang thực thi. Bạn dùng hàm
pthread_cancel () để gởi tín hiệu đến tuyến cần hủy.
#include <phread.h>
int pthread_cancel (pthread_t thread);
- Thiết lập trạng thái chấm dứt của tuyến bằng hàm pthread_setcancelstate ()
#include <pthread.h>
int pthread_setcancelstate (int state, int *oldstate);
- Thiết lập kiểu chấm dứt bằng hàm pthread_setcanceltype ()
#include <pthread.h>
int pthread_setcanceltype (int state, int *oldstate);

thread_cancel.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

/*Hàm thực thi tuyến*/


void * do_thread (void* data);

int main ()
{
int res, i;
pthread_t a_thread;
void* thread_result;

/*Tạo tuyến với giá trị thiết lập mặc định*/


res = pthread_create (&a_thread, NULL, do_thread, NULL);
if (res != 0)
{
perrror ("Thread create error");
exit (EXIT_FAILURE);
}
sleep (3);

/*Gởi tín hiệu yêu cầu chấm dứt tuyến a_thread*/


printf ("Try to cancel thread ...\n");
res = pthread_cancel (a_thread);

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);
}

printf ("All done \n");


exit (EXIT_SUCCESS);
}

/*Cài đặt hàm thực thi tuyến*/


void* do_thread (void* data)
{
int i, res;
res = pthread_setcancelstate (PTHREAD_CANCEL_ENABLE, NULL);
if (res != 0)
{
perror ("Thread set cancel state fail");
exit (EXIT_FAILURE);
}

res = pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, NULL);


if (res != 0)
{
perror ("Thread set cancel type fail");
exit (EXIT_FAILURE);
}

printf ("Thread function is running ... \n");


for (i = 0; i < 10; i++)
{
printf ("Thread is still running (%d) ...\n", i);
sleep (1);
}
pthread_exit (NULL);
}
Biên dịch chương trình từ dòng lệnh
$gcc thread_cancel.c - o thread_cancel -lpthread

Chạy chương trình với kết xuất như sau


$./thread_cancel
Thread function is running ...
Thread is still running (0) ...
Thread is still running (1) ...
Thread is still running (2) ...
Try to cancel thread ...
Waiting for thread to finish ...
Thread is still running (3) ...
All done
Bài 7
TƯƠNG TÁC VỚI MÔI TRƯỜNG LINUX
I. Đối số truyền cho chương trình
- Đối số của hàm main() là nơi nhận đối số dòng lệnh truyền cho chương trình:
int main( int argc, char* argv[] )
argc: số đối số truyền cho chương trình.
argv: con trỏ chỉ đến mảng chứa giá trị các đối số.
- Hệ shell của Hệ Điều Hành sẽ tiếp nhận các đối số dòng lệnh và và gửi đến Hệ Điều Hành cùng tên của chương trình. Hệ Điều Hành đưa các giá
trị đối số vào mảng argv, số tham số được đếm và truyền vào argc. Các xử lý sơ bộ đối số như xử lý ký tự đại diện, xử lý khoảng trắng do hệ
shell thực hiện. Ví dụ: đặt biến môi trường LAST_PARAM trị 'Hello World', sau đó dùng biến môi trường này như một tham số:
$ export LAST_PARAM='Hello World'
$ echo $LAST_PARAM
Hello World
$ myprog hello byebye $LAST_PARAM
- Chương trình args.c dưới đây sẽ in tất cả giá trị của đối số mà hàm main nhận được:
Bài 1: args.c
#include <stdio.h>
int main( int argc, char* argv[] )
{
int arg;
for ( arg = 0; arg < argc; arg++ ) {
if ( argv[arg][0] == '-' )
printf( "option: %s\n", argv[arg] + 1 );
else
printf( "argument %d: %s\n", arg, argv[arg] );
}
return 0;
}
$ ./args –i –lr 'Hello World' –f file.c
argument 0: args
option: i
option: lr
argument 3: HelloWorld
option: f
argument 5: file.c

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ý.

- Chương trình ví dụ argopt.c cho thấy cách sử dụng hàm getopt():


Bài 2: argopt.c
#include <stdio.h>
#include <unistd.h>
int main( int argc, char* argv[] )
{
int opt;
while ( ( opt = getopt( argc, argv, "if:lr" )) != -1 )
{
switch ( opt )
{
case 'i' :
case 'l' :
case 'r' :
printf( "option: %c\n", opt );
break;
case 'f' :
printf( "filename: %s\n", optarg );
break;

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

II. Biến môi trường (Environment Variables)


1. Đọc và thiết lập biến môi trường
- Linux định nghĩa rất nhiều biến môi trường dùng cho các mục đích khác nhau. Biến môi trường dùng kiểm soát và cung cấp thông tin để các
chương trình và script của shell hoạt động, dùng cấu hình môi trường làm việc của người dùng.
- Nội dung một biến môi trường được xem bằng lệnh echo. Ví dụ: echo $HOME
- Danh sách các biến môi trường trong phiên làm việc hiện hành cùng trị của chúng, được xem bằng lệnh set.
- Các chương trình có thể truy xuất và thiết lập biến môi trường dựa vào hàm getenv() và putenv().
#include <stdlib.h>
char* getenv( const char* name );
int putenv( const char* string );
Các biến môi trường thường ở dạng chuỗi: varname = value
- Hàm getenv() sẽ dò tìm trong môi trường hiện hành chuỗi varname, nếu tìm thấy nó sẽ trả về giá trị chuỗi value tương ứng với tên varname.
Nếu tên biến varname không tồn tại hoặc giá trị không có, hàm sẽ trả về null.
- Hàm putenv() tiếp nhận một chuỗi theo mẫu ‘varname=value’ và đặt chuỗi này vào danh sách biến môi trường hiện hành. Nếu vùng nhớ
chứa các biến môi trường của hệ thống đã hết, hàm sẽ trả về trị -1.
- Ví dụ:
Bài 3: get_setenv.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main( int argc, char* argv[] )
{
char* var, * value;
// Kiểm tra đối số
if ( argc == 1 || argc > 3 ) {
fprintf( stderr, "Usage: get_setenv var [value]\n" );
exit( 1 );
}
// Lấy tên biến môi trường rồi tìm trị
var = argv[1];
value = getenv( var );
if ( value )
pritnf( "Variable %s has value %s\n", var, value );
else
pritnf( "Variable %s has no value\n", var );
// Nếu có đối số thứ ba thì thiết lập trị cho biến môi trường với trị là đối số thứ ba
if ( argc == 3 ) {
char* string;
value = argv[2];
string = malloc( strlen( var ) + strlen( value ) + 2 );
if ( !string ) {
fprintf( stderr, "Out of memory\n" );
exit( 1 );
}
strcpy( string, var );
strcat( string, "=" );
strcat( string, value );
printf( "Calling putenv() with: %s\n", string );
if ( putenv( string ) != 0 ) {
fprintf( stderr, "putenv() failed\n" );
free( string );
exit( 1 );
}
// Lấy giá trị mới của biến môi trường
value = getenv( var );

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

2. Sử dụng biến môi trường


- Biến môi trường do ta tạo trong chương trình trên chỉ có giá trị cục bộ đối với chương trình. Muốn biến môi trường trở nên toàn cục trở nên toàn
cục và thấy được bởi chương trình, phải dùng lệnh export, ví dụ:
$ MYVAR=Linux
$ echo $MYVAR
Linux
$ get_setenv MYVAR
Variable MYVAR has no value
$ export MYVAR=Linux
$ get_setenv MYVAR
Variable MYVAR has value Linux

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_t getuid( void );


char* getlogin();
getuid() trả về UID của người dùng đang chạy chương trình, là một số nguyên.
getlogin() trả về chuỗi thông tin về người dùng tương ứng với số UID lấy được.
- Thông tin người dùng chứa trong file /etc/passwd và ánh xạ trong file /etc/shadow (xem bởi root). Có thể lấy bằng cách dùng các hàm sau:
#include <sys/types.h>
#include <pwd.h>
struct passwd* getpwuid( uid_t uid );
struct passwd* getpwname( const char* name );
Cấu trúc passwd trả về cung cấp thông tin người dùng:
struct passwd {
char* pw_name; /* Tên người dùng */
uid_t pw_uid; /* Định danh người dùng UID */
gid_t pw_gid; /* Định danh nhóm GUID */
char* pw_dir; /* Đường dẫn thư mục chủ */
char* pw_shell; /* Shell thực thi khi người dùng đăng nhập */
};
- Ví dụ user_info.c hiển thị thông tin về người dùng đang sử dụng chương trình:
Bài 5: user_info.c
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <pwd.h>
int main()
{
uid_t uid;
gid_t gid;
struct passwd* pw;

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:

- Để lấy về danh sách tất cả người dùng, sử dụng hàm getpwent().

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.

2. Kết nối socket


a) Server
- Trước hết ứng dụng chủ mở một socket . Đây chỉ là quá trình Hệ Điều Hành phân bổ tài nguyên để chuẩn bị kết nối. Bạn gọi là socket() để tạo
“ổ cắm” socket cho trình chủ server.
- Để ứng dụng khách biết đến ổ cắm socket của trình chủ, bạn phải đặt cho socket trình chủ một tên. Nếu trên máy cục bộ và dựa vào hệ thống tập
tin của Linux, bạn có thể đặt tên cho socket như là một tên tập tin (với đầy đủ đường dẫn). Bạn chỉ cần đặt tên còn đường dẫn thường đặt trong thư
mục /tmp hay /usr/tmp. Đối với giao tiếp mạng thông qua giao thức TCP/IP tên của socket được thay thế bằng khái niệm cổng (port). Cổng là
một số nguyên 2 bytes thay thế cho tên tập tin. Nếu trình khách và trình chủ nằm trên hai máy khác nhau, giao thức TCP/IP còn yêu cầu xác định
thêm địa chỉ IP để kết nối đến máy chủ ở xa.
- Sau khi đã chỉ định tên hoặc số hiệu port cho socket, bạn gọi là bind() để ràng buộc hay đặt tên chính thức cho socket của trình chủ. Tiếp đến,
trình chủ sẽ gọi hàm listen() để tạo hàm lắng nghe các kết nối từ trình khách đưa đến. Nếu có yêu cầu kết nối từ trình khách, trình chủ gọi hàm
accept() để tiếp nhận yêu cầu của trình khách.
Hàm accept() sẽ tạo một socket vô danh khác (unnamed socket), cắm kết nối của trình khách vào socket vô danh này và thực hiện quá trình
chuyển dữ liệu trao đổi giữa khách chủ. Socket được đặt tên trước đó vẫn tiếp tục hoạt động để chờ nhận yêu cầu từ trình khách khác.
- Mọi giao tiếp đọc ghi thông qua socket cũng đơn giản như việc bạn dùng lệnh read/write để đọc ghi trên tập tin. Nếu tập tin dựa vào số mô tả
(file descriptor) để đọc ghi trên một tập tin xác định thì socket cũng dựa vào số mô tả (socket descriptor) để xác định socket cần đọc ghi cho hàm
read/write.
b) Client
- Phía trình khách bạn chỉ cần tạo một socket vô danh, chỉ định tên và vị trí socket của trình chủ. Yêu cầu kết nối bằng hàm connect() và đọc
ghi, truy xuất dữ liệu của socket bằng lệnh read/write.

- 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.

II. Cách socket làm việc


1. Thuộc tính của socket
- Socket được định nghĩa dựa trên ba thuộc tính: vùng (domain), kiểu (type) và giao thức (protocol). Socket còn căn cứ vào một địa chỉ kết hợp với
nó, địa chỉ này phụ thuộc vào domain của socket và thường gọi là họ giao thức (protocol family). Ví dụ họ giao thức theo hệ thống tập tin của Sun
lấy tên tập tin làm đường dẫn đến địa chỉ, trong khi giao thức TCP/IP lại lấy địa chỉ là số IP 32bits để tham chiếu và thực hiện kết nối vật lý.

a) Vùng giao tiếp của socket (domain)


- Vùng dùng xác định hạ tầng mạng nơi giao tiếp của socket diễn ra. Vùng giao tiếp socket thông dụng nhất hiện nay là AF_INET hay giao tiếp
socket theo chuẩn mạng Internet. Chuẩn này sử dụng địa chỉ IP để xác định nút kết nối vật lý trên mạng. Ngoài ra nếu bạn chỉ cần giao tiếp cục
bộ, bạn có thể dùng giao tiếp của Sun AF_UNIX, đó là dùng đường dẫn và hệ thống tập tin để đặt tên và xác định kết nối giữa hai hay nhiều ứng
dụng. Chẳng hạn như ví dụ trên, chúng ta đã thực hiện bằng cách đặt tên cho socket là server_socket, và Linux đã tạo ra tập tin server_socket
ngay trên thư mục hiện hành (hay trong thư mục tạm/tmp tùy theo phiên bản của Linux). Tuy nhiên vùng giao tiếp AF_UNIX lại ít được sử dụng
trong thực tế. Ngày nay hầu hết các ứng dụng mạng sử dụng socket đều theo vùng AF_INET là chủ yếu.
- Vùng AF_INET sử dụng địa chỉ IP (Internet Protocol) là một số 32bits để xác định kết nối vật lý. Số này thường được viết ở dạng nhóm như
192.168.1.1, 203.162.42.1 hay 127.0.0.1 là các địa chỉ IP hợp lệ. Địa chỉ IP có thể được ánh xạ thành một tên gợi nhớ hơn như
www.yahoo.com hay www.microsoft.com , chúng được gọi là tên vùng (domain name). Việc ánh xạ địa chỉ IP thành tên vùng thường do máy
chủ DNS (Docmain Name Server) thực hiện. Cũng có thể tự ánh xạ tên vùng ngay trên máy cục bộ bằng cách sử dụng tập tin /etc/host.
- Socket theo dòng giao thức IP sử dụng port (số hiệu cổng) để đặt tên cho một socket. Cổng dùng để phân biệt dữ liệu gửi đến sẽ chuyển cho ứng
dụng nào. Bạn hình dung nếu địa chỉ IP dùng để xác định được máy hay nơi kết nối vật lý để đưa dữ liệu đến thì cổng là địa chỉ phụ dùng để gửi

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;

b) Kiểu socket (type)


- Với mỗi vùng domain của socket, bạn có thể có nhiều cách giao tiếp. Có hai kiểu giao tiếp điển hình là giao tiếp một chiều (SOCK_DGRAM) và
giao tiếp bảo đảm hai chiều ( SOCK_STREAM). Dữ liệu khi chuyển đi được đóng thành từng gói (data package hay datagram). SOCK_STREAM hay
SOCK_DGRAM qui định cách chuyển gói dữ liệu theo 2 cách sau:
+ SOCK_STREAM luôn giữ kết nối và bảo đảm gói thông tin gửi đi được chuyển đến đích và nhận đầy đủ. Xong khi nhận xong dữ liệu, nơi nhận sẽ
gửi thông tin phản hồi cho nơi gửi và quá trình gửi sẽ được xác nhận hoàn tất khi nhận được đầu đủ thông tin gửi nhận ở cả hai phía khách chủ.
Kiểu truyền dữ liệu socket này trong vùng giao thức mạng Internet AF_INET được gọi là giao thức truyền TCP. Khi kết hợp với cơ chế định tuyến
theo địa chỉ IP chúng được gọi tắt là TCP/IP. Kết nối và truyền dữ liệu theo cách này tuy bảo đảm nhưng tốn nhiều tài nguyên của hệ thống do
kết nối phải duy trì và theo dõi thường xuyên.
+ SOCK_DGRAM cách gửi nhận dữ liệu này chỉ diễn ra một chiều. Trình khách, nơi gửi dữ liệu đi không cần biết trình chủ (nơi nhận) có nhận được
gói dữ liệu đầy đủ hay không. Trình chủ khi nhận được dữ liệu cũng không cần thông báo hay xác nhận với trình khách là dữ liệu đã nhận xong.
Cách gửi này có vẻ không an toàn nhưng bù lại thực hiện nhanh và không tiêu tốn nhiều tài nguyên để duy trì kết nối và kiểm tra dữ liệu nhận gửi.
Kiểu truyền dữ liệu socket này trong vùng giao thức mạng Internet AF_INET được gọi là giao thức truyền UDP (User Datagram Protocol). Khi kết
hợp với cơ chế định tuyến theo địa chỉ IP chúng được gọi tắt là UDP/IP. Do tính chất không bảo đảm nên UDP được dùng nhiều trong mạng với
mục đích thông báo hay gửi tin đến một nhóm máy tính nào đó trong mạng. Nó ít khi được dùng trong các dịch vụ cần sự chính xác cao như gửi
nhận tập tin hay kết nối cơ sở dữ liệu.

c) Giao thức của socket (protocol)


- Giao thức là cách quy ước gửi nhận dữ liệu giữa hai hay nhiều máy tính trong mạng. Tùy theo kiểu gửi nhận dữ liệu mà ta có các giao thức khác
nhau. Hiện nay kiểu gửi nhận theo giao thức TCP và UDP là sử dụng nhiều nhất. Tuy nhiên điều này không bắt buộc, TCP và UDP chỉ là cách cài đặt
cho vùng socket AF_INET theo giao tiếp Internet mà thôi. Ví dụ, bạn có thể cài đặt một giao thức khác thay cho UDP áp dụng cho cách chuyển dữ
liệu không đảm bảo.Mỗi kiểu socket đều có giao thức mặc định áp dụng cho nó. Trong tất cả các hàm socket sau này, tùy chọn protocol nếu có
yêu cầu bạn có thể đặt trị 0 để yêu cầu sử dụng giao thức mặc định.
Trong ví dụ ở phần sau ta chỉ tập trung nghiên cứu về vùng socket AF_INET và địa chỉ IP thay cho vùng AF_UNIX ở ví dụ đầu tiên. Với AF_INET
mọi ứng dụng mạng của bạn đều có thể giao tiếp được với nhau bất kể chúng đang chạy trên Windows, Linux hay UNIX.

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. Định địa chỉ socket


- Một socket được tạo ra phải cần có địa chỉ và tên để các trình khách có thể tham chiếu đến. Tùy domain mà cách đánh địa chỉ có thể khác nhau:
+ Khi dùng domain AF_UNIX dựa trên hệ thống tập tin (ví dụ trên) , địa chỉ socket được định nghĩa trong structure sockaddr_un của tập tin
sys/un.h như sau:
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path; /* đường dẫn */
+ Khi dùng AF_INET, structure địa chỉ được định nghĩa trong sockaddr_in của tập tin netinet/in.h như sau:
struct sockadd-in {
short int sin_ family; /* AF_INET */
unsigned short int sin_port /* số hiệu cổng */
struct in_addr sin_addr; /* địa chỉ IP */

3
};
Trong đó:
struct in_addr {
unsigned long int s_addr;
};

4. Đặt tên cho socket


- Sau khi định địa chỉ, cần đặt tên cho socket:
+ Với AF_UNIX, địa chỉ là đường dẫn, tên là tên tập tin để tạo socket.
+ Với AF_INET, địa chỉ chính là IP, tê là số hiệu cổng sin_port.
Sau khi gán thông tin đầy đủ và các structure, gọi hàm bind() ràng buộc thông tin này cho socket đã mở ra trước đó.
#include <sys/socket.h>
int bind( int socketd, const struct sockaddr *address, size_t address_len );
socketd: handle của socket, có được do hàm socket() trả về.
address: struct sockaddr, tổng quát cho sockaddr_un và sockaddr_in, chứa thông tin địa chỉ socket.
address_len: chiều dài struct sockaddr.
Nếu thành công, bind() trả về trị 0, nếu không hàm trả về trị -1 và errno sẽ chứa mã lỗi.

5. Tạo hàng đợi cho socket


- Để đón nhận các kết nối chuyển đến, trình chủ phải tạo hàng đợi (queue) bằng hàm listen():
#include <sys/socket.h>
int listen( int socketd, int backlog );
socketd: handle của socket.
backlog: số kết nối tối đa được phép đưa vào hàng đợi, thông thường là 5.
Nếu thành công, listen() trả về trị 0, nếu không hàm trả về trị -1 và errno sẽ chứa mã lỗi.

6. Chờ và chấp nhận kết nối


- Công việc sau cùng là chờ kết nối của trình khách gửi đến bằng hàm accept():
#include <sys/socket.h>
int accept( int socketd, struct sockaddr *address, size_t address_len );
socketd là handle của socket máy chủ đang lắng nghe. Khi accept() phát hiện có socket trong hàng đợi, nó tự động lấy địa chỉ máy khách đặt
vào cấu trúc address, chiều dài thật sự của cấu trúc địa chỉ này đặt trong address_len. Tiếp đó, accept() tạo ra một socket vô danh, kết quả
trả về của accept() là handle của socket vô danh này, ta dùng nó để liên lạc với trình khách. Socket vô danh có cùng kiểu với socket đặt tên đang
lắng nghe trên hàng đợi. Nếu chưa có kết nối nào trong hàng đợi, accept() sẽ dừng lại chờ (block).

7. Yêu cầu kết nối


- Trình chủ thực hiện chức năng tạo  đặt tên  chờ kết nối bằng các hàm: socket(), bind(), accept(). Trình khách đơn giản hơn, chỉ gọi
hàm connect() để yêu cầu kết nối với trình chủ:
#include <sys/socket.h>
int connect( int socketd, struct sockaddr *address, size_t address_len );
Nếu thành công, connect() trả về trị 0, nếu không hàm trả về trị -1 và errno sẽ chứa mã lỗi. Nếu kết nối không thành công connect() sẽ cố
gắng chờ kết nối lại. Sau một số lần kết nối không thành công, connect() trả về mã lỗi ETIMEOUT.

8. Đóng kết nối


- Đóng kết nối với cả hai trình khách và chủ, sẽ giải phóng tài nguyên và bảo đảm dữ liệu chuyển tải hoàn tất. Dùng hàm close().
#include <sys/socket.h>
int close( int socketd );
- Dưới đây là một ví dụ minh họa dùng AF_INET thay cho AF_UNIX:
client2.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 <unistd.h>
/* dành riêng cho AF_INET */
#include <netinet/in.h>
#include <arpa/inet.h>

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

--- 192.168.2.250 ping statistics ---


4 packets transmitted, 4 received, 0% loss, time 3009ms
rtt min/avg/max/mdev = 0.083/0.090/0.103/0.113 ms
- Việc chọn số hiệu cổng cho chương trình phải loại trừ những cổng đã được các ứng dụng khác sử dụng. Tham khảo tập tin /etc/services liệt
kê danh sách các dịch vụ và cổng đã sử dụng. Số cổng không được nhỏ hơn 1024 (dành cho các dịch vụ của hệ thống).

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;

server_sockfd = socket (AF_INET, SOCK_STREAM, 0)


server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl (INADDR_ANY);
server_address.sin_port = htons (9734);
server_len = sizeof (server_address);

bind(server_sockfd, (struct sockaddr*) &server_address, server_len);

/*
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…

$client2 & client2 & client2 & ps –ax


[8] 1572
[9] 1573
[10] 1574
Server waiting…
Server waiting…
Server waiting…
PID TTY STAT TIME COMMAND
1577 ppo S 0:00:00 server3
1572 ppo S 0:00:00 client2
1573 ppo S 0:00:00 client2
1574 ppo S 0:00:00 client2
1575 ppo S 0:00:00 ps -ax
1576 ppo S 0:00:00 server3
1577 ppo S 0:00:00 server3
1578 ppo S 0:00:00 server3
char from server = B
char from server = B
char from server = B

$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);
}

/*Đọc lại dữ liệu từ tiến trình con trả về*/


rc=read(input_pipe[0], &c, 1);
if(rc <= 0)
{
perror("user_handler: read error");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}

/*In dữ liệu do tiến trình con trả về ra màn hình*/


putchar(c);
}

/*Đóng đầu ống đọc và ghi*/


close(input_pipe[0]);
close(output_pipe[1]);
exit(0);
}

/*Cài đặt hàm xử lý dữ liệu cho tiến trình con*/


void translator( int input_pipe[], int output_pipe[])
{
char c; /*Ký tự đọc được từ đường ống*/
int rc; /*Lưu mã lỗi trả về*/

/*Đóng các đầu ống không cần thiết*/


close(input_pipe[1]);
close(output_pipe[0]);

/*Đọc dữ liệu từ đường ống do tiến trình cha chuyển đến*/


while(read(input_pipe[0], &c, 1)>0)
{
/*Chuyển kí tự thường thành hoa*/
if( isascii(c) && islower(c))
c = toupper(c);
/*Ghi trả dữ liệu về tiến trình cha thông qua đường ống thứ hai*/
rc = write(output_pipe[1], &c, 1);
if(rc == -1) /*Lỗi ghi*/
{
perror("Translator: write");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
}

/*Đóng đường ống*/


close(input_pipe[0]);
close(output_pipe[1]);
exit(0);
}

/*Chương trình chính*/


int main()
{
/*Khai báo mảng lưu số mô tả của các đường ống xuất nhập*/
int user_to_translator[2];
int translator_to_user[2];

int pid; /*pid của tiến trình con*/


int rc; /*Lưu mã lỗi trả về*/

/*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);
}

/*Tạo tiến trình con*/


pid = fork();

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;
}

You might also like