CHƯƠNG 7 - LẬP TRÌNH NÂNG CAO TRÊN LINUX

Màu nền
Font chữ
Font size
Chiều cao dòng

CHƯƠNG 7 - LẬP TRÌNH NÂNG CAO TRÊN LINUX
ThS. Trần Hồ Thuỷ Tiên



Mục đích
•  Giúp sinh viên hiểu sâu hơn về tiến trình và cách quản lý tiến trình trên hệ  điều hành Linux.
•  Giúp người lập trình có thể sử dụng các thư viện chuẩn của C để lập trình liên quan đến tiến trình.


I – Tiến trình (Process)
 Khi chúng ta ngồi trước máy tính, có nhiều tiến trình đang chạy. Mỗi một chương trình đang chạy sử dụng một hoặc nhiều tiến trình.
 Mỗi tiến trình trên Linux được nhận biết thông qua pid của nó. Pid là một số 16 bits được gán tuần tự khi một tiến trình mới khởi tạo.
 Mỗi tiến trình có một tiến trình cha. Các tiến trình trong hệ điều hành Linux được sắp xếp thành cây tiến trình, với tiến trình khởi tạo (init) là tiến trình gốc (root). ID của tiến trình cha gọi là ppid.
 Khi tham chiếu đến ID của tiến trình trong chương trình  C hoặc C++, sử dụng kiểu pid_t, được định nghĩa trong <sys/type.h>.
 Một số lời gọi hệ thống có thể lấy được các thông số này:
int   getpid()    // trả về pid của tiến trình đang chạy.
int   getppid() // trả về pid của tiến trình cha của tiến trình đang chạy.
Ví dụ: Chương trình print-pid.c
#include <stdio.h>
#include <unistd.h>
int main()

printf("ID cua tien trinh la %d!

", (int) getpid());
printf("ID cua tien trinh cha la %d!

", (int) getppid());
return 0;
}


II – Xem các tiến trình
 Lệnh ps  hiển thị các tiến trình hiện diện trên hệ thống. Tại terminal gõ lệnh:
# ps
ps chỉ ra 2 tiến trình: bash là shell đang chạy terminal; và ps
 Để biết chi tiết hơn về các tiến trình đang chạy trên hệ điều hành, gõ lệnh:
# ps -e -o pid,ppid,command
Chú ý quan sát pid và ppid của tiến trình init và tiến trình ps.


III – Khởi tạo tiến trình
 Có 2 kỹ thuật để khởi tạo tiến trình mới:
1. Cách thứ nhất ít được dùng vì không hiệu quả và không an toàn.
2. Cách thứ hai phức tạp  hơn nhưng rất linh động, tốc độ, và an toàn.

III.1. Hàm system
 int system(“shell command” )
 Hàm system trong thư viện chuẩn của C cung cấp cách dễ nhất để thực hiện lệnh trong phạm vi một chương trình, nếu lệnh gõ được ở shell. Trên thực tế, system tạo ra một tiến trình con chạy Bourne shell (/bin/sh) và chuyển lệnh để cho shell thực hiện.
Ví dụ:
#include <stdlib.h>
int main()
{
int return_value;
return_value = system("ls -l /");
return return_value;
}
Hàm system trả về trạng thái exit của shell. Nếu shell không chạy, system trả về 127; nếu xãy ra lỗi khác system trả về -1.

III.2. Hàm fork và exec
int fork()
int exec()
 Linux cung cấp  hàm fork() để tạo ra một tiến trình con là một bản sao của tiến trình cha. Linux cũng cung cấp hàm khác là exec() tạo ra tiến trình riêng biệt để kết thúc một instance của một chương trình thay vì trở thành một instance của một chương trình khác.
Ví dụ1: Chương trình fork.c sử dụng hàm fork()
#include <stdio.h>
#include <unistd.h>
int main()
{
int child_pid;
printf("Chuong trinh chinh co ID cua tien trinh la %d!

", (int) getpid());
child_pid = fork();
if (child_pid !=0)
{
printf("Day la tien trinh cha, voi ID la %d!

", (int) getpid());
printf("ID cua tien trinh con la %d!

", child_pid);
}
else   printf("Day la tien trinh con, voi ID la %d  %d!

", (int) getpid(),child_pid);
return 0;
}

Ví dụ 2: Chương trình fork_exec.c sử dụng hàm fork() & exec()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int spawn(char * program, char* *arg_list)
{
int child_pid=fork();
if(child_pid !=0) return child_pid;
else{
execvp (program, arg_list); fprintf(stderr,"Loi xay ra trong execvpn"); abort();
}

int main()
{
char* arg_list[]={"ls","-l","/",NULL};
spawn("ls", arg_list);
printf("Ket thuc chuong trinh chinh

");
return 0;
}


IV – Kết thúc tiến trình
Một tiến trình có thể kết thúc bằng một trong hai cách
- Gọi hàm exit
- Gặp hàm return của hàm main của chương trình.
Khi tiến trình kết thúc, mỗi tiến trình có mã kết thúc (exit code): một số trả về cho tiến trình cha của nó.

IV.1. Hàm kill()
#include <sys/type.h>
#include <signal.h>
int kill (int child_pid, int signal number);
- child_pid: ID của tiến trình kết thúc.
- signal number: Sử dụng SIGTERM là hằng mặc định để kết thúc tiến trình dùng hàm kill.
Nếu exit code trả về là 0 chương trình kết thúc thành công.Nếu exit code trả về giá trị khác 0 chương trình kết thúc lỗi.

IV.2. Chờ cho tiến trình kết thúc
Trong một số trường hợp tiến trình cha chờ cho đến khi một trong số tiến trình con kết thúc, thực hiện lời gọi hệ thống wait.
wait(int *child_status);
Một số lời gọi hệ thống tương tự:
waitpid: chỉ rõ tiến trình con kết thúc
wait3: thu hồi CPU để kết thúc tiến trình con

IV.3. Các tiến trình Zombie
Nếu tiến trình con kết thúc trong khi tiến trình cha đang gọi wait, tiến trình con và trạng thái kết thúc sẽ bị bỏ qua.
Nhưng điều gì sẽ xãy ra khi một tiến trình con kết thúc và tiến trình cha không gọi  wait? Liệu có bỏ qua đơn giản như vậy không? Câu trả lời là Không. Bởi vì, các thông tin sẽ bị đánh mất. Vì vậy, thay vì kết thúc tiến trình trở thành tiến trình Zombie.
=> Một tiến trình Zombie là tiến trình kết thúc nhưng chưa xoá hẳn.

Ví dụ: Chương trình zombie.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{  int child_pid;
child_pid = fork();   /* Tao tien trinh con*/
if (child_pid > 0){
printf(" Day la tien trinh cha. Ngu 1 phut. Dang sleep

");
sleep(60);
}
else     exit(0); /* Day la tien trinh con. Ket thuc*/
return 0;
}

=> Biên dịch và chạy chương trình từ dòng lệnh.
=> Tại terminal gõ lệnh
$ ps -e -o pid, ppid, stat, cmd


V – Giao tiếp giữa các tiến trình
Có 5 kiểu giao tiếp giữa các tiến trình:
Bộ nhớ chia sẻ (Shared memory). Bộ nhớ ánh xạ (Mapped memory). Giao tiếp bằng đường ống (Pipes). FIFO. Socket.

V.1. Bộ nhớ chia sẻ (Shared memory)
Cho phép các tiến trình giao tiếp bằng cách đọc và ghi lên một vùng nhớ được chỉ ra.
Đây là cách đơn giản nhất cho phép hai hay nhiều tiến trình giao tiếp với nhau bằng cách truy cập đến cùng bộ nhớ.
Khi một tiến trình thay đổi bộ nhớ, tất cả các tiến trình khác cũng sẽ thay đổi theo.
Một số hàm thông dụng:
#include <sys/shm.h>
#include <sys/stat.h>

V1.1.  Hàm tạo phân đoạn bộ nhớ chung
int  shmget(int key, int shared_segment_size, int flag);
- key: chỉ phân đoạn được tạo ra. Sử dụng hằng  IPC_PRIVATE mặc  định tạo ra phân đoạn mới.
- shared_segment_size: số byte của phân đoạn. Vì segment cấp phát phân trang nên số byte là bội số của 1 trang.
- flag: Giá trị của flag có thể là:
IPC_CREAT: chỉ rõ một segment mới sẽ được tạo.
IPC_EXCL: luôn được dùng với IPC_CREAT, tạo ra lỗi nếu segment đã tồn tại. Nếu flag này không chỉ ra và key của một segment đã dùng, shmget trả về segment đã tồn tại thay vì tạo ra một segment mới.
Mode flag: 9 bits liên quan đến owner, group, và cách truy cập đến segment. Các hằng này được định nghĩa trong <sys/stat.h>. (Chẳng hạn: S_IRUSR, S_IWUSR, S_IROTH, S_IWOTH,...)
Ví dụ:
int segment_id = shmget (shm_key, getpagesize(), IPC_CREAT | S_IRUSR | S_IWUSER);
=> Nếu lời gọi thành công, shmget trả về phân đoạn. Nếu phân đoạn sẵn sàng, cho phép  truy cập.

V.1.2. Hàm gắn kết phân đoạn bộ nhớ chung
char *shmat(int segment_id, int * address, int flag);
- segment_id: SHMID trả về bởi hàm shmget.
- address: con trỏ trỏ đến địa của segment mà ở đó ta cần ánh xạ. Nếu chỉ NULL (0), Linux sẽ chọn địa chỉ có giá trị.
- flag: có thể là các hằng sau:
SHM_RND: làm tròn xuống bội số của kích thước trang,
SHM_RDONLY: segment chỉ đọc, không được ghi.
=> Nếu lời gọi thành công thì trả về địa chỉ của phân đoạn dùng chung. Tiến trình con được tạo ra bởi hàm fork() có thể được gắn kết vào phân đoạn này, và có thể gỡ bỏ nếu muốn.

V.1.3. Hàm gỡ bỏ gắn kết phân đoạn bộ nhớ chung
Khi dùng xong phân đoạn bộ nhớ chung, chúng phải được gỡ bỏ, dùng hàm:
char *shmdt(char *address);
- address: địa chỉ trả về bởi shmat.
=> Nếu phân đoạn được cấp phát lại, tiến trình sau cùng sử dụng nó.

V.1.4. Hàm chỉ định kích thước phân đoạn
void shmctl(int segment_id, struct shmid_ds *pointer, struct shmid_ds *shmbuffer);
- segment_id: chỉ ra phân đoạn bộ nhớ,
- pointer: Chỉ ra thông tin về phân đoạn bộ nhớ dùng chung. Có thể là các hằng sau:
IPC_STAT: Con trỏ trỏ đến struct shmid_ds. IPC_RMID: Xoá phân đoạn, và đối số thứ 3 phải là NULL

Ví dụ: Chương trình bộ nhớ chia sẽ shm.c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/stat.h>
int main()
{
int segment_id;
char* shared_memory; struct shmid_ds shmbuffer; int segment_size;
const int shared_segment_size = 0x6400;
/* Chi ra shared memory segment */
segment_id  =  shmget(IPC_PRIVATE,  shared_segment_size,  IPC_CREAT  |  IPC_EXCL  | S_IRUSR | S_IWUSR);
/* Gan ket phan doan bo nho chung*/
shared_memory = (char*) shmat(segment_id, 0, 0);
printf("Bo nho chia se gan ket den dia chi %p

", shared_memory);
/* Chi dinh kich thuoc phan doan */ shmctl(segment_id, IPC_STAT, &shmbuffer); segment_size = shmbuffer.shm_segsz;
printf("Kich thuoc segment: %d

", segment_size);
/* Ghi mot chuoi len phan doan bo nho dung chung*/
sprintf(shared_memory, "Hello, hello, hello!!!");
/* Go bo gan ket phan doan bo nho chung*/
shmdt(shared_memory);              
/* Gan ket lai phan doan bo nho chung voi dia chi khac*/ shared_memory = (char*) shmat(segment_id, (void*) 0x5000000, 0); printf("Bo nho chia se gan ket lai den dia chi %p

", shared_memory);
/* In ra chuoi tu bo nho chia se*/
printf("%s

", shared_memory);
/* Go bo gan ket phan doan bo nho chung*/
shmdt(shared_memory);
/* Chi dinh lai kich thuoc phan doan */ shmctl(segment_id, IPC_RMID,0); return 0;
}
=> Biên dịch và chạy chương trình từ dòng lệnh.
=> Tại terminal gõ lệnh
$ ipcs -m

V.2. Bộ nhớ ánh xạ (Mapped memory)
Bộ nhớ  ánh xạ tương tự như chia sẻ bộ nhớ, nhưng nó cho phép các tiến trình khác nhau có thể giao tiếp dựa vào một file dùng chung.
Bộ nhớ ánh xạ là một dạng giao tiếp giữa một file với bộ nhớ của tiến trình. Linux chia file thành các trang và sau đó copy chúng vào trang bộ nhớ ảo. Vì vậy, một tiến trình có thể đọc nội dung của file bằng cách truy cập bộ nhớ thông thường. Có thể chỉnh sửa nội dung của file bằng cách ghi vào bộ nhớ. Điều này giúp truy cập file nhanh hơn.

V.2.1. Ánh xạ một file thường (Ordinary File)
Để ánh xạ một file thông thường đến bộ nhớ của tiến trình, sử dụng lời gọi:
void* mmap(int *address, int filelength, int property, int flag , int fd, int
offset);
- address: địa chỉ của tiến trình. Nếu bằng NULL cho phép Linux chọn một địa chỉ bắt đầu.
void* mmap(int *address, int filelength, int property, int flag , int fd, int offset);
- filelength: Độ dài của vùng nhớ.
- property: Thuộc tính bảo vệ vùng nhớ ánh xạ. Có thể có các hằng sau:
PROT_WRITE: Quyền ghi PROT_READ: Quyền đọc PROT_EXEC: Quyền thực thi
- flag: Giá trị tuỳ chọn để ánh xạ. Có thể ánh xạ tất cả hay một phần của file vào bộ nhớ bằng các tuỳ chọn sau:
MAP_FIXED: Linux sử dụng địa chỉ mà bạn yêu cầu để ánh xạ đến file.
MAP_PRIVATE: Ghi lên phạm vi vùng nhớ riêng để copy file mà không attached file. Tiến trình khác không nhìn thấy.
void* mmap(int *address, int filelength, int property, int flag , int fd, int offset);
MAP_SHARED: Ngược lại với MAP_PRIVATE. Mode này được dùng khi ánh xạ bộ nhớ cho IPC.
- fd: Bộ mô tả file mở file để ánh xạ.
- offset: Vị trí tương đối của file để bắt đầu.
=> Nếu lời gọi thành công, trả về con trỏ trỏ đến địa chỉ bắt đầu của vùng nhớ. Nếu lỗi, trả về MAP_FAILED.

Ví dụ:
file_memory = mmap(0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd,0); //Để Ghi
file_memory = mmap(0, FILE_LENGTH, PROT_READ  |PROT_WRITE, MAP_SHARED, fd,0); // Để Đọc & Ghi

V.2.2. Gỡ bỏ vùng nhớ ánh xạ
Sau khi hoàn tất ánh xạ bộ nhớ, vùng nhớ phải được gỡ. Bằng cách sử dụng hàm munmap.
void munmap(int *address, int filelength);
Linux tự động gỡ bỏ vùng bộ nhớ ánh xạ khi tiến trình kết thúc.

Ví dụ 1: Chương trình ghi ngẫu nhiên một số lên file ánh xạ bộ nhớ mmap- write.c.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#define FILE_LENGTH 0X100
int random_range (unsigned const low, unsigned const high)
{  unsigned const range = high - low + 1;
return   low   +   (int)   (((double)range)   *   rand()   /(RAND_MAX   +   1.0));   }
int main(int argc, char* const argv[])
{
int fd;
void* file_memory;
/* Khoi tao bo so ngau nhien */
srand (time (NULL));
/* Chuan bi mot file du lon de chua so nguyen unsigned */
fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
lseek(fd, FILE_LENGTH+1, SEEK_SET);
write(fd,"",1);
lseek(fd, 0, SEEK_SET);
/* Tao bo nho anh xa */
file_memory = mmap(0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd,0);
close(fd);
/* Ghi so nguyen random len vung bo nho anh xa*/
sprintf((char*) file_memory,"%d

", random_range(-100,100));
/* Go bo bo nho */
munmap(file_memory, FILE_LENGTH);
return 0;
}

Ví dụ 2:  Chương trình đọc một số nguyên từ file ánh xạ bộ nhớ và nhân đôi nó. mmap-read.c.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#define FILE_LENGTH 0x100
int main(int argc, char* const argv[])
{
int fd;
void* file_memory;
int integer;
/* Mo file */
fd = open(argv[1], O_RDWR, S_IRUSR | S_IWUSR);
/* Tao bo nho anh xa */
file_memory    =    mmap(0,    FILE_LENGTH,   PROT_READ    |PROT_WRITE, MAP_SHARED, fd,0);
close(fd);
/* Doc so nguyen, in ra va nhan doi chung */ scanf(file_memory,"%d",&integer); printf("Gia tri: %d

", integer);
sprintf((char*) file_memory,"%d

", 2*integer);
/* Go bo bo nho */
munmap(file_memory, FILE_LENGTH);
return 0;
}
=> Để chạy 2 ví dụ này. Giả sử ánh xạ đến file có tên ./integer-file. Tại terminal lần lượt gõ các lệnh sau:
$ ./mmap-write ./integer-file
$ cat ./integer-file
-76
176
$ ./mmap-read ./integer-file
-76
176
Gia tri: 7466272
$ cat ./integer-file
14932544

V.3. Giao tiếp bằng đường ống (Pipes)
Pipe cho phép giao tiếp tuần tự từ tiến trình này đến các tiến trình khác.
Một pipe là một thiết  bị truyền thông tuần tự.; dữ liệu có thể đọc từ pipe  cùng lúc ghi lên pipe.
Pipe còn được dùng để liên lạc giữa hai thread trong một tiến trình hay giữa tiến trình cha và tiến trình con.
Trong shell ký hiệu | là để tạo ra pipe.
Ví dụ:
$ ls | less
Shell cũng tạo ra pipe kết nối đầu ra của tiến trình ls với đầu vào của tiến trình less.
Một pipe là một kênh liên lạc trực tiếp giữa hai tiến trình : dữ liệu xuất của tiến trình này được chuyển đến làm dữ liệu nhập cho tiến trình kia dưới dạng một dòng các byte.
Khi một pipe được thiết lập giữa hai tiến trình, một trong chúng sẽ ghi dữ liệu vào pipe và tiến trình kia sẽ đọc dữ liệu từ pipe. Thứ tự dữ liệu truyền qua pipe được bảo toàn theo nguyên tắc FIFO.
Một pipe có kích thước giới hạn. Nếu tiến trình ghidữ liệu nhanh hơn tiến trình đọc thì tiến trình ghi bị chặn lại cho đến khi tiến trình đọc đọc bớt đi 1byte. Và ngược lại.
Lời gọi pipe tạo ra bộ mô tả file, bộ mô tả file này chỉ có giá trị trong tiến trình này và tiến trình con của nó. Bộ mô tả file của tiến trình không thể bỏ qua các tiến trình không liên quan. Tuy nhiên, khi tiến trình gọi fork, các bộ mô tả file  được copy đến tiến trình con mới tạo. Vì vậy, pipe chỉ có thể nối trực tiếp đến các tiến trình liên quan.

# include < unistd.h >
int pipe_fds[2];
int read_fd;
int write_fd;
int pipe(int pipe_fds[]);
read_fd = pipe_fds[0]'
write_fd = pipe_fds[1]'                                                

V.3.2. Giao tiếp giữa tiến trình cha và các tiến trình con
Để tạo một pipe cầ cung cấp một mảng nguyên gồm 2 phần tử. Bộ mô tả file sẽ cho phép đọc lên phần tử 0 và ghi lên phần tử 1. Hàm tạo pipe như sau:
# include < unistd.h >
int pipe_fds[2];
int read_fd;
int write_fd;
int pipe(int pipe_fds[]);
read_fd = pipe_fds[0]' write_fd = pipe_fds[1]'

Ví dụ: Sử dụng pipe giao tiếp với tiến trình con. Chương trình pipe.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
void writer(const char* message, int count, FILE* stream)
{ for (; count > 0; --count){
fprintf (stream, "%s

", message);
fflush(stream);
sleep(1);
}
}
// Đọc ngẫu nhiên chuỗi từ stream void reader(FILE* stream)
{
char buffer[1024];
while (!feof(stream)&& !ferror (stream) && fgets(buffer, sizeof (buffer), stream) != NULL)
fputs (buffer, stdout);
}
int main()
{
int fds[2];
pid_t pid;
pipe (fds); //Tao pipe pid = fork();
if (pid ==(pid_t)0){
FILE* stream;
close (fds[1]);
stream = fdopen(fds[0], "r");
reader (stream);
close (fds[0]);
}
else {
FILE* stream;
close (fds[0]);
stream = fdopen (fds[1],"w");
writer ("Hello, hello, hello!!!", 5, stream);
close(fds[1]);
}
return 0;
}

V.4. FIFO
Tương tự như đường ống, trừ khi nó không liên quan đến các tiến trình có thể truyền thông được bởi vì đường ống đã gán 1 tên trong hệ thống file.

V.5. Socket
Truyền thông được giữa các tiến trình không liên quan ngay cả khi chúng ở trên các máy tính khác nhau.

KẾT THÚC

Bạn đang đọc truyện trên: Truyen2U.Pro