Review một buổi phỏng vấn về Linux kernel với công ty nước ngoài

Chủ đề này đang được nhiều bạn quan tâm, hi vọng mình có thể chia sẻ kinh nghiệm được với mọi người. Bản thân mình cũng từng phỏng vấn khá nhiều với công ty nước ngoài về embedded Linux. Tất nhiên là tiếng anh 100%. Tuy nhiên, nếu phỏng vấn thực sự sâu về kernel, bao gồm cả về các phần khác của kernel common như memory, file system thì mới chỉ có 2 lần. Trong bài này mình sẽ chia sẻ về lần thứ 2 tức là lần gần đây nhất.

Trước khi vào phần cảm nhận, các bạn cần hiểu rõ, phân biệt được về sự khác nhau khi phỏng vấn embedded Linux tại các công ty ở Việt Nam (có thể là công ty nước ngoài nhưng đặt trụ sở ở Việt Nam) và các công ty hoàn toàn ở nước ngoài.

Đầu tiên là phần kiến thức về C/C++ hoặc OOP. Khi bạn phỏng vấn ở VN mọi người thường hỏi khá sâu và có những câu hỏi đánh đố. Trái lại, phần embedded Linux/Linux kernel thường chỉ hỏi về kinh nghiệm làm việc, các kiến thức cơ bản, cách bạn xử lý vấn đề trong dự án. (Ví dụ: interviewer sẽ hỏi bạn về những dự án bạn đã làm, nếu như bạn trả lời bạn đã từng porting u-boot thì họ sẽ hỏi thêm các bước khi bạn porting u-boot, ngoài ra interviewer có thể hỏi bạn các câu hỏi về dự án của họ, ví dụ như bạn có biết về ethernet driver không, bạn có biết yocto không? Nếu bạn biết thì họ sẽ hỏi thêm). Nói tóm lại thì các câu hỏi về embedded Linux sẽ xoay quanh về kinh nghiệm, cách xử lý tình huống đã gặp.

Tuy nhiên, đối với 2 lần mình phỏng vấn về embedded Linux tại 2 công ty khá nổi tiếng trên thế giới thì họ có cách phỏng vấn hoàn toàn khác. Họ sẽ hỏi các câu hỏi khá ngẫu nhiên về kernel, driver, về hệ điều hành, ngoài ra sẽ có các câu hỏi khá hóc búa đòi hỏi bạn phải dùng kiến thức để suy luận.

Từ những trải nghiệm cá nhân, mình đánh giá phỏng vấn về embedded Linux ở các công ty Việt Nam khá dễ so với khi phỏng vấn cho công ty nước ngoài. Chắc chỉ tầm 2/10. Bản thân mình khi đi phỏng vấn mình cũng không muốn hỏi câu nào đánh đố ứng viên về embedded Linux cả, vì mình sợ các bạn ý bị tâm lý.

Để các bạn hình dung rõ về cuộc phỏng vấn hơn, dưới đây là một đoạn hội thoại giữa mình và người phỏng vấn mình khi đó. Cho đơn giản, (I) là viết tắt của interviewer, (M) là viết tắt của Me có nghĩa là mình.

I 🐔 : Tạm bỏ qua phần giới thiệu bản thân, chúng ta sẽ đi vào phỏng vấn luôn nhé.

I 🐔: Bạn có biết device tree là gì không? Tác dụng của nó như thế nào?

M 🐸 : Device tree là một kỹ thuật dùng để mô tả phần cứng. Nó có thể mô tả nhiều thứ như địa chỉ vật lý, chân pin, ngắt,… Dùng device tree thì không cần mô tả phần cứng trong source code và sẽ dễ dạng để dùng 1 driver điều khiển nhiều dòng device khác nhau.

I 🐔: Vậy hệ điều hành nó mapping giữa device node với driver như thế nào?

M 🐸: Hệ điều hành nó dựa vào chuỗi compatible trong device tree. Mỗi device tree node có một chuỗi string compatible. Mỗi driver có một struct device_of_match, struct này cũng có chuỗi string compatible. Khi hệ điều hành boot lên, ứng với mỗi device tree node nó sẽ khởi tạo ra một struct device, mỗi 1 driver nó sẽ khởi tạo ra 1 struct driver. Sau đó nó sẽ tìm kiếm chuỗi tương thích từ danh sách cacstruct device và struct driver và thực hiện mapping giữa device tree node và driver. Nếu mapping thành công thì hàm probe sẽ được gọi ra.

I 🐔: Bạn hiểu như thế nào về ngắt không? Rising edge và falling edge là gì?

M 🐸: Cả rising edege và falling edge đều có thể mô tả được trong trường irq của device tree. Khi điện áp chuyển từ thấp sang cao thì rising edge được tạo ra, khi điện áp chuyển từ cao xuống thấp thì falling edge được tạo ra.

I 🐔: Bạn có biết cách hệ điều hành gọi hàm xử lý ngắt như thế nào không?

M 🐸: Mỗi khi ngắt vật lý xảy ra thì ngắt sẽ được truyền cho irq-controller driver xử lý. Trong driver của mình sẽ có một đoạn đăng ký interrupt handler. Sau khi đăng ký thì địa chỉ của interrupt handler sẽ được lưu trên 1 table của OS. irq-controller sẽ gọi ra interrupt-handler dựa vào bảng đó.

I 🐔: Trong irq-handler chúng ta có thể làm 1 số việc như wait input from user không?

M 🐸: Không. Vì làm thế hệ thống có thể bị treo.

I 🐔: Bạn có thể giải thích lí do tại sao khi làm vậy hệ thống lại bị treo được không?

M 🐸: Tại vì irq-handler có độ ưu tiên cao, các task trên tầng user có độ ưu tiên thấp (Thực ra vì đoạn này giải thích phức tạp mà mình lại bị bí từ vựng nên không biết nói sao).

I 🐔: Tại sao lại như vậy? Nếu như hệ thống mutil core thì tôi có thể có 1 core xử lý ngắt, core còn lại có thể wait input từ user. Như vậy thì sẽ không bị treo nữa.

M 🐸: (Đúng ra đoạn này mình phải giải thích được là nó sẽ không gây treo cả hệ thống mà nó sẽ gây treo nội bộ 1 core xử lý ngắt đó thôi. Nhưng mình không biết nói sao nên giải thích lòng vòng 1 hồi)
Sau đó mình tóm tắt lại là do cái task wait input chạy ở tầng user nó có độ ưu tiên thấp, nên khi chạy nó thì core có thể chuyển đổi sang task khác ở tầng user có cùng độ ưu tiên, kết quả là cái irq-handler kia có thể lặp lại mãi mãi. (Trả lời như này thực sự không đúng lắm. Đúng ra phải giải thích được hiện tường dead lock trong nội bộ của irq-handler, cái này thì không liên quan đến chuyện có bao nhiêu core).

I 🐔: Trong trường hợp tôi bắt buộc phải get input from user trong irq-handler thì tôi phải làm thế nào để hệ thống không bị treo?

M 🐸: Bạn có thể chia irq-handler ra làm 2 phần: 1 phần chạy trong top-half và 1 phần chạy trong bottom half. Bên cạnh đó, trong irq-handler bạn nên add-timer hoặc dùng work queue để thực hiện công việc get input from user.

I 🐔: Vậy bạn có biết sự khác nhau giữa work queue và tasklet không?

M 🐸: Phần này tôi không nhớ lắm.

I 🐔: (Nói thêm 1 đoạn về tình huống irq-handler gọi ra thread khác ở dưới kernel).

I 🐔: Bạn có biết về cơ chế synchronization của kernel không?

M 🐸: Có nhiều kỹ thuật để synchronization dưới kernel lắm. Ví dụ như RCU, mutex_lock, spin_lock.

I 🐔: RCU là gì?

M 🐸: RCU có nghĩa là đọc bản cập nhật bản sao. Nó sẽ tạo ra 1 vùng nhớ vật lý để chia sẻ giữa các thread của kernel. Nhiều thread có thể cùng đọc 1 lúc mà không cần khóa. Nhưng nếu bạn muốn ghi thì phải tạo ra 1 bản sao. Ghi vào bản mà bạn vừa nhân bản từ bản sao đó. Tiếp theo, đợi đến khi không ai đọc nữa thì khóa vùng nhớ đấy lại rồi tiến hành cập nhật.

I 🐔: Bạn có thể cho chúng tôi biết về sự khác nhau giữa mutex_lock và spin_lock không?

M 🐸: Spin_lock là busy lock. Khi nhảy vào hàm spin_lock nếu như key không có sẵn thì cpu sẽ dành full cycle để take lock chứ không chuyển sang làm việc khác. Còn mutex_lock thì nếu như key không có sẵn thì cpu sẽ chuyển sang làm việc khác. Sau đó nó sẽ định kỳ kiểm tra lại key.

I 🐔: Làm thế nào mutex_lock có thể làm được như vậy? (ý là hỏi cơ chế vận hành bên trong của mutex để làm được như vậy)

M 🐸: Trong hàm mutex nó có 1 đoạn check lock, nếu như lock bị lấy thì nó sẽ lưu lại bối cảnh của thread hiện tại và lưu vào 1 queue. Sau đó nó sẽ chuyển sang chạy thread khác và định kỳ kiểm tra lại các lock ở trong queue trên.

I 🐔: Tôi không hiểu rõ ý bạn vừa giải thích lắm, “Nó” mà mày nói ở đây là gì?

M 🐸: (Đoạn này mình cũng bí từ tiếng anh nên đành phải giải thích lại giống như mình nói ở trên. Bạn ý cũng có vẻ hiểu.)

I 🐔: Tôi thấy trong hàm mutex_lock nó chỉ đơn giản gọi ra scheduler là xong. <Ý của bạn interviwer chắc là hàm need_resched()>.

(Thực ra mình định cãi là cái hàm need_resched nó cũng làm tương tự như cách mình giải thích. Nhưng mình nghĩ chỗ đấy mà giải thích thì nó cũng phức tạp, mình không chắc là có thể nói gãy gọn được bằng tiếng anh không nên mình thôi. À phỏng vấn ở đấy là dạng gọi điện giống như điện thoại chứ không phải dùng video và được viết ở trên bảng đâu).

I 🐔: Bạn có biết hàm fork() không? Nó hoạt động như nào?

M 🐸: Hàm fork() nó tạo ra 2 process cha và con bằng cách nó nhân bản process con từ process cha. Lúc nhân bản thì nó nhân bản toàn bộ không gian địa chỉ bộ nhớ và dữ liệu của process cha.

I 🐔: Ví dụ, tôi có 1 biến có giá trị rồi. Bạn thử giải thích giá trị của biến đó sau khi fork() ở cha và con. Nếu 1 người sửa biến đấy thì người còn lại sẽ nhìn thấy giá trị của biến đó như thế nào?

M 🐸: Cơ chế nhân bản của hàm fork() là lazy copy hoặc là copy on write. Tức là khi nó nhân bản thì nó chỉ nhân bản dải địa chỉ địa chỉ bộ nhớ ảo thôi. Còn dải bộ nhớ vật lý thì nó để nguyên. Sau khi nhân bản thì cả cha và con đều trỏ về cùng 1 vùng nhớ vật lý. Tuy nhiên, nếu có người tiến hành chỉnh sửa giá trị trên vùng nhớ vật lý thì nó sẽ tiến hành nhân bản và vùng nhớ vật lý đó và tạo ra 2 vùng nhớ vật lý riêng biệt cho mỗi người. Vì vậy, nếu như 1 người sửa giá trị của 1 biến thì người còn lại sẽ vẫn nhìn thấy giá trị cũ của biến đó.

I 🐔: Dưới kernel nếu như tôi muốn truy cập vào 1 địa chỉ vật lý thì phải làm thế nào?

M 🐸: Bạn phải cấp phát 1 địa chỉ mapping ảo vào địa chỉ vật lý đó thông qua các hàm như io_remap. Sau đó truy cập vào vùng nhớ đó thông qua địa chỉ ảo được cấp phát.

I 🐔: Thế trên tầng user nếu muốn truy cập vào 1 địa chỉ vật lý thì tôi làm thế nào?

M 🐸: Bạn phải dùng hàm mmap() rồi trỏ vào file /dev/mem. File đó nó đại diện cho toàn bộ dải địa chỉ vật lý của hệ thống.

I 🐔: Tất cả các process đều mmap được vào /dev/mem à?

M 🐸: Không. Chỉ những process với quyền root mới mmap vào /dev/mem được.

I 🐔: Thế nếu tôi có 1 process không có quyền root thì tôi không có cách nào truy cập vào 1 địa chỉ vật lý được à?

M 🐸: Bạn phải viết 1 driver, tạo ra 1 device file trong thư mục /dev. Driver của bạn phải tự vận hành hàm mmap, sau đó process của bạn sẽ mmap vào thiết bị do bạn tạo ra.

I 🐔: Trong kernel tôi thích mapping vào địa chỉ vật lý nào cũng được à?

M 🐸: Uhm, miễn là bạn phải dùng các hàm như io_remap().

I 🐔: Bạn có chắc không? Nếu như tao muốn truy cập vào vùng nhớ DMA thì tao cũng dùng io_remap được à? Tao nghĩ là io_remap chỉ map được cho các địa chỉ IO thôi.

M 🐸: (Chỗ này thì mình cũng không chắc lắm. Đúng ra như mình biết thì để truy cập vào vùng dma họ sẽ dùng các hàm như page_mapping. Nhưng đoạn giải thích được bản chất của các hàm như page_mapping rồi io_remap bằng tiếng anh cũng phức tạp. Do mình không chắc chắn là nói được nên mình trả lời là dùng io_remap là map được hết. Khả năng chỗ này mình trả lời vậy là sai.)

I 🐔: Bạn có biết về memory context và memory context switching không?

M 🐸: Xin lỗi, tôi không hiểu câu hỏi của bạn lắm.
(Mình thực sự hơi đơ vì mình chưa nghe về cụm từ này bao giờ. Với lúc họ hỏi thì họ nói 1 đoạn khá dài chứ không ngắn gọn như câu trên nên mình sợ là mình không thực sự hiểu câu hỏi của họ).

I 🐔: Thôi đơn giản là trên tầng user mỗi 1 process nó có 1 memory context riêng để bảo vệ bộ nhớ không bị truy cập không hợp lệ giữa các process. (Đoạn này thì mình hiểu cái memory context mà họ hỏi nó là memory address space của process).

I 🐔: Bạn có biết về cái memory context này không? Nếu có thì bạn giải thích đi?

M 🐸: Trong Linux nó có cơ chế virtual memory. Mỗi 1 process sẽ chạy trên 1 không gian địa chỉ bộ nhớ riêng biệt.

I 🐔: Dưới kernel có trường hợp 2 địa chỉ ảo cùng trỏ vào 1 địa chỉ vật lý không?

M 🐸: Không. Vì tất cả thread dưới kernel đều chung 1 không gian địa chỉ. Nên không có chuyện 2 địa chỉ ảo trỏ vào 1 địa chỉ vật lý được.

I 🐔: Thế trên tầng user thì sao?

M 🐸: Tầng user thì được, do có nhiều process mà mỗi process chạy trên 1 không gian riêng. Ví dụ như tôi chia sẻ vùng nhớ giữa các process thì kết quả là sẽ có nhiều địa chỉ ảo trỏ vào chung vùng nhớ vừa được chia sẻ.

I 🐔: Bạn có biết trong Linux có mấy loại context không?

M 🐸: 3. 1 là user-space, 2 là kernel và 3 là interrupt.

(Mình thực sự không hiểu từ context này ám chỉ cái gì, vì trong hệ điều hành họ thường ghép nó với 1 từ khác nữa. Đoạn này mình đoán là như vậy)

I 🐔: Bạn thử mô tả quá trình chuyển đổi context từ tầng user-space xuống tầng kernel đi.

M 🐸: Khi chuyển đổi từ user xuống kernel thì đầu tiên là MMU nó phải load lại memory page table mới-TLB. Sau đó nó phải thay đổi giá trị của 1 thanh ghi của cpu là context register. Mỗi khi thực hiện bất cứ câu lệnh assembly nào thì cpu nó luôn check giá trị trong context register để xem có được quyền hay không.
(Thực ra quá trình chuyển đổi context gồm rất nhiều bước chứ không đơn giản như mình nói. Nhưng do nói bằng tiếng anh nên mình chỉ muốn nói đơn giản thôi, sợ nói phức tạp thì các bạn ý không hiểu).

I 🐔: Thế bạn có biết về về cached memory không? Nếu có thì bạn hãy giải thích cách hoạt động của hardware khi dùng cached memory đi.

M 🐸: (Mình trả lời lòng vòng mất 1 đoạn khá dài do bí từ. Sau đó họ giải thích lại câu hỏi cho mình. Họ cũng khá kiên nhẫn giải thích để mình trả lời lại chứ không next sang câu hỏi khác).

I 🐔: Trên tầng user thì tao dùng cách nào để tránh cached memory khi tôi có nhiều thread truy cập vào 1 biến?

M 🐸: Trên tầng user thì mày dùng từ khóa volatile.

I 🐔: Bạn thử giải thích tại sao từ khóa volatile lại làm được như vậy?

M 🐸: Khi bạn dùng từ khóa volatile thì mày đã chỉ định trình biên dịch biên dịch những đoạn code truy cập vào biến đó theo 1 cách đặc biệt. Khi biên dịch những đoạn mã assembly read/write và biến volatile thì nó sẽ không dùng mã assembly thông thường mà sẽ dùng 1 bộ bảng mã khác để by pass cached.

I 🐔: Thế dưới kernel thì nên dùng cách nào?

M 🐸: Đầu tiên bạn mapping vùng nhớ đó bằng io_remap, sau đó đọc ghi thông qua các hàm io_read, io_write.

I 🐔: Dưới kernel khi truy cập vào 1 địa chỉ bất kỳ, làm thế nào để biết được nó là vùng nhớ bị cached hay none cached?

M 🐸: Đầu tiên bạn lấy ra struct page từ địa chỉ ảo của vùng nhớ đó. Trong struct page có 1 trường cached flag, bạn có thể đọc giá trị trong đó để biết được vùng nhớ đó là cached hay none cached.

I 🐔: Bây giờ trong tay tôi có rất nhiều thứ như jtag, hardware debug, printk… Làm thế nào sau khi tôi ghi giá trị vào 1 biến, đảm bảo được giá trị đó đã ghi vào memory hay mới ghi vào cached.

M 🐸: Đầu tiên tôi sẽ dùng virtual_to_phy để lấy ra địa chỉ vật lý của biến đó. Sau đó, tôi io_remap vào địa chỉ vật lý của biến. Tiếp theo, tôi sẽ đọc lại 1 lần bằng io_read để kiểm tra xem giá trị bộ nhớ thật đã thay đổi chưa.
(Nói đến đây thì mình thấy họ phá lên cười, mình mới nhớ ra là ở trước họ nói họ có hardware debug nên mình trả lời lại)

M 🐸: Tôi sẽ dùng hardware debugger để đọc thẳng địa chỉ vật lý của biến đó rồi kiểm tra.

I 🐔: Bây giờ tôi có 2 process, 1 process mapping vào biến đó theo kiểu none cached, process còn lại mapping vào theo kiểu cached. Vậy khi 1 process ghi giá trị vào biến đó, làm thế nào khi tao đọc biến đó ở process còn lại thì tao đảm bảo là đọc dc giá trị đúng?

M 🐸: Sau khi bạn ghi xong thì bạn gọi ra hàm flush_cached() để nó synchronize vào bộ nhớ thật là được.

I 🐔: Cách bạn nói mới chỉ xử lý được 1 nửa trường hợp. Còn 1 tình huống nữa là tôi ghi từ thread none cached, giá trị được update trên vùng nhớ rồi. Tuy nhiên, khi đọc từ thread cached thì tôi vẫn đọc giá trị từ cached chứ ko phải từ vùng nhớ, như vậy thì trên cached thread tôi đọc vẫn là giá trị cũ.

M 🐸: (Mình có bảo khi bạn đọc hoặc ghi thì bạn gọi hàm synchronize để nó sync giữa cached và memory là được. Tuy nhiên có thể do mình nói họ không hiểu nên họ bảo như thế không được. Sau đó mình đành nói là cái tình huống này thì mình không biết cách xử lý. Thực ra trong quá trình mình trả lời họ cũng có giải thích thêm nhưng có lẽ do mình không hiểu nên mình không muốn trả lời câu này).

I 🐔: Bạn có thể gọi hàm cached validation để sync dữ liệu từ memory vào cached và gọi thêm hàm flush_cached để sync giữa cached và memory là được.

I 🐔 : Thế bạn có biết write_true() không?

M 🐸: Tôi lần đầu nghe thấy từ này.

(Họ giải thích thêm 1 đoạn về cached, sau đó hỏi lại mình là có 2 cách ghi dữ liệu vào bộ nhớ là write_back và write_true. Xong họ hỏi lại có có biết không? Mình trả lời là mình biết write_back).

I 🐔 : Bạn có thể giải thích write_back() được không?

M 🐸: Khi bạn ghi dữ liệu vào cached, đến 1 lúc nào đó kernel nó sẽ ghi lại từ cached vào vùn nhớ thì gọi là write_back.

I 🐔: Uhm đúng rồi, còn write_true là bạn ghi trực tiếp vào vùng nhớ và bỏ qua được cached đó.

I 🐔: Bạn có biết dùng hardware debugger không?

M 🐸: Tôi không biết, tôi thường debug bằng printk.

I 🐔: Dùng printk thì nhiều hạn chế lắm, bạn biết không?

M 🐸: uhm tôi biết, ví dụ như không printk được trong ngắt. Tuy nhiên, trong ngắt tôi sẽ print log ra 1 buffer sau đó dùng thread khác để print log từ buffer vào system log. Làm thế vẫn được.

I 🐔: Thế bạn biết dùng gdb với kgb không?

M 🐸: GDB thì tôi dùng nhiều còn KDB thì tôi chưa dùng bao giờ.

Tổng kết:

Các câu hỏi trên không thực sự quá khó, ít nhất đối với mình. Nếu như bỏ thời gian ra nghiên cứu kernel một cách bài bản thì sẽ trả lời được hết. Interviewer ở công ty thuộc top đầu nên câu hỏi cũng khó và sâu hơn. Các công ty nước ngoài khác câu hỏi sẽ dễ hơn. Được cái họ làm việc với người Việt nhiều rồi nên họ khá kiên nhẫn giải thích mỗi khi mình hiểu sai câu hỏi. Ngoài ra do tiếng anh mình không tốt nên khi phỏng vấn mình mất khá nhiều năng lượng cho việc suy nghĩ câu tiếng anh để trả lời. Đâu đó 70% sức lực cho tiếng anh và 30% sức lực cho việc suy nghĩ kỹ thuật.

1 thought on “Review một buổi phỏng vấn về Linux kernel với công ty nước ngoài”

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top