Dạo gần đây mình bắt đầu các bài viết về học thuật bằng tiếng Việt - tiếng mẹ đẻ, và tất nhiên mình gặp nhiều khó khăn trong việc tìm kiếm từ học thuật phù hợp vì mình quen với việc viết bằng tiếng Anh rồi. Đây cũng là một sự trải nghiệm mới mẻ và đầy hứng thú. Hiện tại mình đang có 2 series đi sâu vào các đề tài về Lập trình Linux và Lập trình nhúng, tuy nhiên trong bài viết này mình sẽ viết đôi dòng về một trong những con bugs mà mình hay gặp report từ phía khách hàng (mình làm joint venture nên khách hàng là 2 công ty mẹ :v). Ok bắt đầu. Thông thường chúng ta dễ nhầm lẫn hai khái niệm là signaling và locking, tương ứng với semaphore và mutex. Mình sẽ không giải thích dài dòng về hai khái niệm này vì mình để dành cho bài viết trong series, và tất nhiên mình đang nói tới POSIX semaphore và POSIX thread. Scenario: Hệ thống mình đang bảo trì bao gồm một shared lib: libdlt.so, lib này bao gồm một ring buffer để lưu trữ các cấu trúc dữ liệu người dùng từ phía users. Buffer này sẽ được lock gọn ghẽ sao cho tại một thời điểm chỉ có một user truy cập vào vùng critical này. Problem: Hệ thống trong 4 ngày test và trải qua 1000 lượt test mỗi ngày bao gồm đủ các loại test ABCXYZ thì nhoi ra một con bug: SIGSEGV Bước 1: RCA - Root cause analyzing Phân tích vấn đề là một khâu khá quan trọng trước khi đi đến các bước tiếp theo. Chúng ta nhìn nhận lỗi Segmentation Fault có nhiều nguyên nhân: + Dereferencing a NULL pointer + Accessing unititialized pointer + Buffer Overflow + Invalid Memory Access + etc Ban đầu mình bị lừa khi pointer của Buffer phía user truy cập vào vùng nhớ không xác định (Invalid Memory Access). Tuy nhiên, nguyên nhân sâu xa nằm ở việc vùng nhớ này nằm ngay bên trong phân vùng được cấp, và vùng này đang trong quá trình giải phóng (free). Lật ngược lại vấn đề, mình tự xây dựng một mô hình user đơn giản để mô phỏng. Hóa ra có 2 users đang chạy song song, và 1 trong 2 gọi lệnh exit(), trong khi hàm handler trong lib đã đăng kí atexit(), nghĩa là bất cứ khi nào exit() được gọi thì hàm này sẽ được gọi theo. Xem thêm tại:
Bước 2: Tiến hành reproduce Vấn đề nan giải xuất hiện do các bugs loại này thường xảy ra hy hữu trong một số điều kiện nhất định, khi các bánh răng ăn khớp nhau hơi trật xí và bum! core dumped. Đầu tiên soi vào core mà khách gửi mình đánh hơi và tiến hành khoanh vùng khu vực có khả năng cao xảy ra race. Sau đó mình tiến hành delay các threads để xem xét sự va chạm của tụi nó, kết quả mình reproduce được như sau:
Program terminated with signal SIGSEGV
Program terminated with signal SIGSEGV
Tại thời điểm mà thread 2 đang access vào buffer và chờ (sleep) thì một thread khác đang cố log vào buffer này, dẫn đến lỗi SIGSEGV (Segmentation fault). Trong quá trình này, biến sem_t của mình thật sự đang lock mọi người ạ, và vị trí bị lỗi đang dereference một con trỏ NULL (shm = 0x0):
(gdb) p dlt_mutex $1 = {__size = '\000' <repeats 31 times>, __align = 0} (gdb) p dlt_user.startup_buffer $2 = {shm = 0x0, size = 49988, mem = 0x0, min_size = 50000, max_size = 500000, step_size = 50000}
Bước 3: Đặt câu hỏi + Liệu vùng critical này có thật sự được lock? + Cơ chế locking nó đang dùng là gì? Với những nghi vấn đó, mình lật lại code base thì phát hiện vùng critical này đang sử dụng semaphore. Tuy nhiên, semaphore này được dùng như một count semaphore mặc dù comment để là binary semaphore :v, và dòng code này đã tồn tại 13 năm (từ lúc bắt đầu phát triển SW này). OK what next? Có một số vấn đề ở đây, thứ nhất semaphore, mà thậm chí là binary semaphore, được sử dụng như một cơ chế tín hiệu (signaling mechanism), trong khi mutex là một loại binary semaphore đặc biệt mà ở đó sử dụng như cơ chế khóa (locking mechanism). Tưởng tượng đơn giản bạn đang sử dụng phòng tự học và chiếm cả phòng, khi bạn rời khỏi phòng và người kế tiếp nhận được thông tin thì họ mới được sử dụng phòng, đó là binary semaphore - signaling. Tuy nhiên, bạn rời phòng, khóa lại và giữ chìa, như vậy không ai có thể sử dụng trừ khi bạn mở nó ra, đây chính là mutex - locking. Rõ ràng là do một số lỗi implementation mà semaphore của lib ngoài nhận giá trị 0/1 còn nhận thêm giá trị lớn hơn 1 trong một số race condition. Đây chính là nguyên nhân mà vùng critical được access vào rất dễ dàng khi mình treo threads. Bước 4: Fix the bugs Sai là sai ngay từ đầu khi không dùng cơ chế locking, như vậy mình chỉ việc thay semaphore bằng mutex, và lock chính xác chỗ cần lock.
DONE! Cảm ơn mọi người đã dành thời gian đọc. Các bạn có thể tham khảo fix của mình tại: