8 Commits

Author SHA1 Message Date
b0c7a463f7 [优化]优化说明文档 2024-09-05 12:55:06 +08:00
ffe2d62bef [优化]添加新翻译第零章 2024-09-05 12:53:35 +08:00
88db7fbc28 使用md格式重写翻译 2024-09-03 14:48:19 +08:00
b8430a39b5 [更新] 添加md格式文档 2024-08-13 09:24:26 +08:00
c23e68ba24 [更新] 修复pdf生成 2024-08-12 18:02:09 +08:00
2155f68a8d [汉化] 翻译第6章节 2024-08-05 17:31:33 +08:00
b03a709553 [汉化] 翻译1-5章节 2024-08-02 18:05:24 +08:00
7ef351b791 [汉化] 汉化项目说明 2024-07-30 09:56:28 +08:00
111 changed files with 4175 additions and 2612 deletions

View File

@ -1,30 +0,0 @@
Jian-Xing Wu <fdgkhdkgh@gmail.com> 吳建興
Meng-Zong Tsai <hwahwa649@gmail.com> fennecJ <58484289+fennecJ@users.noreply.github.com>
Meng-Zong Tsai <hwahwa649@gmail.com> fennecJ <hwahwa649@gmail.com>
Jim Huang <jserv.tw@gmail.com> Jim Huang <jserv@ccns.ncku.edu.tw>
Jim Huang <jserv.tw@gmail.com> Jim Huang <jserv@biilabs.io>
Chih-En Lin <shiyn.lin@gmail.com> linD026 <shiyn.lin@gmail.com>
Chih-En Lin <shiyn.lin@gmail.com> linD026 <66012716+linD026@users.noreply.github.com>
Chih-En Lin <shiyn.lin@gmail.com> linD026 <0086d026@email.ntou.edu.tw>
Chih-En Lin <shiyn.lin@gmail.com> linzhien <0086d026@email.ntou.edu.tw>
mengxinayan <31788564+mengxinayan@users.noreply.github.com> 萌新阿岩
Ethan Chan <F04066028@gs.ncku.edu.tw> tzuyichan
Peter Lin <peterlin@qilai.dev> lyctw <lyctw.ee@gmail.com>
Peter Lin <peterlin@qilai.dev> Peter Lin <peterlin.tw@protonmail.com>
Che-Chia Chang <vivahavey@gmail.com> gagachang
Shao-Tse Hung <ccs100203@gmail.com> ccs100203
Yi-Wei Lin <s921975628@gmail.com> RinHizakura
Chih-Hsuan Yang <zxc25077667@gmail.com> 25077667
Yin-Chiuan Chen <leovincentseles@gmail.com> leovincentseles
Xatierlike Lee <xatierlike@gmail.com> xatier
Chin Yik Ming <yikming2222@gmail.com> ChinYikMing
Tse-Wei Lin <20110901eric@outlook.com> 2011eric
Yu-Hsiang Tseng <asas1asas200@gmail.com> asas1asas200
Kuan-Wei Chiu <visitorckw@gmail.com> visitorckw
I-Hsin Cheng <richard120310@gmail.com> vax-r
Wei-Hsin Yeh <weihsinyeh168@gmail.com> weihsinyeh <weihsinyeh168@gmail.com>
Wei-Hsin Yeh <weihsinyeh168@gmail.com> weihsinyeh <90430653+weihsinyeh@users.noreply.github.com>
Cheng-Shian Yeh <yehchanshen@gmail.com> yeh-sudo
Yo-Jung Lin <0xff07@gmail.com> <leo.lin@canonical.com> 0xff07
YYGO <srayuws@users.noreply.github.com> srayuws
Yen-Yu Chen <matt162162162@gmail.com> <69316865+YLowy@users.noreply.github.com> Ylowy

View File

@ -1,24 +0,0 @@
PROJ = lkmpg
all: $(PROJ).pdf
$(PROJ).pdf: lkmpg.tex
@if ! hash latexmk; then echo "No Latexmk found. See https://mg.readthedocs.io/latexmk.html for installation."; exit 1; fi
rm -rf _minted-$(PROJ)
latexmk -shell-escape lkmpg.tex -pdf
html: lkmpg.tex html.cfg assets/Manrope_variable.ttf
sed $ 's/\t/ /g' lkmpg.tex > lkmpg-for-ht.tex
make4ht --shell-escape --utf8 --format html5 --config html.cfg --output-dir html lkmpg-for-ht.tex "fn-in"
ln -sf lkmpg-for-ht.html html/index.html
cp assets/Manrope_variable.ttf html/Manrope_variable.ttf
rm -f lkmpg-for-ht.tex lkmpg-for-ht.xref lkmpg-for-ht.tmp lkmpg-for-ht.html lkmpg-for-ht.css lkmpg-for-ht.4ct lkmpg-for-ht.4tc lkmpg-for-ht.dvi lkmpg-for-ht.lg lkmpg-for-ht.idv lkmpg*.svg lkmpg-for-ht.log lkmpg-for-ht.aux
rm -rf _minted-$(PROJ) _minted-lkmpg-for-ht
indent:
(cd examples; find . -name '*.[ch]' | xargs clang-format -i)
clean:
rm -f *.dvi *.aux *.log *.ps *.pdf *.out lkmpg.bbl lkmpg.blg lkmpg.lof lkmpg.toc lkmpg.fdb_latexmk lkmpg.fls
rm -rf html
.PHONY: html

View File

@ -1,73 +1,27 @@
# The Linux Kernel Module Programming Guide
# Linux 内核模块编程指南
This project keeps the Linux Kernel Module Programming Guide up to date, with [working examples](examples/) for recent 5.x and 6.x kernel versions.
The guide has been around since 2001 and most copies of it on the web only describe old 2.6.x kernels.
本项目可以看作为[lkmpg](https://github.com/sysprog21/lkmpg)项目的中文译本, 但并添加了部分译者注, 需要注意的是此项目和原版在使用的中央处理器架构上有所不同, 原版是使用的主流X86架构, 本版本会将部分涉及到架构相关的内容修改为龙架构,目前本译本已支持到 6.9.x 版本的内核, 相关[示例](示例/)。
The book can be freely accessed via https://sysprog21.github.io/lkmpg/ or [latest PDF file](https://github.com/sysprog21/lkmpg/releases).
The original guide may be found at [Linux Documentation Project](http://www.tldp.org/LDP/lkmpg/).
You may check other [freely available programming books](https://ebookfoundation.github.io/free-programming-books-search/) listed by The [Free Ebook Foundation](https://ebookfoundation.org/) or [Linux online books](https://onlinebooks.library.upenn.edu/webbin/book/browse?type=lcsubc&key=Linux) collected by [The Online Books Page](https://onlinebooks.library.upenn.edu/).
原作者与本书的一些介绍和致谢说明请看[引言](./引言.md)
## Getting Started
本书是由寻觅(樊旭东)翻译为中文
### Summary
1. Get the latest source code from the [GitHub page](https://github.com/sysprog21/lkmpg).
2. Install the prerequisites.
3. Generate PDF and/or HTML documents.
本项目原版是使用TeXLie格式编写, 在译者对此格式较为陌生, 已改为更为主流的 `markdown` 格式重写
### Step 1: Get the latest source code
## 开始使用
Make sure you can run `git` with an Internet connection.
在线文档: http://223.76.216.188:50201/books/linux/chapter/6c4b7
```shell
$ git clone https://github.com/sysprog21/lkmpg.git && cd lkmpg
```
> 目录:
> * [0 准备工作](文档/00-准备工作.md)
> * [1 编写驱动](文档/01-编写驱动.md)
> * [2 驱动相关文件](文档/02-驱动相关文件.md)
> * [3 ioctl](文档/03-ioctl.md)
> * [4 系统调用](文档/04-系统调用.md)
> * [5 阻塞进程和线程](文档/05-阻塞进程和线程.md)
> * [6 内核模块的锁](文档/06-内核模块的锁.md)
> * [7 驱动与用户交互](文档/07-驱动与用户交互.md)
> * [8 调度与中断](文档/08-调度与中断.md)
> * [9 设备驱动](文档/09-设备驱动.md)
> * [10 优化与常见问题](文档/10-优化与常见问题.md)
### Step 2: Install the prerequisites
To generate the book from source, [TeXLive](https://www.tug.org/texlive/) ([MacTeX](https://www.tug.org/mactex/)) is required.
For Ubuntu Linux, macOS, and other Unix-like systems, run the following command(s):
```bash
# Debian / Ubuntu
$ sudo apt install make texlive-full
# Arch / Manjaro
$ sudo pacman -S make texlive-binextra texlive-bin
# macOS
$ brew install mactex
$ sudo tlmgr update --self
```
Note that `latexmk` is required to generated PDF, and it probably has been installed on your OS already. If not, please follow the [installation guide](https://mg.readthedocs.io/latexmk.html#installation).
In macOS systems, package `Pygments` may not be pre-installed. If not, please refer to the [installation guide](https://pygments.org/download/) before generate documents.
Alternatively, using [Docker](https://docs.docker.com/) is recommended, as it guarantees the same dependencies with our GitHub Actions workflow.
After install [docker engine](https://docs.docker.com/engine/install/) on your machine, pull the docker image [twtug/lkmpg](https://hub.docker.com/r/twtug/lkmpg) and run in isolated containers.
```shell
# pull docker image and run it as container
$ docker pull twtug/lkmpg
$ docker run --rm -it -v $(pwd):/workdir twtug/lkmpg
```
[nerdctl](https://github.com/containerd/nerdctl) is a Docker-compatible command line tool for [containerd](https://containerd.io/), and you can replace the above `docker` commands with `nerdctl` counterparts.
### Step 3: Generate PDF and/or HTML documents
Now we could build document with following commands:
```bash
$ make all # Generate PDF document
$ make html # Convert TeX to HTML
$ make clean # Delete generated files
```
## License
The Linux Kernel Module Programming Guide is a free book; you may reproduce and/or modify it under the terms of the [Open Software License](https://opensource.org/licenses/OSL-3.0).
Use of this work is governed by a copyleft license that can be found in the `LICENSE` file.
The complementary sample code is licensed under GNU GPL version 2, as same as Linux kernel.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

View File

@ -1,59 +0,0 @@
Amit Dhingra, % <mechanicalamit@gmail.com>
Andy Shevchenko, % <andriy.shevchenko@linux.intel.com>
Arush Sharma, % <46960231+arushsharma24@users.noreply.github.com>
Benno Bielmeier, % <32938211+bbenno@users.noreply.github.com>
Bob Lee, % <defru04002@gmail.com>
Brad Baker, % <brad@brdbkr.com>
Che-Chia Chang, % <vivahavey@gmail.com>
Cheng-Shian Yeh, % <yehchanshen@gmail.com>
Chih-En Lin, % <shiyn.lin@gmail.com>
Chih-Hsuan Yang, % <zxc25077667@gmail.com>
Chih-Yu Chen, % <34228283+chihyu1206@users.noreply.github.com>
Ching-Hua (Vivian) Lin, % <jkrvivian@gmail.com>
Chin Yik Ming, % <yikming2222@gmail.com>
cvvletter, % <cvvletter@pm.me>
Cyril Brulebois, % <cyril@debamax.com>
Daniele Paolo Scarpazza, % <>
David Porter, % <>
demonsome, % <horseradish1208@gmail.com>
Dimo Velev, % <>
Ekang Monyet, % <ekangmonyet@posteo.net>
Ethan Chan, % <F04066028@gs.ncku.edu.tw>
Francois Audeon, % <>
Gilad Reti, % <gilad.reti@gmail.com>
heartofrain, % <heartofrain@outlook.com>
Horst Schirmeier, % <>
Hsin-Hsiang Peng, % <hsinspeng@gmail.com>
Ignacio Martin, % <>
I-Hsin Cheng, % <richard120310@gmail.com>
Iûnn Kiàn-îng, % <black.yangcr@gmail.com>
Jian-Xing Wu, % <fdgkhdkgh@gmail.com>
Johan Calle, % <43998967+jcallemc@users.noreply.github.com>
keytouch, % <qsytech@126.com>
Kohei Otsuka, % <13173186+rjhcnf@users.noreply.github.com>
Kuan-Wei Chiu, % <visitorckw@gmail.com>
manbing, % <manbing3@gmail.com>
Marconi Jiang, % <marconi1964@yahoo.com>
mengxinayan, % <31788564+mengxinayan@users.noreply.github.com>
Meng-Zong Tsai, % <hwahwa649@gmail.com>
Peter Lin, % <peterlin@qilai.dev>
Roman Lakeev, % <>
Sam Erickson, % <samuelerickson977@gmail.com>
Shao-Tse Hung, % <ccs100203@gmail.com>
Shih-Sheng Yang, % <james1qaz2wsx12qw@gmail.com>
Stacy Prowell, % <sprowell@gmail.com>
Steven Lung, % <1030steven@gmail.com>
Tristan Lelong, % <tristan.lelong@blunderer.org>
Tse-Wei Lin, % <20110901eric@outlook.com>
Tucker Polomik, % <tucker.polomik@inficon.com>
Tyler Fanelli, % <tfanelli@redhat.com>
VxTeemo, % <tcccvvv123@gmail.com>
Wei-Hsin Yeh, % <weihsinyeh168@gmail.com>
Wei-Lun Tsai, % <alan23273850@gmail.com>
Xatierlike Lee, % <xatierlike@gmail.com>
Yen-Yu Chen, % <matt162162162@gmail.com>
Yin-Chiuan Chen, % <leovincentseles@gmail.com>
Yi-Wei Lin, % <s921975628@gmail.com>
Yo-Jung Lin, % <0xff07@gmail.com>
Yu-Hsiang Tseng, % <asas1asas200@gmail.com>
YYGO. % <srayuws@users.noreply.github.com>

View File

@ -1,91 +0,0 @@
\Preamble{xhtml}
\Configure{tableofcontents*}{chapter,section,subsection}
\Css{html {
width: 100vw;
overflow-x: hidden;
}}
\Css{@font-face {
font-family: Manrope;
src: url(Manrope_variable.ttf);
}}
\Css{body {
max-width: 55rem;
box-sizing: border-box;
padding: 1rem;
margin: 0 auto;
overflow-x: hidden;
background-color: \#F4ECD8;
color: \#5B464B;
line-height: 1.5;
}}
\Css{a {
color: \#0060DF;
}}
\Css{p, a {
font-size: 1.2rem;
font-family: Manrope;
}}
\Css{p + pre {
font-size: 1.1em;
}}
\Css{div.author {
white-space: normal;
}}
\Css{img.math {
height: 1rem;
vertical-align: top;
}}
\Css{pre.fancyvrb, {
white-space: pre;
}}
\Css{figure, .fancyvrb, .verbatim {
margin-inline: 0;
overflow-x: auto;
}}
\Css{.ecrm-0500 {
font-size: 70\%;
font-style: italic;
color: gray;
width: 1.5rem;
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}}
\Css{.flushright:first-child {
position:absolute;
top: 10px;
right: 50px;
}}
\Css{.right {
text-align: right;
}}
\AtBeginDocument{%
\Configure{@HEAD}{\HCode{
<script async defer src="https://buttons.github.io/buttons.js"></script>
<div class="right">
<a class="github-button" href="https://github.com/sysprog21/lkmpg" data-size="large" aria-label="View on GitHub">View on GitHub</a>
<a class="github-button" href="https://github.com/sysprog21/lkmpg/releases/download/latest/lkmpg.pdf" data-icon="octicon-download" data-size="large" aria-label="Download PDF document">Download PDF document</a>
<br><br>
</div>
\Hnewline}}
}
\begin{document}
\EndPreamble

View File

@ -1,37 +0,0 @@
\newminted[code]{c}{frame=single,
framesep=2mm,
baselinestretch=1,
fontsize=\footnotesize,
breaklines,
breakafter=d,
linenos
}
\usemintedstyle{vs}
\NewDocumentCommand{\samplec}{oom}{%
\IfNoValueTF{#1}%
{%
\inputminted[frame=single, framesep=2mm, baselinestretch=1, fontsize=\footnotesize, breaklines, breakafter=d, linenos]{c}{#3}%
}%
{%
\IfNoValueTF{#2}%
{%
\inputminted[frame=single, framesep=2mm, baselinestretch=1, fontsize=\footnotesize, breaklines, breakafter=d, firstline=#1, linenos]{c}{#3}%
}%
{%
\inputminted[frame=single, framesep=2mm, baselinestretch=1, fontsize=\footnotesize, breaklines, breakafter=d, firstline=#1, lastline=#2, linenos]{c}{#3}%
}%
}%
}
\newminted[codebash]{bash}{frame=single,
framesep=2mm,
baselinestretch=1.2,
breaklines,
breakafter=d,
linenos
}
\newmintinline[sh]{bash}{}
\newmintinline[cpp]{c}{}

View File

@ -1,2 +0,0 @@
\newcommand*{\src}[2][]{\href{https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/#2}%
{\ifthenelse{\equal{#1}{}}{#2}{#1}}}

2176
lkmpg.tex

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +0,0 @@
Amit Dhingra,<mechanicalamit@gmail.com>
Andy Shevchenko,<andriy.shevchenko@linux.intel.com>
Arush Sharma,<46960231+arushsharma24@users.noreply.github.com>
Benno Bielmeier,<32938211+bbenno@users.noreply.github.com>
Bob Lee,<defru04002@gmail.com>
Brad Baker,<brad@brdbkr.com>
Che-Chia Chang,<vivahavey@gmail.com>
Cheng-Shian Yeh,<yehchanshen@gmail.com>
Chih-En Lin,<shiyn.lin@gmail.com>,<0086d026@email.ntou.edu.tw>,<66012716+linD026@users.noreply.github.com>
Chih-Hsuan Yang,<zxc25077667@gmail.com>
Chih-Yu Chen,<34228283+chihyu1206@users.noreply.github.com>
Ching-Hua (Vivian) Lin,<jkrvivian@gmail.com>
Chin Yik Ming,<yikming2222@gmail.com>
cvvletter,<cvvletter@pm.me>
Cyril Brulebois,<cyril@debamax.com>
Daniele Paolo Scarpazza,<>
David Porter,<>
demonsome,<horseradish1208@gmail.com>
Dimo Velev,<>
Ekang Monyet,<ekangmonyet@posteo.net>
Ethan Chan,<F04066028@gs.ncku.edu.tw>
Francois Audeon,<>
Gilad Reti,<gilad.reti@gmail.com>
heartofrain,<heartofrain@outlook.com>
Horst Schirmeier,<>
Hsin-Hsiang Peng,<hsinspeng@gmail.com>
Ignacio Martin,<>
I-Hsin Cheng,<richard120310@gmail.com>
Iûnn Kiàn-îng,<black.yangcr@gmail.com>
Jian-Xing Wu,<fdgkhdkgh@gmail.com>
Johan Calle,<43998967+jcallemc@users.noreply.github.com>
keytouch,<qsytech@126.com>
Kohei Otsuka,<13173186+rjhcnf@users.noreply.github.com>
Kuan-Wei Chiu,<visitorckw@gmail.com>
manbing,<manbing3@gmail.com>
Marconi Jiang,<marconi1964@yahoo.com>
mengxinayan,<31788564+mengxinayan@users.noreply.github.com>
Meng-Zong Tsai,<hwahwa649@gmail.com>,<58484289+fennecJ@users.noreply.github.com>
Peter Lin,<peterlin@qilai.dev>,<lyctw.ee@gmail.com>,<peterlin.tw@protonmail.com>
Roman Lakeev,<>
Sam Erickson,<samuelerickson977@gmail.com>
Shao-Tse Hung,<ccs100203@gmail.com>
Shih-Sheng Yang,<james1qaz2wsx12qw@gmail.com>
Stacy Prowell,<sprowell@gmail.com>
Steven Lung,<1030steven@gmail.com>
Tristan Lelong,<tristan.lelong@blunderer.org>
Tse-Wei Lin,<20110901eric@outlook.com>
Tucker Polomik,<tucker.polomik@inficon.com>
Tyler Fanelli,<tfanelli@redhat.com>
VxTeemo,<tcccvvv123@gmail.com>
Wei-Hsin Yeh,<weihsinyeh168@gmail.com>,<90430653+weihsinyeh@users.noreply.github.com>
Wei-Lun Tsai,<alan23273850@gmail.com>
Xatierlike Lee,<xatierlike@gmail.com>
Yen-Yu Chen,<matt162162162@gmail.com>,<69316865+YLowy@users.noreply.github.com>
Yin-Chiuan Chen,<leovincentseles@gmail.com>
Yi-Wei Lin,<s921975628@gmail.com>
Yo-Jung Lin,<0xff07@gmail.com>,<leo.lin@canonical.com>
Yu-Hsiang Tseng,<asas1asas200@gmail.com>
YYGO,<srayuws@users.noreply.github.com>

View File

@ -1 +0,0 @@
Jim Huang, One of main author

View File

@ -1,8 +0,0 @@
Daniele Paolo Scarpazza,<>
David Porter,<>
Dimo Velev,<>
Francois Audeon,<>
Horst Schirmeier,<>
Ignacio Martin,<>
Roman Lakeev,<>
Tristan Lelong,<tristan.lelong@blunderer.org>

View File

@ -1,60 +0,0 @@
#!/usr/bin/env bash
FORMAT="%aN,<%aE>" #Set git output format in "Name,<Email>" //Capital in aN and aE means replace str based on .mailmap
TARGET=(examples lkmpg.tex) #Target files we want to trace
DIR=`git rev-parse --show-toplevel` #Get root dir of the repo
TARGET=("${TARGET[@]/#/$DIR/}") #Concat $DIR BEFORE ALL elements in array TARGET
#The str in each line should be Username,<useremail>
function gen-raw-list()
{
git log --pretty="$FORMAT" ${TARGET[@]} | sort -u
}
function parse-list()
{
> Contributors # Clear contributors' list (Overwrite with null)
while read -r line; do
User=`echo "$line" | awk -F "," '{print $1}'`
if [[ `grep -w "$User" Exclude` ]]; then
echo "[skip] $User"
continue;
fi
echo "[Add] $User"
MainMail=`echo "$line" | awk -F "[<*>]" '{print $2}'`
Emails=(`cat $DIR/.mailmap | grep -w "$User" | awk -F "[<*>]" '{print $4}' | sort -u`)
for Email in ${Emails[@]}; do
if [[ "$Email" != "$MainMail" ]]; then
line="$line,<$Email>";
fi
done
echo "$line" >> Contributors
done <<< $(gen-raw-list)
cat Include >> Contributors
}
function sort-list()
{
if [[ `git diff Contributors` ]]; then
sort -f -o Contributors{,}
git add $DIR/scripts/Contributors
fi
}
#For all lines before endline, print "name, % <email>"
#For endline print "name. % <email>"
function gen-tex-file()
{
cat Contributors | awk -F "," \
' BEGIN{k=0}{name[k]=$1;email[k++]=$2}
END{
for(i=0;i<k;i++){
name[i]=(i<k-1)?name[i]",":name[i]".";
printf("%-30s %% %s\n",name[i],email[i]);
}
}
'
}
parse-list
sort-list
gen-tex-file > $DIR/contrib.tex

29
引言.md Normal file
View File

@ -0,0 +1,29 @@
# 引言
《Linux内核模块编程指南》是一本免费的书;你可以根据 [开放软件许可证(Open Software License)](https://opensource.org/licenses/OSL-3.0) 3.0版的条款复制和/或修改它。
本书的发行是希望它会有用, 但没有任何担保, 甚至没有对适销性或特定用途的适用性的暗示担保。
作者鼓励广泛分发本书供个人或商业使用, 前提是上述版权声明保持不变, 并且该方法符合[开放软件许可证(Open Software License)](https://opensource.org/licenses/OSL-3.0)
的规定。总之, 你可以免费复制和分发本书, 也可以赢利。以任何媒介、物理或电子形式复制本书, 无需获得作者的明确许可。
本文档的衍生作品和翻译必须置于开放软件许可证下, 并且原始版权声明必须保持不变。如果你为本书贡献了新材料, 则必须为你的修订提供材料和源代码。请直接向文档维护者Jim Huang提供修订和更新<jserv@ccns.ncku.edu.tw>
这将允许合并更新, 并为Linux社区提供一致的修订。
如果你以商业方式出版或分发本书, 作者和[Linux Documentation Project](https://tldp.org/) (LDP)将非常感谢你的捐赠、版税和/或印刷本。以这种方式贡献表明你支持自由软件和LDP。如果你有任何问题或意见, 请联系上述地址。
# 原作者介绍
《Linux内核模块编程指南》最初由Ori Pomerantz for Linux v2.2编写。随着Linux内核的发展, Ori维护文档的可用性降低了。因此, Peter Jay Salzman承担了维护者的角色, 并更新了Linuxv2.4指南。在跟踪Linux v2.6中的开发时, Peter也受到了类似的限制, 导致Michael Burian加入为共同维护者, 以使该指南与Linux v2.6同步。Bob Mottram通过更新Linux v3.8和更高版本的示例对该指南做出了贡献。Jim Huang随后承担了更新最新Linux版本(v5.0及更高版本)指南的任务, 并修订LaTeX文档。
# 致谢
以下人员为本书提供了一些更正与良好的建议:
Amit Dhingra, Andy Shevchenko, Arush Sharma, Benno Bielmeier, Bob Lee, Brad Baker, Che-Chia Chang, Cheng-Shian Yeh, Chih-En Lin, Chih-Hsuan
Yang, Chih-Yu Chen, Ching-Hua (Vivian) Lin, Chin Yik Ming, cvvletter, Cyril Brulebois, Daniele Paolo Scarpazza, David Porter, demonsome, Dimo
Velev, Ekang Monyet, Ethan Chan, Francois Audeon, Gilad Reti, heartofrain, Horst Schirmeier, Hsin-Hsiang Peng, Ignacio Martin, I-Hsin
Cheng, Iûnn Kiàn-îng, Jian-Xing Wu, Johan Calle, keytouch, Kohei Otsuka, Kuan-Wei Chiu, manbing, Marconi Jiang, mengxinayan, Meng-Zong Tsai,
Peter Lin, Roman Lakeev, Sam Erickson, Shao-Tse Hung, Shih-Sheng Yang, Stacy Prowell, Steven Lung, Tristan Lelong, Tse-Wei Lin, Tucker Polomik,
Tyler Fanelli, VxTeemo, Wei-Hsin Yeh, Wei-Lun Tsai, Xatierlike Lee, Yen-Yu Chen, Yin-Chiuan Chen, Yi-Wei Lin, Yo-Jung Lin, Yu-Hsiang Tseng, YYGO.

769
文档/00-准备工作.md Normal file
View File

@ -0,0 +1,769 @@
## 准备开始
### 什么是内核模块?
参与Linux内核模块开发需要扎实的C编程语言基础和创建用于进程执行的传统程序的丰富经验。这一追求涉及到一个领域, 如果处理不当, 一个未受限制的指针可能会导致整个文件系统的崩溃, 导致系统必须重启。
Linux 内核模块的确切定义是: 能够根据需要在内核中动态加载和卸载的代码段。
这些模块增强了内核功能, 而无需重新启动系统。
一个值得注意的例子是设备驱动程序模块, 它促进了内核与链接到系统的硬件组件的交互。
在没有模块的情况下, 主流的方法倾向于使用单内核( Monolithic Kernel 也可以称之为宏内核 Macrokernel ), 需要将新功能直接集成到内核镜像中。这种方法会导致内核变得更大, 并在需要新增功能时, 必须重新构建内核并进行系统重启。
> 译者注: 单内核( Monolithic Kernel) 有时候也被称之为宏内核(Macrokernel), 与其对应的另一种内核形式是微内核, Linux内核是微内核和内核的混合产物
### 内核模块包
Linux发行版在一个包中提供了命令 `modprobe`, `insmod``depmod`
在安同上:
```bash
sudo oma install gcc kmod
```
在 Debian 与其衍生发行版(乌班图ubuntu, 深度deebin, kali)上:
```bash
sudo apt-get install build-essential kmod
```
在 Arch Linux系上:
```bash
sudo pacman -S gcc kmod
```
### 我的内核中有哪些模块?
要发现当前内核中已经加载了哪些模块可以使用命令 `sudo lsmod`
模块存储在文件 `/proc/modules`中, 可以使用`sudo cat /proc/modules`命令查看它们
这可能是一个很长的列表, 可以使用grep命令搜索某类模块, 比如使用`sudo lsmod | grep net`命令搜索网络模块
### 是否需要下载和编译内核?
本指南并没有对此有强制要求。但建议本指南的示例使用虚拟机运行, 从而减少可能对系统造成的任何风险
### 开始前
在深入研究代码之前, 需要注意某些事项。每个人的主机都有所不同, 所以在不同的内核与发行版之间都可能存在一些差异, 成功编译和加载首个 "hello world" 程序有时可能会是一个挑战。 克服最初的障碍是一个令人欣慰的过程, 它会为你后续的努力铺平道路。
1. `Modversioning`: 如果引导不同的内核, 则为一个内核编译的模块将不会加载, 除非在内核中启用了 `CONFIG_MODVERSIONS` 本指南稍后将讨论模块版本化问题。在讨论模块版本控制之前, 如果在启用了 `modversioning` 的情况下运行内核, 则本指南中的示例可能无法正常工作。然而, 大多数Linux发行版内核都启用了 `modversioning` 。如果由于版本控制错误而在加载模块时出现问题, 请考虑在关闭 `modversioning` 的情况下编译内核。
2. 使用 `X Window System`: 强烈建议使用控制台(终端)中获取、编译、运行本指南中讨论的所有示例, 不建议使用可视化窗口运行, 模块不能像 `printf()` 那样直接打印到屏幕上, 但它们可以记录最终显示在屏幕上的信息和警告, 特别是在控制台中。如果从 `xTER` 加载模块, 则信息和警告将被记录在`systemd`日志中。只有查询 `journalctl` 才能看到这些日志。 有关详细信息, 请看 [Hello World](#bkmrk-hello-world) 。为了即时访问此信息, 建议从控制台执行所有任务。
3. SecureBoot: 许多现代计算机都预先配置了`UEFI SecureBoot`, 这是一个基本的安全标准, 确保仅通过原始设备制造商认可的可信软件引导。某些Linux发行版甚至附带配置为支持SecureBoot的默认Linux内核。在这些情况下, 内核模块需要一个签名的安全密钥。如果在启动"hello world"模块时出现错误信息: `ERROR: could not insert module (错误:无法插入模块)`。如果此消息 `Lockdown: insmod: unsigned module loading is restricted; see man kernel lockdown.7(未签名的模块的装载限制, 请参阅 man kernel lockdown.7 )` 出现在 `dmesg` 输出, 最简单的方法是从主机的引导菜单中禁用UEFI SecureBoot。当然, 另一种方法涉及复杂的过程, 如生成密钥、系统密钥安装和模块签名, 以实现功能。然而, 这种复杂的过程不太适合初学者。如果有兴趣, 可以探索和遵循[SecureBoot](https://wiki.debian.org/SecureBoot)的更详细步骤。
> 译者注: 编译时需要调用内核中的`objtool`模块, 但并不是所有发行版的内核都包含此模块, 可以使用`file /lib/modules/$(uname -r)/build/tools/objtool/objtool`查看当前发行版内核中是否存在此模块, 如果为找到此模块可以尝试替换其他内核或者使用其他发行版
### 准备头文件
在构建任何东西之前, 必须安装内核的头文件。对于几乎所有的发行版都可以使用 `ls /lib/modules/$(uname -r)/build` 命令来确定是否已有内核头文件, 返回不为空则代表存在相关内核头文件, 其他情况需要从包管理器中手动下载
内核头文件的包名在Debian系、Arch系中名称一般是`linux-headers linux-headers-内核版本号-备注(如: linux-headers-5.4.0-80-generic)`, 而在红帽系中一般名称是 `kernel-devel kernel-headers`
```
# Ubuntu/Debian
sudo apt install linux-headers-$(uname -r)
# Arch Linux:
sudo pacman -S linux-headers
# Fedora:
sudo dnf install kernel-devel kernel-headers
```
### 示例
本文档中的所有示例都位于 [示例(examples)](./examples) 子目录中。
如果发生编译错误, 可能是由于正在使用较新的内核版本, 或者可能需要安装相应的内核头文件。
## Hello World
### 最简单的模块
大多数人开始他们的编程旅程通常从 `hello world` 示例的某种变体开始。偏离这一传统的人目前尚未可知会产生什么结果, 但坚持这一传统似乎是明智的。 学习过程将从一系列 `hello world` 程序开始, 这些程序说明了编写内核模块的各个基本方面。
接下来介绍的可能是最简单的模块。
创建测试目录:
```
mkdir -p /develop/kernel/hello-1
cd /develop/kernel/hello-1
```
将下述代码粘贴到你喜爱的编辑器中, 并将其命名为 `hello-1.c`:
```c
/*
* hello-1.c - 最简单的内核模块
*/
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
int init_module(void)
{
pr_info("Hello world 1.\n");
/* 非0返回意味着init_module失败; 无法加载模块 */
return 0;
}
void cleanup_module(void)
{
pr_info("Goodbye world 1.\n");
}
MODULE_LICENSE("GPL");
```
现在你需要一个 `Makefile` 。如果复制并粘贴此内容, 需要注意的是复制后下述代码中的`制表符(tabs)`可能会变成空格, 这里我们需要使用制表符。
```Makefile
obj-m += hello-1.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
```
`Makefile` 中, `$(CURDIR)` 可以设置为当前工作目录的绝对路径名(在处理所有 `-C` 选项后, 如果有)。请参阅 [GNU makemanual](https://www.gnu.org/software/make/manual/make.html) 中有关 `CURDIR` 的更多信息。
在完成上述操作后, 之后直接运行`make`即可
如果在Makefile中没有`PWD := $(CURDIR)`这样的语句, 那么使用`sudo make`可能无法正确编译。这是因为一些环境变量受到安全策略的限制, 不能被继承。默认的安全策略是`sudoers`。在`sudoers`安全策略中, 默认启用了`env_reset`, 这会限制环境变量的使用。具体来说, 路径变量不会从用户环境中保留, 而是被设置为默认值(更多信息请参考[sudoers 手册](https://www.sudo.ws/docs/man/sudoers.man/))。你可以通过`sudo -s``sudo -V` 方式查看环境变量的设置
下面以一个简单的 Makefile 为例, 演示上述问题。
```Makefile
all:
echo $(PWD)
```
我们可以使用`-p`标志从Makefile中打印出环境变量值。
```shell
$ make -p | grep PWD
PWD = /home/ubuntu/temp
OLDPWD = /home/ubuntu
echo $(PWD)
```
`PWD``sudo`不会被一起继承。
```shell
$ sudo make -p | grep PWD
echo $(PWD)
```
有三种方法可以解决这个问题。
1. 可以使用`-E`标志临时保留它们。
> ```shell
> $ sudo -E make -p | grep PWD
> PWD = /home/ubuntu/temp
> OLDPWD = /home/ubuntu
> echo $(PWD)
> ```
2. 可以通过执行`visudo`命令, 编辑`/etc/sudoers`来禁用`env_reset`(环境重置)。
> 1. `vim /etc/sudoers`
> 2. 在其中找到 `Defaults env_reset`
> 3. 将`env_reset`更改为`!env_reset`, 其他环境变量不做更改
> 分别执行env和sudo env
> ```shell
> # 禁用 env_reset
> echo "user:" > non-env_reset.log; env >> non-env_reset.log
> echo "root:" >> non-env_reset.log; sudo env >> non-env_reset.log
> # 启用 env_reset
> echo "user:" > env_reset.log; env >> env_reset.log
> echo "root:" >> env_reset.log; sudo env >> env_reset.log
> ```
> 可以查看日志(`cat non-env_reset.log`和`cat env_reset.log`)发现`env_reset`和`!env_reset`的差异
3. 可以通过将环境变量附加到/etc/sudoers中的env_keep来保留环境变量
> `Defaults env_keep += "PWD"`
> 应用上述更改后, 可以通过`sudo -s`或`sudo -V`的方式检查环境变量设置:
如果一切顺利, 你应该会发现你有一个已编译的hello-1.ko模块。你可以使用以下命令查找有关它的信息:
```shell
sudo modinfo hello-1.ko
```
使用 `sudo lsmod | grep hello` 命令应该不返回任何内容。使用下述的命令来尝试加载新模块:
```shell
sudo insmod hello-1.ko
```
载入模块时中划线(hello-1)将会被替换成下划线(hello_1), 再次尝试时`sudo lsmod | grep hello`
就可以看到已加载的模块。可以使用以下命令再次移除它(请注意, 中划线已被下划线替换):
```shell
sudo rmmod hello_1
```
要查看日志中刚刚发生的情况, 请执行以下操作:
```shell
sudo journalctl --since "1 hour ago" | grep kernel
```
[![](http://223.76.216.188:50201/uploads/images/gallery/2024-08/scaled-1680-/image-1723541087064.png)](http://223.76.216.188:50201/uploads/images/gallery/2024-08/image-1723541087064.png)
现在你已经了解了创建、编译、安装和删除模块的基础知识。下面将会更多地描述该模块的工作原理。
内核模块必须至少有两个函数: 一个名为`init_module()`**"init"(初始化)** 函数, 当模块被插入内核时调用;另一个名为主模块的 **"cleanup"(清理)** 函数, 在将其从内核中删除之前调用。实际上, 从内核2.3.13开始, 情况发生了变化。现在, 你可以为模块的开始和结束函数使用任何名称, 你将在第4.2节中学习如何做到这一点。事实上, 新方法是首选方法。然而, 许多人仍然使用`init_module()``cleanup_module()`作为他们的开始和结束函数。
通常,`init_module()`要么向内核注册某个对象的处理程序, 要么用自己的代码替换其中一个内核函数(通常是执行某些操作的代码, 然后调用原始函数)。`cleanup_module()`函数的作用是撤消`init_module()`所做的任何操作,因此可以安全地卸载该模块。
每个内核模块都需要包含`<linux/module.h>`。但只有在需要输出日志时(`pr_alert()`)包含`<linux/printk.h>`宏扩展即可, 你将在 [载入与卸载](#bkmrk-载入与卸载) 中了解这一点。
#### 关于编码风格
对任何开始内核编程的人来说都不容易被注意到的事情是, 代码中的缩进应该使用\textbf{制表符(tabs)}, 而不是空格。它是内核的编码约定之一。你可能不喜欢它, 但如果你想要向上游提交补丁, 则需要习惯它。
> 译者注: 内核中的注释无论是单行还是多行都习惯使用 /**/ 的方式而非 //
#### 打印(print)宏
早期打印日志通常使用的是`printk`函数,`printk`函数需要写优先级, 如`KERN_INFO` `KERN_DEBUG`。在较新的内核也可以使用一组打印宏, 如`pr_info` `pr_debug` 以缩写形式表示。省去了一些无意义的输入, 看起来更加整洁。有关`printk`的头文件可以在[include/linux/printk.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/printk.h)中找到。务必花时间阅读一下这些宏(参考说明[中文版 printk 手册](https://www.kernel.org/doc/html/latest/translations/zh_CN/core-api/printk-basics.html})/[英文版 printk 手册](https://www.kernel.org/doc/html/latest/core-api/printk-basics.html)。
> 译者注: 根据测试在具有中文环境的系统中打印宏是可以正常输出中文的
#### 关于编译
内核模块的编译需要与常规用户空间应用程序稍有不同。以前的内核版本要求我们非常关心这些设置, 这些设置通常存储在Makefiles中。尽管分层组织, 但许多冗余设置积累在子级Makefiles中, 使它们变得很大, 很难维护。幸运的是, 有一种新的方法来完成这些事情, 称为kbuild, 并且外部可加载模块的构建过程现在完全集成到标准内核构建机制中。要了解有关如何编译不属于官方内核的模块(如本指南中的所有示例)的更多信息, 请参阅文件[Documentation/kbuild/modules.rst](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/Documentation/kbuild/modules.rst)。
有关内核模块的Makefiles的其他详细信息, 请参阅[Documentation/kbuild/makefiles.rst](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/Documentation/kbuild/makefiles.rst)。在开始修改Makefile之前, 请务必阅读此文件和相关文件。它可能会为你节省大量工作。
> 练习:
>
> 看到`init_module()`中`return`语句上方的注释了吗?将返回值更改为负值, 重新编译并再次加载模块。看看会发生什么!
> 译者注: 如果返回值是非零的正整数则会在日志里看见报错
> ```shell
> 8月 13 17:42:50 xunmi-pc kernel: do_init_module: 'hello_1'->init suspiciously returned 1, it should follow 0/-E convention
> 8月 13 17:42:50 xunmi-pc kernel: CPU: 2 PID: 6800 Comm: insmod Tainted: G O 4.19.0-19-loongson-3 #1
> 8月 13 17:42:50 xunmi-pc kernel: Hardware name: Loongson Loongson-3A5000-HV-7A1000-1w-A2101/Loongson-LS3A5000-7A1000-1w-A2101, BIOS vUDK2018-LoongArch-V4.0.05132-beta10 12/13/
> 8月 13 17:42:50 xunmi-pc kernel: Stack : 9000000000ffb918 9000000000ca60bc 90000003e0e14000 90000003e0e17bb0
> 8月 13 17:42:50 xunmi-pc kernel: 0000000000000007 0000000000071c1c 00000000000b0000 9000000001156950
> 8月 13 17:42:50 xunmi-pc kernel: ...
> 8月 13 17:42:50 xunmi-pc kernel: Call Trace:
> 8月 13 17:42:50 xunmi-pc kernel: [<9000000000209d54>] show_stack+0x34/0x140
> 8月 13 17:42:50 xunmi-pc kernel: [<9000000000ca60b8>] dump_stack+0xa4/0xdc
> 8月 13 17:42:50 xunmi-pc kernel: [<90000000002de294>] do_init_module+0x224/0x230
> 8月 13 17:42:50 xunmi-pc kernel: [<90000000002e038c>] load_module+0x201c/0x24d0
> 8月 13 17:42:50 xunmi-pc kernel: [<90000000002e0b1c>] sys_finit_module+0xcc/0x120
> 8月 13 17:42:50 xunmi-pc kernel: [<9000000000211c74>] syscall_common+0x20/0x34
> ```
> 但模块会部分加载, 使用`sudo lsmod | grep hello`能看到模块
>
> 但如果出现负整数则会在加载模块的时候出现如下报错:
> ```
> insmod: ERROR: could not insert module hello-1.ko: Operation not permitted
> ```
> 模块不会正常加载, 使用`sudo lsmod | grep hello`无法看到模块
### 载入与卸载
在早期的内核版本中, 你必须使用`init_module``cleanup_module`函数, 就像在第一个 hello world 示例中一样, 但现在你可以通过使用`module_init``module_exit`宏来命名这些函数。这些宏在[include/linux/module.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/module.h) 中定义。唯一的要求是必须在调用这些宏之前定义init和cleanup函数, 否则将出现编译错误。下面是该技术的一个示例:
```c
/*
* hello-2.c - 演示module_init()和module_exit()宏
* 这比使用init_module()和cleanup_module()更受欢迎
*/
#include <linux/init.h> /* 初始化需要的宏 */
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
static int __init hello_2_init(void)
{
pr_info("Hello, world 2\n");
return 0;
}
static void __exit hello_2_exit(void)
{
pr_info("Goodbye, world 2\n");
}
module_init(hello_2_init);
module_exit(hello_2_exit);
MODULE_LICENSE("GPL");
```
现在我们有两个内核模块。新增一个模块非常简单:
```Makefile
obj-m += hello-1.o
obj-m += hello-2.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
```
现在来看一个真实示例[drivers/char/Makefile](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/char/Makefile)。从上述文件可以看到有些东西已经被硬编码到内核中(`obj-y`), 但另一些`obj-m`都到哪里去了? 熟悉shell脚本的人将很容易发现它们。对于那些不熟悉的人来说, 你在各处看到的`obj-$(CONFIG_FOO)`条目会根据`CONFIG_FOO`变量是设置为y 还是m, 展开为`obj-y``obj-m`。顺便提一下, 这些正是你在`Linux`内核源代码树的顶级目录下使用`make menuconfig`或类似命令时, 在`.config`文件中设置的那种变量。
> 译者注: 在构建内核或模块时, `obj-y` 和 `obj-m` 是用于 `Makefile` 的变量。y 代表“yes”, 表示这个文件或模块会被编译成内核的一部分, 而 m 代表“module”, 表示这个文件或模块会被编译成一个可加载的模块。
### `__init`和`__exit`宏
`__init`宏: 在初始化函数为内置驱动程序(不是可加载模块)完成后丢弃初始化函数并释放其内存。如果你思考一下初始化函数何时被调用就会发现这是非常合理的操作, 因为内置模块不需要再次加载, 而可加载的模块则需要。还有一个`__initdata`, 其工作方式类似于`__init`, 但用于初始化变量而不是函数。
`__exit`宏: 在将模块内置到内核时也会被省略, 与__init 类似, 对可加载模块没有影响。考虑到退出函数的作用这种做法也是完全合理的。这些宏在[include/linux/init.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/init.h)中定义, 用于释放内核内存。当引导内核并看到类似"释放未使用的内核内存: 236k已释放(Freeing unused kernel memory: 236k freed)"的内容时, 这正是内核正在释放的内容。
```c
/*
* hello-3.c - __init, __initdata 和 __exit 宏的使用说明
*/
#include <linux/init.h> /* 初始化需要的宏 */
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
static int hello3_data __initdata = 3;
static int __init hello_3_init(void)
{
pr_info("Hello, world %d\n", hello3_data);
return 0;
}
static void __exit hello_3_exit(void)
{
pr_info("Goodbye, world 3\n");
}
module_init(hello_3_init);
module_exit(hello_3_exit);
MODULE_LICENSE("GPL");
```
### 许可(Licensing)和模块文档
老实说, 谁加载甚至关心专有模块?如果你这样做那么你可能已经看到了这样的事情:
$ sudo insmod xxxxxx.ko
loading out-of-tree module taints kernel.
module license 'unspecified' taints kernel.
你可以使用几个宏来指示模块的许可证。常见示例有"GPL"、"GPL v2"、"GPL和附加权限(GPL and additional rights)"、"Dual BSD/GPL", "Dual MIT/GPL""DualMPL/GPL"和"Proprietary"。它们在[include/linux/module.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/module.h)中定义。
要引用你正在使用的许可证, 可以使用名为`MODULE_LICENSE`的宏。下面的示例中说明了该宏和几个描述该模块的其他宏。
```c
/*
* hello-4.c - 演示模块文档
*/
#include <linux/init.h> /* 初始化需要的宏 */
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("寻觅");
MODULE_DESCRIPTION("一个练习驱动");
static int __init init_hello_4(void)
{
pr_info("Hello, world 4\n");
return 0;
}
static void __exit cleanup_hello_4(void)
{
pr_info("Goodbye, world 4\n");
}
module_init(init_hello_4);
module_exit(cleanup_hello_4);
```
### 将命令行参数传递到模块
模块可以采用命令行参数, 但不能使用你可能习惯的argc/argv。
要允许将参数传递给模块, 请声明将采用命令行参数值的变量作为全局变量,然后使用`module_param()`宏(在[include/linux/moduleparam.h]((https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/moduleparam.h))中定义)来设置机制。在运行时, `insmod`将使用给定的任何命令行参数填充变量, 如`insmod mymodule.ko myvariable=5`。为了清楚起见, 变量声明和宏应该放在模块的开头。这块解释的很糟糕, 还是无法理解可以试着查看实例代码。
`module_param()`宏接受3个参数: 变量的名称、类型和sysfs中相应文件的权限。整数类型可以像平常一样有符号, 也可以无符号。如果要使用整数数组或字符串数组, 请使用`module_param_array()``module_param_string()`
```c
int myint = 3;
module_param(myint, int, 0);
```
数组也得到了支持, 但与过去相比, 现在的处理方式有所不同。为了跟踪参数数量, 你需要将一个指向计数变量的指针作为第三个参数传递。你也可以选择忽略计数并传递NULL。下面我们展示了两种可能性:
```c
int myintarray[2];
module_param_array(myintarray, int, NULL, 0); /* 不需求计数器 */
short myshortarray[4];
int count;
module_param_array(myshortarray, short, &count, 0); /* 将计数器的值传给count */
```
这种用法的一个好处是可以设置模块变量的默认值, 如端口或IO 地址。如果变量包含默认值, 则执行自动检测(在其他地方解释)。否则, 保留当前值。这将在后面详细说明。
最后, 有一个宏函数`MODULE_PARM_DESC()`, 用于记录模块可以接受的参数。它接受两个参数: 变量名和描述该变量的自由格式字符串。
```c
/*
* hello-5.c - 演示传递给模块的命令行参数
*/
#include <linux/init.h>
#include <linux/kernel.h> /* 引入 ARRAY_SIZE() */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/printk.h>
#include <linux/stat.h>
MODULE_LICENSE("GPL");
static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "寻觅";
static int myintarray[2] = { 420, 420 };
static int arr_argc = 0;
/* module_param(foo, int, 0000)
* foo: 传入参数的名称
* int: 参数的数据类型
* 0000: 权限位, 用于在稍后阶段公开sysfs中的参数(如果非零)
*/
module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myshort, "一个短整数");
module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(myint, "一个整数");
module_param(mylong, long, S_IRUSR);
MODULE_PARM_DESC(mylong, "一个长整数");
module_param(mystring, charp, 0000);
MODULE_PARM_DESC(mystring, "一个字符");
/* module_param_array(name, type, num, perm);
* name: 数组的名称
* type: 数组元素的类型
* num: 指向变量的指针, 该变量将在模块加载时存储用户初始化的数组元素数量
* perm: 权限位
*/
module_param_array(myintarray, int, &arr_argc, 0000);
MODULE_PARM_DESC(myintarray, "一组整数数组");
static int __init hello_5_init(void)
{
int i;
pr_info("你好 5\n=============\n");
pr_info("myshort是一个短整数: %hd\n", myshort);
pr_info("myint是一个整数: %d\n", myint);
pr_info("mylong是一个长整数: %ld\n", mylong);
pr_info("mystring是一个字符: %s\n", mystring);
for (i = 0; i < ARRAY_SIZE(myintarray); i++)
pr_info("myintarray[%d] = %d\n", i, myintarray[i]);
pr_info("从 myintarray 中获得 %d 个参数\n", arr_argc);
return 0;
}
static void __exit hello_5_exit(void)
{
pr_info("再见 5\n");
}
module_init(hello_5_init);
module_exit(hello_5_exit);
```
推荐使用下述方法测试参数传递代码:
```shell
$ sudo insmod hello-5.ko mystring="bebop" myintarray=-1
$ sudo dmesg -t | tail -7
myshort is a short integer: 1
myint is an integer: 420
mylong is a long integer: 9999
mystring is a string: bebop
myintarray[0] = -1
myintarray[1] = 420
got 1 arguments for myintarray.
$ sudo rmmod hello-5
$ sudo dmesg -t | tail -1
Goodbye, world 5
$ sudo insmod hello-5.ko mystring="supercalifragilisticexpialidocious" myintarray=-1,-1
$ sudo dmesg -t | tail -7
myshort is a short integer: 1
myint is an integer: 420
mylong is a long integer: 9999
mystring is a string: supercalifragilisticexpialidocious
myintarray[0] = -1
myintarray[1] = -1
got 2 arguments for myintarray.
$ sudo rmmod hello-5
$ sudo dmesg -t | tail -1
Goodbye, world 5
$ sudo insmod hello-5.ko mylong=hello
insmod: ERROR: could not insert module hello-5.ko: Invalid parameters
```
### 跨多文件的模块
有时将内核模块划分为几个源文件是有意义的。会分别创建一个`start.c``stop.c`来共同组合成一个模块。
跨文件模块的初始化部分, `start.c`:
```c
/*
* start.c - 跨文件模块-初始化
*/
#include <linux/kernel.h> /* 进行内核工作时引入的头 */
#include <linux/module.h> /* 内核模块必要的头 */
int init_module(void)
{
pr_info("你好, 内核初始化模块启动\n");
return 0;
}
MODULE_LICENSE("GPL");
```
跨文件模块的结束部分, `stop.c`
```c
/*
* stop.c - 跨文件模块-结束
*/
#include <linux/kernel.h> /* 进行内核工作时引入的头 */
#include <linux/module.h> /* 内核模块必要的头 */
void cleanup_module(void)
{
pr_info("一个很短的内核结束模块\n");
}
MODULE_LICENSE("GPL");
```
这里makefile和之前有所不同, 跨多文件的模块加上我们上述所有hello模块的makefile文件如下:
```Makefile
obj-m += hello-1.o
obj-m += hello-2.o
obj-m += hello-3.o
obj-m += hello-4.o
obj-m += hello-5.o
obj-m += startstop.o
startstop-objs := start.o stop.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
```
前五行没什么特别的, 但对于最后一个示例, 在其中使用了两行。首先, 我们为我们的组合模块发明一个对象名称(`startstop.o`), 其次我们告诉`make`哪些对象文件是该模块的一部分。
### 为预编译内核构建模块
我们强烈建议你重新编译内核, 以便你可以启用许多有用的调试功能, 例如强制模块卸载(MODULE_FORCE_UNLOAD): 当启用此选项时, 你可以通过sudo rmmod -f module命令强制内核卸载模块, 即使它认为模块不安全。在模块开发过程中, 此选项可以节省大量时间和多次重新启动。如果不想重新编译内核, 则应该考虑在虚拟机上的测试发行版中运行这些示例。如果你搞坏了什么, 可以快速重新启动或恢复虚拟机(VM)。
在许多情况下, 你可能希望将模块加载到预编译的运行内核中, 例如常见Linux发行版附带的内核, 或者你过去编译过的内核。在某些情况下, 你可能需要编译模块并将其插入到不允许重新编译的正在运行的内核中, 或者在不希望重新启动的机器上。如果你想不出一种情况会迫使你为预编译内核使用模块,那么你可能想跳过本节剩下的内容, 并将本节的其余部分视为一个大注脚。
如果你只是安装内核源代码(kernel source tree), 请使用它来编译内核模块, 然后尝试将模块插入内核, 在大多数情况下, 你将获得如下错误:
> * insmod: ERROR: could not insert module poet.ko: Invalid module format
> * 报错翻译: insmod: 错误:无法插入poet.ko模块: 无效的模块格式
一些隐秘信息被记录到systemd日志中:
> * kernel: poet: disagrees about version of symbol module_layout
> * 报错翻译: kernel: poet: 对 module_layout 的版本符号存在分歧
换句话说, 内核拒绝接受模块, 因为版本信息不匹配(版本信息更准确的翻译应该是`版本魔术(version magic)`, 在这里版本信息更有助于理解, 后续所有`version magic`的翻译都会翻译成版本信息, 请参阅[include/linux/vermagic.h]((https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/vermagic.h)))。顺便说一句,版本信息以静态字符串的形式存储在模块对象中, 从`vermagic`开始。当版本信息链接到`kernel/module.o`文件时, 它会插入到模块中。要检查存储在给定模块中的版本信息和其他字符串, 可以使用命令`modinfo module.ko`:
```shell
$ sudo modinfo hello-4.ko
filename: /home/xunmi/object/drive/hello-4.ko
description: 一个练习驱动
author: 寻觅
license: GPL
depends:
name: hello_4
vermagic: 4.19.0-19-loongson-3 SMP mod_unload modversions LOONGARCH 64BIT
```
为了克服这个问题, 我们可以使用`--force-vermagic`选项, 但这种解决方案可能是不安全的, 并且在生产环境中是不可接受的。因此, 我们希望在与构建预编译内核的环境相同的环境中编译模块。如何做到这一点, 是本章其余部分的主要议题。
首先, 确保有一个内核源代码树可用, 其版本与当前内核完全相同。然后, 找到用于编译预编译内核的配置文件。这通常在/boot目录下, 文件名类似于config-5.14.x。你可能只需将其复制到内核源代码中: 执行命令`cp /boot/config-$(uname -r) .config`
观察之前的版本信息, 即使两个配置文件完全相同, 版本信息还是可能会出现一些差异阻止模块插入到内核中。这种差异是由于对某些发行版修改了Makefile中定义的版本信息所致。查看你的内核源码根目录下的Makefile, 确保指定的版本信息与当前内核使用的完全匹配。发行版的Makefile是类似如下格式:
```shell
VERSION = 5
PATCHLEVEL = 14
SUBLEVEL = 0
EXTRAVERSION = -rc2
```
假设出现上述情况时, 我们需要将`Makefile`中的`EXTRAVERSION`的值配置为`-rc2`。可以使用如下命令保留编译内核的makefile的备份。
```shell
cp /lib/modules/$(uname -r)/build/Makefile linux-$(uname -r)
```
`linux-$(uname -r)`是你试图构建的linux内核源代码, 可以在命令行中执行`uname -r`查看返回值。最后, 运行make来更新配置、头文件:
```shell
$ make
SYNC include/config/auto.conf.cmd
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTLD scripts/kconfig/conf
```
如果你不想编译完整的内核, 在SPLIT行之后中断构建(CTRL-C) 即可, 后续返回到模块的目录并编译模块即可: 它将完全根据你当前的内核设置构建, 并且它将在没有任何错误的情况下加载。
## 准备工作
### 模块开始和结束方式
典型的程序以 `main()` 函数开始, 执行一系列指令, 并在完成这些指令后终止。然而内核模块遵循模式有所不同。模块总是以`init_module`函数或`module_init`调用指定的函数开头。该函数是模块的入口, 会通知内核模块的功能, 并在必要时让内核做好使用模块功能的准备。完成这些任务后, 入口函数返回, 模块将保持不活动状态, 直到内核使用其代码。
所有模块都通过`cleanup_module`函数 或`module_exit`调用指定的函数来结束。用作模块的退出功能, 通过注销以前注册的功能来逆转入口函数操作。
每个模块都必须具有进入和退出功能。虽然有多种方法来定义这些函数, 但通常使用术语"进入函数(entry function)"和"退出函数(exit function)"。它们有时可能被称为`init_module``cleanup_module`这两个模块的含义是相同的。
### 模块可用的功能
程序员经常使用不是自己定义的函数。这方面的一个常见的例子是`printf()`。在使用标准C库libc提供的这些库函数实际上直到链接阶段才会进入你的程序, 这确保代码(例如`printf()`的代码)可用, 并将调用指令指向那段代码。
内核模块在这里也有所不同。在"Hello World"示例中, 你可能注意到我们使用了函数`pr_info()`, 但没有包括标准I/O库。这是因为模块是对象文件, 其符号在运行`insmod``modprobe`时得到解析。符号的定义来自内核本身; 你只能使用内核提供的外部函数。如果你对内核已导出的符号感兴趣, 可以查看`/proc/kallsyms`
需要记住的一点是库函数和系统调用之间的区别。库函数是更上层的, 完
全在用户空间中运行, 并为程序员提供了一个更方便的接口, 以实现真正的工
作系统调用。系统调用代表用户在内核模式下运行, 并由内核本身提供。库函
数printf()可能看起来像一个非常通用的打印函数, 但它真正做的只是将数据
格式化为字符串, 并使用底层系统调用write()写入字符串数据, 然后将数据发
送到标准输出。
可以使用`strace`看看`printf()`都进行了那些系统调用。编译以下程序:
```c
#include <stdio.h>
int main(void)
{
printf("你好");
return 0;
}
```
使用`gcc -Wall -o hello hello.c`编译此文件后可以调用`strace ./hello`运行可执行文件, 可以看到的每一行都对应于一个系统调用。[strace](https://strace.io/)是一个非常实用的程序, 它可以告诉你一个程序正在进行的系统调用的详细信息, 包括调用的是哪个系统函数、其参数是什么以及它返回了什么。这是一个极其宝贵的工具, 用来了解诸如程序试图访问哪些文件之类的问题。在输出的最后, 你会看到像`write(1, "\344\275\240\345\245\275", 6你好)` 这样的一行。这就是`printf()`函数背后的真实系统调用。你可能不熟悉`write`, 因为大多数人会使用库函数来进行文件`I/O(文件读写操作)`(如`fopen` `fputs` `fclose`)。如果想要了解更详细的信息可以查看`man 2 write`。第二部分的手册专门讨论系统调用(如`kill()` `read()`), 而第三部分的手册则专门讨论库调用, 你可能会更熟悉这部分(如`cosh()` `random()`)。
你甚至可以编写模块来替换内核的系统调用, 我们很快就会这样做。这种操作常被骇客用于制作后门或木马程序, 但你可以编写自己的模块来做无害的事情, 比如每当有人试图删除你系统上的文件时, 让内核输出"哎呀!你干~嘛?"
### 用户空间vs内核空间
内核主要管理对资源的访问, 无论是显卡、硬盘还是内存。程序经常争夺相同的资源。例如, 在保存文档时, **updatedb** 可能会开始更新 **locate** 数据库。vim等编辑器中的会话和 **updatedb** 等进程可以同时利用硬盘。内核的作用是维护秩序, 确保用户不会随意访问资源。
为了管理这些资源, CPU 在不同模式下运行, 每个模式都提供不同级别的系统控制。例如, 英特尔的80386架构就具有四种这样的模式, 称为环。然而, Unix仅使用其中两个环: 最高环(环0, 也称为"内核模式", 其中允许所有操作)和最低环, 称为"用户模式"。
回顾关于库函数与系统调用的讨论。通常会在用户模式下使用库函数。库函数调用一个或多个系统调用, 并且这些系统调用以库函数的名义执行, 但在内核模式下执行, 因为它们是内核本身的一部分。一旦系统调用完成其任务, 它将返回, 并将执行权转交回用户模式。
### 命名空间
当你在编写一个小规模项目时, 使用通俗易懂的变量名通常是为了增加程序的可读性。但如果参与一个多人维护的庞大项目, 你的全局变量会和其他人的全局变量共存, 可能会出现全局变量名冲突的情况。当一个程序拥有大量全局变量且这些变量名不足以准确描述其用途时, 就会出现命名空间污染。在大型团队项目中, 在确保能记住保留的名称的同时, 需要找到一种制定方案, 以便为变量名和符号命名保持唯一性。
在编写内核代码时, 即使是最小的模块也会链接到整个内核, 因此需要避免命名空间污染这个问题。处理此问题的最佳方法是将所有变量声明为静态, 并使用一个精准不会重复的前缀。按照惯例, 所有内核前缀都是小写的。如果你不想将所有内容都声明为静态, 另一个方法是声明符号表并将其注册到内核。我们稍后将讨论这个问题。
文件 `/proc/kallsyms` 包含了内核所知道的所有符号, 因此这些符号对你的模块来说是可访问的, 因为它们共享了内核的代码空间。
### 代码空间
内存管理是一个非常复杂的主题, O'Reilly 出版的 [Understanding The Linux Kernel(理解 Linux 内核)](https://www.oreilly.com/library/view/understanding-the-linux/0596005652/)一书中大部分内容都专注于内存管理!我们的目标不是成为内存管理方面的专家, 但我们确实需要掌握一些基本知识, 才能开始着手编写实际的模块。
如果你没有深入思考过段错误(segfault)的真正含义, 你可能会惊讶地发现指针并不真正指向内存位置——至少不是真实的内存位置。当一个进程被创建时,内核会划分一部分真实的物理内存并分配给该进程, 用于其执行代码、变量、栈、堆以及计算机科学家所熟知的其他内容。这部分内存从0x00000000开始,延伸到所需的任何地址。由于任何两个进程的内存空间不会重叠, 任何可以访问某个内存地址(比如0xbffff978)的进程, 实际上访问的是物理内存中的不同位置!这些进程实际上访问的是名为0xbffff978的索引, 该索引指向为该特定进程预留的内存区域中的某个偏移量。大多数情况下, 像我们的"Hello, World"程序这样的进程不能访问其他进程的空间, 我们稍后会讨论一些可以做到这一点的方法。
内核也有自己的内存空间。由于模块是可以在内核中动态插入和删除的代码(与半独立对象相反), 因此它共享内核的代码空间, 而不是拥有自己的代码空间。因此, 如果模块段出错, 则内核段出错。如果你因为一个"边界错误(off-by-one)"开始覆写数据, 那么你就是在破坏内核数据(或代码)。这种情况比听起来的还要危险, 所以尽量小心处理。
> 译者注: "半独立"强调的是既有独立操作的能力, 同时又不完全脱离外部控制或支持的状态, 内核模块直接运行在内核空间, 与内核共享相同的内存和执行环境, 而半独立的对象通常在用户空间运行, 拥有自己独立的内存和资源管理, 这使得它们在出现错误时不会直接影响到内核的稳定性。
应当注意, 上述讨论适用于任何使用单块式内核(monolithic kernel)的操作系统。这个概念与"将所有模块构建到内核中"略有不同, 尽管底层原理类似。相反, 在微内核模块会分配自己的代码空间。微内核的著名的例子有[GNU
Hurd](https://www.gnu.org/software/hurd/)和谷歌Fuchsia的[锆石内核(Zircon kernel)](https://fuchsia.dev/fuchsia-src/concepts/kernel)。
### 设备驱动
有一类模块是设备驱动程序, 它为硬件(如串行端口)提供功能。在Unix上, 每个硬件都通过位于`/dev`目录下的设备文件来表示, 这些文件提供了与硬件通信的手段。设备驱动程序代表用户程序提供通信。因此, **es1370.ko** 声卡设备驱动程序可以将`/dev/sound`设备文件连接到 **Ensoniq ES1370** 声卡。像 `mp3blaster` 这样的用户空间程序可以使用`/dev/sound`, 而无需知道安装了何种声卡。
我们可以看一些设备文件。以下是表示`串行端口(tty-teletypewriter)`上的前三个端口的设备文件:
```shell
$ ls -l /dev/tty[1-3]
crw--w---- 1 root tty 4, 1 7月28日 15:44 /dev/tty1
crw--w---- 1 root tty 4, 2 7月28日 15:44 /dev/tty2
crw--w---- 1 root tty 4, 3 7月28日 15:44 /dev/tty3
```
请注意日期前方由逗号分隔的数字列(4, 1 ; 4, 2 ; 4, 3)。第一个数字称为设备的主编号。第二个数字是次要数字。主编号告诉你使用哪个驱动程序访问硬件。每个驱动程序都有唯一的一个主编号;主编号相同的设备文件都由同一个驱动程序控制。上述所有主编号都是 **4** , 因为它们都由同一个驱动程序控制。
次要编号由驱动程序用于区分其控制的各种硬件。回到上面的例子, 尽管所有三个设备都由相同的驱动程序处理, 但它们具有唯一的次要编号, 因为驱动程序将它们视为不同的硬件。
设备分为两种类型: 字符设备和块设备。区别在于块设备具有请求的缓冲区, 因此它们可以选择响应请求的最佳顺序。这一点对存储设备尤为重要, 因为读写相邻的扇区比读写距离较远的扇区更快。另一个区别是, 块设备只能按块接收输入和返回输出(块的大小可以根据设备而变), 而字符设备则可以按需要使用任意数量的字节。世界上大多数设备都是字符型的, 它们不需要这种缓冲不以固定的块大小运行。你可以通过查看`ls -l`输出中的第一个字符来判断设备文件是用于块设备还是字符设备。如果它是"b", 则它是块设备, 如果它是c, 则它为字符设备。上面看到的设备是字符设备。以下是一些块设备(存储):
```shell
brw-rw---- 1 root disk 7, 0 7月28日 15:44 loop0
brw-rw---- 1 root disk 7, 1 7月28日 15:44 loop1
brw-rw---- 1 root disk 7, 2 7月28日 15:44 loop2
brw-rw---- 1 root disk 7, 3 7月28日 15:44 loop3
brw-rw---- 1 root disk 7, 4 7月28日 15:44 loop4
brw-rw---- 1 root disk 7, 5 7月28日 15:44 loop5
brw-rw---- 1 root disk 7, 6 7月28日 15:44 loop6
brw-rw---- 1 root disk 7, 7 7月28日 15:44 loop7
brw-rw---- 1 root disk 259, 0 7月28日 15:44 nvme0n1
brw-rw---- 1 root disk 259, 1 7月28日 15:44 nvme0n1p1
brw-rw---- 1 root disk 8, 0 7月28日 15:44 sda
brw-rw---- 1 root disk 8, 1 7月28日 15:44 sda1
brw-rw---- 1 root disk 8, 2 7月28日 15:44 sda2
```
如果要查看已分配的主编号, 可以查看[Documentation/admin-guide/devices.txt](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/Documentation/admin-guide/devices.txt).
安装系统时, 所有设备文件都是通过`mknod`命令创建的。要创建一个名为`coffee`的新字符设备, 主为12和次为2, 只需执行`mknod /dev/coffee c 12 2`。你不必将设备文件放入`/dev`中, 但这是按照约定完成的。Linus将他的设备文件放在`/dev`中, 你也应该这样。然而, 当为测试目的创建设备文件时, 将其放置在你编译内核模块的工作目录中通常是可以的。但完成设备驱动程序编写后, 确保将其放在正确的位置。
最后需要明确的是当访问设备文件时, 内核使用该文件的主编号来识别处理访问对应的驱动程序。这意味着内核不必依赖或了解次设备号。处理次设备号的是驱动程序本身, 它用次设备号来区分不同的硬件设备。
需要注意的是, 当提到 **"硬件(hardware)"** 时, 该术语的使用比较抽象, 他指的不仅仅是PCI设备。看看以下两个设备文件:
```c
ls -l /dev/nvme0n1* /dev/sda
brw-rw---- 1 root disk 259, 0 728 15:44 /dev/nvme0n1
brw-rw---- 1 root disk 259, 1 728 15:44 /dev/nvme0n1p1
```
从这两个设备文件的信息你应该能看出它们是块设备, 并且他们是由相同的驱动程序处理(块, 主259)。但是上述具有相同主编号但不同次号的两个设备文件实际上是表示相同的物理硬件。所以请注意, 我们讨论中的 **"硬件(hardware)"** 一词可能意味着一些抽象的东西。

View File

View File

0
文档/03-ioctl.md Normal file
View File

View File

View File

View File

View File

View File

View File

View File

17
示例/0-hello/Makefile Normal file
View File

@ -0,0 +1,17 @@
obj-m += hello-1.o
obj-m += hello-2.o
obj-m += hello-3.o
obj-m += hello-4.o
obj-m += hello-5.o
obj-m += startstop.o
startstop-objs := start.o stop.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

21
示例/0-hello/hello-1.c Normal file
View File

@ -0,0 +1,21 @@
/*
* hello-1.c - 最简单的内核模块
*/
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
int init_module(void)
{
pr_info("你好,世界 1(报错返回).\n");
/* 非0返回意味着init_module失败; 无法加载模块 */
return 1;
}
void cleanup_module(void)
{
pr_info("再见,世界 1.\n");
}
MODULE_LICENSE("GPL");

24
示例/0-hello/hello-2.c Normal file
View File

@ -0,0 +1,24 @@
/*
* hello-2.c - 演示module_init()和module_exit()宏
* 这比使用init_module()和cleanup_module()更受欢迎
*/
#include <linux/init.h> /* 初始化需要的宏 */
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
static int __init hello_2_init(void)
{
pr_info("你好 2\n");
return 0;
}
static void __exit hello_2_exit(void)
{
pr_info("再见 2\n");
}
module_init(hello_2_init);
module_exit(hello_2_exit);
MODULE_LICENSE("GPL");

25
示例/0-hello/hello-3.c Normal file
View File

@ -0,0 +1,25 @@
/*
* hello-3.c - __init, __initdata 和 __exit 宏的使用说明
*/
#include <linux/init.h> /* 初始化需要的宏 */
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
static int hello3_data __initdata = 3;
static int __init hello_3_init(void)
{
pr_info("Hello, world %d\n", hello3_data);
return 0;
}
static void __exit hello_3_exit(void)
{
pr_info("Goodbye, world 3\n");
}
module_init(hello_3_init);
module_exit(hello_3_exit);
MODULE_LICENSE("GPL");

25
示例/0-hello/hello-4.c Normal file
View File

@ -0,0 +1,25 @@
/*
* hello-4.c - 演示模块文档
*/
#include <linux/init.h> /* 初始化需要的宏 */
#include <linux/module.h> /* 内核模块必要的头 */
#include <linux/printk.h> /* 引入 pr_info() */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("寻觅");
MODULE_DESCRIPTION("一个练习驱动");
static int __init init_hello_4(void)
{
pr_info("Hello, world 4\n");
return 0;
}
static void __exit cleanup_hello_4(void)
{
pr_info("Goodbye, world 4\n");
}
module_init(init_hello_4);
module_exit(cleanup_hello_4);

67
示例/0-hello/hello-5.c Normal file
View File

@ -0,0 +1,67 @@
/*
* hello-5.c - 演示传递给模块的命令行参数
*/
#include <linux/init.h>
#include <linux/kernel.h> /* 引入 ARRAY_SIZE() */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/printk.h>
#include <linux/stat.h>
MODULE_LICENSE("GPL");
static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "寻觅";
static int myintarray[2] = { 420, 420 };
static int arr_argc = 0;
/* module_param(foo, int, 0000)
* foo: 传入参数的名称
* int: 参数的数据类型
* 0000: 权限位, 用于在稍后阶段公开sysfs中的参数(如果非零)
*/
module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myshort, "一个短整数");
module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(myint, "一个整数");
module_param(mylong, long, S_IRUSR);
MODULE_PARM_DESC(mylong, "一个长整数");
module_param(mystring, charp, 0000);
MODULE_PARM_DESC(mystring, "一个字符");
/* module_param_array(name, type, num, perm);
* name: 数组的名称
* type: 数组元素的类型
* num: 指向变量的指针, 该变量将在模块加载时存储用户初始化的数组元素数量
* perm: 权限位
*/
module_param_array(myintarray, int, &arr_argc, 0000);
MODULE_PARM_DESC(myintarray, "一组整数数组");
static int __init hello_5_init(void)
{
int i;
pr_info("你好 5\n=============\n");
pr_info("myshort是一个短整数: %hd\n", myshort);
pr_info("myint是一个整数: %d\n", myint);
pr_info("mylong是一个长整数: %ld\n", mylong);
pr_info("mystring是一个字符: %s\n", mystring);
for (i = 0; i < ARRAY_SIZE(myintarray); i++)
pr_info("myintarray[%d] = %d\n", i, myintarray[i]);
pr_info("从 myintarray 中获得 %d 个参数\n", arr_argc);
return 0;
}
static void __exit hello_5_exit(void)
{
pr_info("再见 5\n");
}
module_init(hello_5_init);
module_exit(hello_5_exit);

7
示例/0-hello/hello.c Normal file
View File

@ -0,0 +1,7 @@
#include <stdio.h>
int main(void)
{
printf("你好");
return 0;
}

15
示例/0-hello/start.c Normal file
View File

@ -0,0 +1,15 @@
/*
* start.c - 跨文件模块-初始化
*/
#include <linux/kernel.h> /* 进行内核工作时引入的头 */
#include <linux/module.h> /* 内核模块必要的头 */
int init_module(void)
{
pr_info("你好, 内核初始化模块启动\n");
return 0;
}
MODULE_LICENSE("GPL");

14
示例/0-hello/stop.c Normal file
View File

@ -0,0 +1,14 @@
/*
* stop.c - 跨文件模块-结束
*/
#include <linux/kernel.h> /* 进行内核工作时引入的头 */
#include <linux/module.h> /* 内核模块必要的头 */
void cleanup_module(void)
{
pr_info("一个很短的内核结束模块\n");
}
MODULE_LICENSE("GPL");

11
示例/1-chardev/Makefile Normal file
View File

@ -0,0 +1,11 @@
obj-m += chardev.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

161
示例/1-chardev/chardev.c Normal file
View File

@ -0,0 +1,161 @@
/*
* chardev.c: 创建一个只读 char 设备,显示从 dev 文件读取的次数
*/
#include <linux/atomic.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h> /* 引入 sprintf() */
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/uaccess.h> /* 引入 get_user 和 put_user */
#include <linux/version.h>
#include <asm/errno.h>
/* 原型(Prototypes) - 这通常会放在一个 .h 文件中 */
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char __user *, size_t,
loff_t *);
#define SUCCESS 0
#define DEVICE_NAME "chardev" /* 在/proc/devices中显示的Dev名称 */
#define BUF_LEN 80 /* 来自驱动消息的最大长度 */
/* 全局变量声明为静态的, 文件中的全局变量也是如此 */
static int major; /* 分配给设备驱动程序的主编号 */
enum {
CDEV_NOT_USED = 0,
CDEV_EXCLUSIVE_OPEN = 1,
};
/* 设备是否打开?用于防止多次访问设备 */
static atomic_t already_open = ATOMIC_INIT(CDEV_NOT_USED);
static char msg[BUF_LEN + 1]; /* 当被请求时,设备将给出的信息 */
static struct class *cls;
static struct file_operations chardev_fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release,
};
static int __init chardev_init(void)
{
major = register_chrdev(0, DEVICE_NAME, &chardev_fops);
if (major < 0) {
pr_alert("注册字符设备失败 %d\n", major);
return major;
}
pr_info("分配的主编号 %d.\n", major);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
cls = class_create(DEVICE_NAME);
#else
cls = class_create(THIS_MODULE, DEVICE_NAME);
#endif
device_create(cls, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
pr_info("设备创建于 /dev/%s\n", DEVICE_NAME);
return SUCCESS;
}
static void __exit chardev_exit(void)
{
device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
/* 注销设备 */
unregister_chrdev(major, DEVICE_NAME);
}
/* 方法 */
/* 当进程尝试打开设备文件时调用,如
* "sudo cat /dev/chardev"
*/
static int device_open(struct inode *inode, struct file *file)
{
static int counter = 0;
if (atomic_cmpxchg(&already_open, CDEV_NOT_USED, CDEV_EXCLUSIVE_OPEN))
return -EBUSY;
sprintf(msg, "我已经跟你说了 %d 次 Hello world!\n", counter++);
try_module_get(THIS_MODULE);
return SUCCESS;
}
/* 当进程关闭设备文件时调用 */
static int device_release(struct inode *inode, struct file *file)
{
/* 准备好迎接下一个触发 */
atomic_set(&already_open, CDEV_NOT_USED);
/* 减少使用次数,否则一旦打开文件,就永远无法删除模块 */
module_put(THIS_MODULE);
return SUCCESS;
}
/* 当一个已打开 dev 文件的进程试图读取该文件时调用 */
static ssize_t device_read(struct file *filp, /* 请参见 include/linux/fs.h */
char __user *buffer, /* 待填充数据的缓冲区 */
size_t length, /* 缓冲区长度 */
loff_t *offset)
{
/* 实际写入缓冲区的字节数 */
int bytes_read = 0;
const char *msg_ptr = msg;
if (!*(msg_ptr + *offset)) { /* 已到达消息的末尾 */
*offset = 0; /* 重置偏移量 */
return 0; /* 表示文件结束 */
}
msg_ptr += *offset;
/* 实际将数据放入缓冲区 */
while (length && *msg_ptr) {
/* 缓冲区在用户数据段,而不是内核段所以直接赋值 "*" 是无效的
* 我们必须使用 put_user 函数将数据从内核数据段复制到用户数据段
*/
put_user(*(msg_ptr++), buffer++);
length--;
bytes_read++;
}
*offset += bytes_read;
/* 大多数读取函数返回放入缓冲区的字节数 */
return bytes_read;
}
/* 当进程写入dev文件时调用: echo "hi" > /dev/hello */
static ssize_t device_write(struct file *filp, const char __user *buff,
size_t len, loff_t *off)
{
pr_alert("对不起,不支持此操作.\n");
return -EINVAL;
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,15 @@
obj-m += procfs1.o
obj-m += procfs2.o
obj-m += procfs3.o
obj-m += procfs4.o
obj-m += hello-sysfs.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
gcc procfs.c -o 占位
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm 占位

View File

@ -0,0 +1,61 @@
/*
* hello-sysfs.c sysfs 示例
*/
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kobject.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/sysfs.h>
static struct kobject *mymodule;
/* 你希望能够更改的变量 */
static int myvariable = 0;
static ssize_t myvariable_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", myvariable);
}
static ssize_t myvariable_store(struct kobject *kobj,
struct kobj_attribute *attr, char *buf,
size_t count)
{
sscanf(buf, "%du", &myvariable);
return count;
}
static struct kobj_attribute myvariable_attribute =
__ATTR(myvariable, 0660, myvariable_show, (void *)myvariable_store);
static int __init mymodule_init(void)
{
int error = 0;
pr_info("我的模块: 初始化完成\n");
mymodule = kobject_create_and_add("我的模块", kernel_kobj);
if (!mymodule)
return -ENOMEM;
error = sysfs_create_file(mymodule, &myvariable_attribute.attr);
if (error) {
pr_info("在 /sys/kernel/我的模块 创建 myvariable 文件失败\n");
}
return error;
}
static void __exit mymodule_exit(void)
{
pr_info("我的模块: 退出成功\n");
kobject_put(mymodule);
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,37 @@
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
char procName[256];
strcpy(procName, "/proc/buffer2k");
int fd = open(procName, O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
char buffer[256];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer) - 1);
if (bytesRead < 0) {
perror("read");
close(fd);
return 1;
}
int seconds = 30;
buffer[bytesRead] = '\0';
printf("%s 内容: %s \n", procName, buffer, seconds);
while (seconds > 0) {
printf("\r文件将在: %d 秒后关闭", seconds);
fflush(stdout);
sleep(1);
seconds--;
}
close(fd);
printf("\r文件已关闭");
return 0;
}

View File

@ -0,0 +1,69 @@
/*
* procfs1.c
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define procfs_name "helloworld"
static struct proc_dir_entry *our_proc_file;
static ssize_t procfile_read(struct file *file_pointer, char __user *buffer,
size_t buffer_length, loff_t *offset)
{
char s[13] = "HelloWorld!\n";
int len = sizeof(s);
ssize_t ret = len;
/* 如果偏移量大于或等于字符串长度,或者 copy_to_user 失败,则读取操作失败,返回 0。 */
if (*offset >= len || copy_to_user(buffer, s, len)) {
pr_info("copy_to_user 执行结束 \n");
ret = 0;
} else {
pr_info("读取项目文件: %s\n", file_pointer->f_path.dentry->d_name.name);
*offset += len;
}
return ret;
}
#ifdef HAVE_PROC_OPS
static const struct proc_ops proc_file_fops = {
.proc_read = procfile_read,
};
#else
static const struct file_operations proc_file_fops = {
.read = procfile_read,
};
#endif
static int __init procfs1_init(void)
{
our_proc_file = proc_create(procfs_name, 0644, NULL, &proc_file_fops);
if (NULL == our_proc_file) {
proc_remove(our_proc_file);
pr_alert("错误: 无法初始化 /proc/%s\n", procfs_name);
return -ENOMEM;
}
pr_info("/proc/%s 已创建\n", procfs_name);
return 0;
}
static void __exit procfs1_exit(void)
{
proc_remove(our_proc_file);
pr_info("/proc/%s 已卸载\n", procfs_name);
}
module_init(procfs1_init);
module_exit(procfs1_exit);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,98 @@
/*
* procfs2.c - 在/proc中创建一个 "文件"
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h> /* 使用 procfs 时的必要模块 */
#include <linux/uaccess.h> /* 引入 copy_from_user */
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define PROCFS_MAX_SIZE 1024
#define PROCFS_NAME "buffer1k"
/* 此结构保存有关 /proc 文件的信息 */
static struct proc_dir_entry *our_proc_file;
/* 用于存储此模块字符的缓冲区 */
static char procfs_buffer[PROCFS_MAX_SIZE];
/* 缓冲区的大小 */
static unsigned long procfs_buffer_size = 0;
/* 在读取 /proc 文件时调用该函数 */
static ssize_t procfile_read(struct file *file_pointer, char __user *buffer,
size_t buffer_length, loff_t *offset)
{
char s[13] = "HelloWorld!\n";
int len = sizeof(s);
ssize_t ret = len;
if (*offset >= len || copy_to_user(buffer, s, len)) {
pr_info("copy_to_user 执行结束 \n");
ret = 0;
} else {
pr_info("读取项目文件: %s\n", file_pointer->f_path.dentry->d_name.name);
*offset += len;
}
return ret;
}
/* 在写入 /proc 文件时调用该函数 */
static ssize_t procfile_write(struct file *file, const char __user *buff,
size_t len, loff_t *off)
{
procfs_buffer_size = len;
if (procfs_buffer_size > PROCFS_MAX_SIZE)
procfs_buffer_size = PROCFS_MAX_SIZE;
if (copy_from_user(procfs_buffer, buff, procfs_buffer_size))
return -EFAULT;
procfs_buffer[procfs_buffer_size & (PROCFS_MAX_SIZE - 1)] = '\0';
*off += procfs_buffer_size;
pr_info("写入项目文件 %s\n", procfs_buffer);
return procfs_buffer_size;
}
#ifdef HAVE_PROC_OPS
static const struct proc_ops proc_file_fops = {
.proc_read = procfile_read,
.proc_write = procfile_write,
};
#else
static const struct file_operations proc_file_fops = {
.read = procfile_read,
.write = procfile_write,
};
#endif
static int __init procfs2_init(void)
{
our_proc_file = proc_create(PROCFS_NAME, 0644, NULL, &proc_file_fops);
if (NULL == our_proc_file) {
pr_alert("错误: 无法初始化 /proc/%s\n", PROCFS_NAME);
return -ENOMEM;
}
pr_info("/proc/%s 已创建\n", PROCFS_NAME);
return 0;
}
static void __exit procfs2_exit(void)
{
proc_remove(our_proc_file);
pr_info("/proc/%s 已卸载\n", PROCFS_NAME);
}
module_init(procfs2_init);
module_exit(procfs2_exit);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,112 @@
/*
* procfs3.c
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
#include <linux/minmax.h>
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define PROCFS_MAX_SIZE 2048UL
#define PROCFS_ENTRY_FILENAME "buffer2k"
static struct proc_dir_entry *our_proc_file;
static char procfs_buffer[PROCFS_MAX_SIZE];
static unsigned long procfs_buffer_size = 0;
static ssize_t procfs_read(struct file *filp, char __user *buffer,
size_t length, loff_t *offset)
{
if (*offset || procfs_buffer_size == 0) {
pr_notice("procfs_read: 结束\n");
*offset = 0;
return 0;
}
procfs_buffer_size = min(procfs_buffer_size, length);
if (copy_to_user(buffer, procfs_buffer, procfs_buffer_size))
return -EFAULT;
*offset += procfs_buffer_size;
pr_notice("procfs_read: 读取 %lu 字节\n", procfs_buffer_size);
return procfs_buffer_size;
}
static ssize_t procfs_write(struct file *file, const char __user *buffer,
size_t len, loff_t *off)
{
procfs_buffer_size = min(PROCFS_MAX_SIZE, len);
if (copy_from_user(procfs_buffer, buffer, procfs_buffer_size))
return -EFAULT;
*off += procfs_buffer_size;
pr_notice("procfs_write: 写入 %lu 字节\n", procfs_buffer_size);
return procfs_buffer_size;
}
static int procfs_open(struct inode *inode, struct file *file)
{
try_module_get(THIS_MODULE);
pr_notice("procfs_open: 文件被打开, 目前文件正在被打开 -> %d \n", module_refcount(THIS_MODULE));
return 0;
}
static int procfs_close(struct inode *inode, struct file *file)
{
module_put(THIS_MODULE);
if (module_refcount(THIS_MODULE) == 0){
pr_notice("procfs_close: 文件已全部关闭, 可以安全卸载\n");
} else {
pr_notice("procfs_close: 文件剩余开启数为 -> %d \n", module_refcount(THIS_MODULE));
}
return 0;
}
#ifdef HAVE_PROC_OPS
static struct proc_ops file_ops_4_our_proc_file = {
.proc_read = procfs_read,
.proc_write = procfs_write,
.proc_open = procfs_open,
.proc_release = procfs_close,
};
#else
static const struct file_operations file_ops_4_our_proc_file = {
.read = procfs_read,
.write = procfs_write,
.open = procfs_open,
.release = procfs_close,
};
#endif
static int __init procfs3_init(void)
{
our_proc_file = proc_create(PROCFS_ENTRY_FILENAME, 0644, NULL,
&file_ops_4_our_proc_file);
if (our_proc_file == NULL) {
pr_notice("错误: 无法初始化 /proc/%s\n",
PROCFS_ENTRY_FILENAME);
return -ENOMEM;
}
proc_set_size(our_proc_file, 80);
proc_set_user(our_proc_file, GLOBAL_ROOT_UID, GLOBAL_ROOT_GID);
pr_notice("/proc/%s 已创建\n", PROCFS_ENTRY_FILENAME);
return 0;
}
static void __exit procfs3_exit(void)
{
remove_proc_entry(PROCFS_ENTRY_FILENAME, NULL);
pr_notice("/proc/%s 已卸载\n", PROCFS_ENTRY_FILENAME);
}
module_init(procfs3_init);
module_exit(procfs3_exit);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,114 @@
/*
* procfs4.c - 创建一个使用seq_file库来管理的/proc文件
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h> /* 引入 seq_file */
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define PROC_NAME "iter"
/* 这个函数在序列开始时被调用, 即:
* - 第一次读取 /proc 文件时
* - 或在调用 stop 函数后(序列结束时)
*/
static void *my_seq_start(struct seq_file *s, loff_t *pos)
{
static unsigned long counter = 0;
/* 是否开始一个新的序列? */
if (*pos == 0) {
/* yes => 返回非 NULL 值以开始序列 */
return &counter;
}
/* no => 这是序列的结束,返回 NULL 以停止读取 */
*pos = 0;
return NULL;
}
/* 这个函数在序列开始后被调用。它会被调用直到返回 NULL(序列结束) */
static void *my_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
unsigned long *tmp_v = (unsigned long *)v;
(*tmp_v)++;
(*pos)++;
return NULL;
}
/* 这个函数在序列结束时被调用 */
static void my_seq_stop(struct seq_file *s, void *v)
{
/* 无需操作,因为我们在 start() 中使用了静态值 */
}
/* 这个函数在序列的每一步中被调用 */
static int my_seq_show(struct seq_file *s, void *v)
{
loff_t *spos = (loff_t *)v;
seq_printf(s, "%Ld\n", *spos);
return 0;
}
/* 这个结构体包含了管理序列的函数 */
static struct seq_operations my_seq_ops = {
.start = my_seq_start,
.next = my_seq_next,
.stop = my_seq_stop,
.show = my_seq_show,
};
/* 这个函数在打开 /proc 文件时被调用 */
static int my_open(struct inode *inode, struct file *file)
{
return seq_open(file, &my_seq_ops);
};
/* 这个结构体包含了管理 /proc 文件的函数 */
#ifdef HAVE_PROC_OPS
static const struct proc_ops my_file_ops = {
.proc_open = my_open,
.proc_read = seq_read,
.proc_lseek = seq_lseek,
.proc_release = seq_release,
};
#else
static const struct file_operations my_file_ops = {
.open = my_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release,
};
#endif
static int __init procfs4_init(void)
{
struct proc_dir_entry *entry;
entry = proc_create(PROC_NAME, 0, NULL, &my_file_ops);
if (entry == NULL) {
pr_debug("错误:无法初始化 /proc/%s\n", PROC_NAME);
return -ENOMEM;
}
return 0;
}
static void __exit procfs4_exit(void)
{
remove_proc_entry(PROC_NAME, NULL);
pr_debug("/proc/%s 已移除\n", PROC_NAME);
}
module_init(procfs4_init);
module_exit(procfs4_exit);
MODULE_LICENSE("GPL");

14
示例/3-ioctl/Makefile Normal file
View File

@ -0,0 +1,14 @@
obj-m += ioctl.o
obj-m += chardev2.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
gcc userspace_ioctl.c -o 读写_用户空间
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm 读写_用户空间

37
示例/3-ioctl/chardev.h Normal file
View File

@ -0,0 +1,37 @@
/*
* chardev.h - 包含 ioctl 定义的头文件。
* 这些声明必须放在头文件中,因为它们需要同时被内核模块(在 chardev2.c 中)和调用 ioctl() 的进程(在 userspace_ioctl.c 中)所知晓。
*/
#ifndef CHARDEV_H
#define CHARDEV_H
#include <linux/ioctl.h>
/* 主设备号。由于 ioctl 需要知道主设备号,我们不能再依赖动态注册。 */
#define MAJOR_NUM 100
/* 设置设备驱动的消息 */
#define IOCTL_SET_MSG _IOW(MAJOR_NUM, 0, char *)
/* _IOW 表示我们正在创建一个 ioctl 命令,用于将信息从用户进程传递到内核模块。
* 第一个参数 MAJOR_NUM 是我们使用的主设备号。
* 第二个参数是命令的编号(可以有多个命令,含义不同)。
* 第三个参数是我们希望从进程传递到内核的数据类型。
*/
/* 获取设备驱动的消息 */
#define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *)
/* 此 IOCTL 用于输出,获取设备驱动的消息。
* 需要一个缓冲区来存放消息,该缓冲区由进程分配。
*/
/* 获取消息的第 n 个字节 */
#define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int)
/* 此 IOCTL 用于输入和输出。它从用户获取一个数字 n,并返回消息的第 n 个字节。
*/
/* 设备文件的名称 */
#define DEVICE_FILE_NAME "char_dev"
#define DEVICE_PATH "/dev/char_dev"
#endif

207
示例/3-ioctl/chardev2.c Normal file
View File

@ -0,0 +1,207 @@
/*
* chardev2.c - 创建一个输入/输出符的设备
*/
#include <linux/atomic.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/uaccess.h> /* 引入 get_user 和 put_user */
#include <linux/version.h>
#include <asm/errno.h>
#include "chardev.h"
#define SUCCESS 0
#define DEVICE_NAME "char_dev"
#define BUF_LEN 80
enum {
CDEV_NOT_USED = 0,
CDEV_EXCLUSIVE_OPEN = 1,
};
/* 设备当前是否已打开?用于防止对同一设备的并发访问 */
static atomic_t already_open = ATOMIC_INIT(CDEV_NOT_USED);
/* 设备在被请求时返回的消息 */
static char message[BUF_LEN + 1];
static struct class *cls;
/* 当进程尝试打开设备文件时调用此函数 */
static int device_open(struct inode *inode, struct file *file)
{
pr_info("[device_open]打开: (%p)\n", file);
try_module_get(THIS_MODULE);
return SUCCESS;
}
static int device_release(struct inode *inode, struct file *file)
{
pr_info("[device_release]释放: (%p,%p)\n", inode, file);
module_put(THIS_MODULE);
return SUCCESS;
}
/* 打开设备文件的进程尝试从中读取数据时调用此函数 */
static ssize_t device_read(struct file *file, /* 参见include/linux/fs.h */
char __user *buffer, /* 要填充的用户缓冲区 */
size_t length, /* 缓冲区的长度 */
loff_t *offset)
{
/* 实际写入到缓冲区的字节数 */
int bytes_read = 0;
/* 读取消息的进度指针,如果消息比缓冲区大,这很有用 */
const char *message_ptr = message;
if (!*(message_ptr + *offset)) { /* 到达消息末尾 */
*offset = 0; /* 重置偏移量 */
return 0; /* 表示文件结束 */
}
message_ptr += *offset;
/* 实际将数据写入缓冲区 */
while (length && *message_ptr) {
/* 由于缓冲区在用户数据段而非内核数据段,需要使用 put_user 将数据从内核复制到用户空间 */
put_user(*(message_ptr++), buffer++);
length--;
bytes_read++;
}
pr_info("读取 %d 字节, %ld 字节剩余\n", bytes_read, length);
*offset += bytes_read;
/* 读取函数应返回实际写入缓冲区的字节数 */
return bytes_read;
}
/* 当有人尝试向我们的设备文件写入数据时调用此函数 */
static ssize_t device_write(struct file *file, const char __user *buffer,
size_t length, loff_t *offset)
{
int i;
pr_info("[device_write]写入: %p,%p,%ld", file, buffer, length);
/* 将数据从用户空间缓冲区读取到内核空间的 message 中 */
for (i = 0; i < length && i < BUF_LEN; i++)
get_user(message[i], buffer + i);
/* 返回实际写入 message 中的字符数 */
return i;
}
/* 当进程尝试对设备文件执行 ioctl 操作时调用此函数。除了 inode 和 file 结构外,我们还接收到两个额外的参数:ioctl 的编号和传递给 ioctl 的参数。
* 如果 ioctl 是写操作或读/写操作(即返回输出给调用进程),ioctl 调用将返回此函数的输出。
*/
static long
device_ioctl(struct file *file,
unsigned int ioctl_num, /* ioctl 操作编号 */
unsigned long ioctl_param) /* ioctl 参数 */
{
int i;
long ret = SUCCESS;
/* 不允许同时与两个进程交互 */
if (atomic_cmpxchg(&already_open, CDEV_NOT_USED, CDEV_EXCLUSIVE_OPEN))
return -EBUSY;
/* 根据 ioctl 操作编号进行切换 */
switch (ioctl_num) {
case IOCTL_SET_MSG: {
/* 接收一个用户空间的消息指针,并将其设置为设备的消息 */
char __user *tmp = (char __user *)ioctl_param;
char ch;
/* 查找消息的长度 */
get_user(ch, tmp);
for (i = 0; ch && i < BUF_LEN; i++, tmp++)
get_user(ch, tmp);
device_write(file, (char __user *)ioctl_param, i, NULL);
break;
}
case IOCTL_GET_MSG: {
loff_t offset = 0;
/* 将当前消息返回给调用进程,参数是一个指针,将数据填充到该指针指向的缓冲区中 */
i = device_read(file, (char __user *)ioctl_param, 99, &offset);
/* 在缓冲区末尾添加一个终止符 */
put_user('\0', (char __user *)ioctl_param + i);
break;
}
case IOCTL_GET_NTH_BYTE:
/* 这个 ioctl 同时用于输入(ioctl_param)和输出(函数的返回值) */
ret = (long)message[ioctl_param];
break;
}
/* 准备好接待下一个调用者 */
atomic_set(&already_open, CDEV_NOT_USED);
return ret;
}
/* 模块声明 */
/* 这个结构体将保存当进程对我们创建的设备进行操作时调用的函数。
* 由于指向这个结构体的指针被保存在设备表中,它不能是 init_module 的局部变量。
* NULL 表示未实现的函数。
*/
static struct file_operations fops = {
.read = device_read,
.write = device_write,
.unlocked_ioctl = device_ioctl,
.open = device_open,
.release = device_release, /* 类似 关闭`close` 操作 */
};
/* 初始化模块-注册字符设备 */
static int __init chardev2_init(void)
{
/* 注册字符设备(尝试一下) */
int ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &fops);
/* 出现负值代表有个错误 */
if (ret_val < 0) {
pr_alert("注册字符设备 %d 失败\n", ret_val);
return ret_val;
}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
cls = class_create(DEVICE_FILE_NAME);
#else
cls = class_create(THIS_MODULE, DEVICE_FILE_NAME);
#endif
device_create(cls, NULL, MKDEV(MAJOR_NUM, 0), NULL, DEVICE_FILE_NAME);
pr_info("设备创建于 /dev/%s\n", DEVICE_FILE_NAME);
return 0;
}
/* 清理模块 - 从/proc注销移除相关文件 */
static void __exit chardev2_exit(void)
{
device_destroy(cls, MKDEV(MAJOR_NUM, 0));
class_destroy(cls);
/* 注销设备 */
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
}
module_init(chardev2_init);
module_exit(chardev2_exit);
MODULE_LICENSE("GPL");

212
示例/3-ioctl/ioctl.c Normal file
View File

@ -0,0 +1,212 @@
/*
* ioctl.c
*/
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ioctl.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/version.h>
// 定义用于 ioctl 操作的数据结构
struct ioctl_arg {
unsigned int val;
};
// 定义 ioctl 操作的宏
/* Documentation/userspace-api/ioctl/ioctl-number.rst */
#define IOC_MAGIC '\x66' // 自定义的魔术数字,用于标识 ioctl 操作的类别
// 定义不同的 ioctl 操作
#define IOCTL_VALSET _IOW(IOC_MAGIC, 0, struct ioctl_arg) // 写操作,设置一个值
#define IOCTL_VALGET _IOR(IOC_MAGIC, 1, struct ioctl_arg) // 读操作,获取一个值
#define IOCTL_VALGET_NUM _IOR(IOC_MAGIC, 2, int) // 读操作,获取一个整数
#define IOCTL_VALSET_NUM _IOW(IOC_MAGIC, 3, int) // 写操作,设置一个整数
#define IOCTL_VAL_MAXNR 3 // 最大的 ioctl 命令号
#define DRIVER_NAME "ioctltest" // 驱动名称
// 定义一些全局变量
static unsigned int test_ioctl_major = 0; // 主设备号
static unsigned int num_of_dev = 1; // 设备数量
static struct cdev test_ioctl_cdev; // 字符设备结构体
static int ioctl_num = 0; // 用于存储整数值的全局变量
// 定义用于保存设备数据的结构体
struct test_ioctl_data {
unsigned char val; // 存储一个值
rwlock_t lock; // 读写锁,用于保护数据的并发访问
};
// 处理 ioctl 操作的函数
static long test_ioctl_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct test_ioctl_data *ioctl_data = filp->private_data; // 从文件私有数据中获取设备数据
int retval = 0; // 操作结果
unsigned char val;
struct ioctl_arg data;
memset(&data, 0, sizeof(data)); // 清空 data 结构体
switch (cmd) {
case IOCTL_VALSET:
// 处理 IOCTL_VALSET 命令,将用户空间的数据复制到内核空间
if (copy_from_user(&data, (int __user *)arg, sizeof(data))) {
retval = -EFAULT;
goto done;
}
pr_alert("IOCTL 设置值:%x .\n", data.val); // 打印设置的值
write_lock(&ioctl_data->lock); // 获取写锁
ioctl_data->val = data.val; // 更新设备数据
write_unlock(&ioctl_data->lock); // 释放写锁
break;
case IOCTL_VALGET:
// 处理 IOCTL_VALGET 命令,将内核空间的数据复制到用户空间
read_lock(&ioctl_data->lock); // 获取读锁
val = ioctl_data->val; // 读取设备数据
read_unlock(&ioctl_data->lock); // 释放读锁
data.val = val;
if (copy_to_user((int __user *)arg, &data, sizeof(data))) {
retval = -EFAULT; // 复制失败
goto done;
}
break;
case IOCTL_VALGET_NUM:
// 处理 IOCTL_VALGET_NUM 命令,将整数值返回给用户空间
retval = __put_user(ioctl_num, (int __user *)arg);
break;
case IOCTL_VALSET_NUM:
// 处理 IOCTL_VALSET_NUM 命令,从用户空间获取整数值
ioctl_num = arg;
break;
default:
retval = -ENOTTY; // 命令不被支持
}
done:
return retval; // 返回操作结果
}
// 处理文件读取操作的函数
static ssize_t test_ioctl_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct test_ioctl_data *ioctl_data = filp->private_data; // 从文件私有数据中获取设备数据
unsigned char val;
int retval;
int i = 0;
read_lock(&ioctl_data->lock); // 获取读锁
val = ioctl_data->val; // 读取设备数据
read_unlock(&ioctl_data->lock); // 释放读锁
// 将数据从内核空间复制到用户空间
for (; i < count; i++) {
if (copy_to_user(&buf[i], &val, 1)) {
retval = -EFAULT; // 复制失败
goto out;
}
}
retval = count; // 成功读取的字节数
out:
return retval; // 返回读取结果
}
// 处理文件关闭操作的函数
static int test_ioctl_close(struct inode *inode, struct file *filp)
{
pr_alert("%s 呼叫\n", __func__); // 打印函数名
if (filp->private_data) {
kfree(filp->private_data); // 释放内存
filp->private_data = NULL;
}
return 0; // 成功关闭文件
}
// 处理文件打开操作的函数
static int test_ioctl_open(struct inode *inode, struct file *filp)
{
struct test_ioctl_data *ioctl_data;
pr_alert("%s 呼叫\n", __func__); // 打印函数名
ioctl_data = kmalloc(sizeof(struct test_ioctl_data), GFP_KERNEL); // 分配内存
if (ioctl_data == NULL)
return -ENOMEM; // 内存分配失败
rwlock_init(&ioctl_data->lock); // 初始化读写锁
ioctl_data->val = 0xFF; // 初始化设备数据
filp->private_data = ioctl_data; // 将设备数据指针保存到文件私有数据中
return 0;
}
// 文件操作结构体,定义了设备文件的操作函数
static struct file_operations fops = {
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 4, 0)
.owner = THIS_MODULE, // 设置模块所有者
#endif
.open = test_ioctl_open, // 打开文件操作函数
.release = test_ioctl_close, // 关闭文件操作函数
.read = test_ioctl_read, // 读取文件操作函数
.unlocked_ioctl = test_ioctl_ioctl, // ioctl 操作函数
};
// 模块初始化函数
static int __init ioctl_init(void)
{
dev_t dev;
int alloc_ret = -1;
int cdev_ret = -1;
// 分配字符设备号
alloc_ret = alloc_chrdev_region(&dev, 0, num_of_dev, DRIVER_NAME);
if (alloc_ret)
goto error; // 分配失败,跳转到错误处理
test_ioctl_major = MAJOR(dev); // 获取主设备号
cdev_init(&test_ioctl_cdev, &fops); // 初始化字符设备结构体
cdev_ret = cdev_add(&test_ioctl_cdev, dev, num_of_dev); // 添加字符设备
if (cdev_ret)
goto error; // 添加失败,跳转到错误处理
pr_alert("%s 驱动(主编号: %d) 已安装\n", DRIVER_NAME,
test_ioctl_major); // 打印安装成功信息
return 0;
error:
if (cdev_ret == 0)
cdev_del(&test_ioctl_cdev); // 释放字符设备
if (alloc_ret == 0)
unregister_chrdev_region(dev, num_of_dev); // 释放设备号
return -1;
}
// 模块卸载函数
static void __exit ioctl_exit(void)
{
dev_t dev = MKDEV(test_ioctl_major, 0); // 创建 dev_t 变量
cdev_del(&test_ioctl_cdev); // 删除字符设备
unregister_chrdev_region(dev, num_of_dev); // 释放字符设备号
pr_alert("%s 模块卸载 \n", DRIVER_NAME); // 打印卸载信息
}
module_init(ioctl_init);
module_exit(ioctl_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("这是 ioctl 测试模块");

View File

@ -0,0 +1,103 @@
/* userspace_ioctl.c - 用于通过 ioctl 控制内核模块的进程
*
* 之前我们可以使用 cat 进行输入和输出,但现在需要使用 ioctl,这要求我们编写自己的进程。
*/
/* 包含设备特定的信息,如 ioctl 编号和主设备号。 */
#include "./chardev.h"
#include <stdio.h> /* 标准输入/输出 */
#include <fcntl.h> /* 打开文件 */
#include <unistd.h> /* 关闭文件 */
#include <stdlib.h> /* 退出程序 */
#include <sys/ioctl.h> /* ioctl 调用 */
/* ioctl 调用的函数 */
int ioctl_set_msg(int file_desc, char *message)
{
int ret_val;
ret_val = ioctl(file_desc, IOCTL_SET_MSG, message);
if (ret_val < 0) {
printf("ioctl_set_msg 失败: %d\n", ret_val);
}
return ret_val;
}
int ioctl_get_msg(int file_desc)
{
int ret_val;
char message[100] = { 0 };
/* 警告 - 这里存在风险,因为我们没有告知内核写入的缓冲区长度,可能会导致缓冲区溢出。在实际生产程序中,
* 我们应该使用两个 ioctl 调用 - 一个告知内核缓冲区长度,另一个提供要填充的缓冲区。
*/
ret_val = ioctl(file_desc, IOCTL_GET_MSG, message);
if (ret_val < 0) {
printf("ioctl_get_msg 失败: %d\n", ret_val);
}
printf("获取的消息: %s", message);
return ret_val;
}
/* 注意:这种按字节读取消息的方式无法正常读取中文 */
int ioctl_get_nth_byte(int file_desc)
{
int i, c, err;
printf("按字节获取到的消息: ");
err = 0;
i = 0;
do {
c = ioctl(file_desc, IOCTL_GET_NTH_BYTE, i++);
if (c < 0) {
err = 1;
continue;
}
putchar(c);
} while (c != 0);
if (err == 1){
printf("(非ascii字符已被省略)\n\n");
}
return 0;
}
/* 主函数 - 调用 ioctl 函数 */
int main(void)
{
int file_desc, ret_val;
char *msg = "通过 ioctl 传递的消息\n";
/* char *msg = "qwer1234\n"; */
file_desc = open(DEVICE_PATH, O_RDWR);
if (file_desc < 0) {
printf("无法打开设备文件: %s, 错误: %d\n", DEVICE_PATH, file_desc);
exit(EXIT_FAILURE);
}
ret_val = ioctl_set_msg(file_desc, msg);
if (ret_val)
goto error;
ret_val = ioctl_get_nth_byte(file_desc);
if (ret_val)
goto error;
ret_val = ioctl_get_msg(file_desc);
if (ret_val)
goto error;
close(file_desc);
return 0;
error:
close(file_desc);
exit(EXIT_FAILURE);
}

View File

@ -0,0 +1,11 @@
obj-m += syscall-steal_loong64.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

View File

@ -0,0 +1,275 @@
/*
* syscall-steal.c
* 系统调用 "监视" 示例(龙架构专用)。
* 通过更改 fcsr 寄存器中的第 16 位,在处理器级别禁用页面保护
*/
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h> /* 包含一些参数 */
#include <linux/unistd.h> /* 系统调用列表 */
#include <linux/cred.h> /* 引入 current_uid() */
#include <linux/uidgid.h> /* 引入 __kuid_val() */
#include <linux/version.h>
/* 根据本程序的需求我们需要了解当前用户是谁 */
#include <linux/sched.h>
#include <linux/uaccess.h>
/* 访问“sys_call_table”的方式随着内核内部的变化而变化。
* 内核版本 <= v5.4 : 手动符号查找
* 内核版本 == v5.5 或 v5.6 : 使用 kallsyms_lookup_name()
* 内核版本 >= v5.7 : Kprobes 或特定的内核模块参数
*/
/* 在 Linux v5.11 及以上版本中,内核调用 ksys_close() 的方式被移除 */
#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 7, 0))
#if LINUX_VERSION_CODE <= KERNEL_VERSION(5, 4, 0)
#define HAVE_KSYS_CLOSE 1
#include <linux/syscalls.h> /* 引入 ksys_close() */
#else
#include <linux/kallsyms.h> /* 引入 kallsyms_lookup_name */
#endif
#else
#if defined(CONFIG_KPROBES)
#define HAVE_KPROBES 1
#if defined(CONFIG_X86_64)
/* 如果你尝试使用系统调用表来拦截系统调用但未成功,你可以尝试使用 Kprobes 来拦截系统调用。
* 将 USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL 设置为 1 以注册一个预处理程序在系统调用之前。
*/
#define USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL 0
#endif
#include <linux/kprobes.h>
#else
#define HAVE_PARAM 1
#include <linux/kallsyms.h> /* 引入 sprint_symbol */
/* sys_call_table 的地址可以通过查看 "/boot/System.map" 或 "/proc/kallsyms" 获得。
* 当内核版本为 v5.7 及以上时,如果没有 CONFIG_KPROBES,你可以输入参数或者模块会查找所有内存。
*/
static unsigned long sym = 0;
module_param(sym, ulong, 0644);
#endif /* CONFIG_KPROBES */
#endif /* 版本 < v5.7 */
/* 我们想要监视的 UID - 将从命令行填充。 */
static uid_t uid = -1;
module_param(uid, int, 0644);
#if USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL
/* syscall_sym 是要监视的系统调用的符号名称。默认值为"__x64_sys_openat"
* 可以通过模块参数进行更改。你可以在 /proc/kallsyms 中查找系统调用的符号名称。
*/
static char *syscall_sym = "__x64_sys_openat";
module_param(syscall_sym, charp, 0644);
static int sys_call_kprobe_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
if (__kuid_val(current_uid()) != uid) {
return 0;
}
pr_info("%s 被 %d 调用\n", syscall_sym, uid);
return 0;
}
static struct kprobe syscall_kprobe = {
.symbol_name = "__x64_sys_openat",
.pre_handler = sys_call_kprobe_pre_handler,
};
#else
static unsigned long **sys_call_table_stolen;
/* 指向原始系统调用的指针。我们保留这个指针,而不是直接调用原始函数 (sys_openat),是因为可能有其他模块在我们之前已经替换了系统调用。
* 注意,这不是 100% 安全的,因为如果其他模块在我们之前替换了 sys_openat,那么当我们插入时,我们将调用那个模块中的函数 -
* 而且那个模块可能在我们之前已经被移除。
*
* 另一个原因是我们无法获取 sys_openat。它是一个静态变量,因此没有被导出。
*/
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
static asmlinkage long (*original_call)(const struct pt_regs *);
#else
static asmlinkage long (*original_call)(int, const char __user *, int, umode_t);
#endif
/* 我们将用来替换 sys_openat (即调用 open 系统调用时调用的函数)的函数
* 为了找到准确的原型,包含参数的数量和类型,我们首先找到原始函数(它位于 fs/open.c 文件中)
*
* 理论上,这意味着我们绑定于当前版本的内核。实际上,系统调用几乎不会改变
* (因为系统调用是内核和进程之间的接口,如果系统调用发生变化,会引起混乱并需要重新编译程序)
*/
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
static asmlinkage long our_sys_openat(const struct pt_regs *regs)
#else
static asmlinkage long our_sys_openat(int dfd, const char __user *filename,
int flags, umode_t mode)
#endif
{
int i = 0;
char ch;
if (__kuid_val(current_uid()) != uid)
goto orig_call;
/* 报告相关文件 */
pr_info("Opened file by %d: ", uid);
do {
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
get_user(ch, (char __user *)regs->si + i);
#else
get_user(ch, (char __user *)filename + i);
#endif
i++;
pr_info("%c", ch);
} while (ch != 0);
pr_info("\n");
orig_call:
/* 调用原始的sys_openat-否则,我们将失去打开文件的能力 */
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
return original_call(regs);
#else
return original_call(dfd, filename, flags, mode);
#endif
}
static unsigned long **acquire_sys_call_table(void)
{
#ifdef HAVE_KSYS_CLOSE
unsigned long int offset = PAGE_OFFSET;
unsigned long **sct;
while (offset < ULLONG_MAX) {
sct = (unsigned long **)offset;
if (sct[__NR_close] == (unsigned long *)ksys_close)
return sct;
offset += sizeof(void *);
}
return NULL;
#endif
#ifdef HAVE_PARAM
const char sct_name[15] = "sys_call_table";
char symbol[40] = { 0 };
if (sym == 0) {
pr_alert("对于 Linux v5.7 以上版本,Kprobes 是获取符号的首选方法\n");
pr_info("如果没有 Kprobes,则必须指定 sys_call_table 符号的地址\n");
pr_info("通过 /boot/System.map 或 /proc/kallsyms 将包含所有符号地址的符号转换为sym参数\n");
return NULL;
}
sprint_symbol(symbol, sym);
if (!strncmp(sct_name, symbol, sizeof(sct_name) - 1))
return (unsigned long **)sym;
return NULL;
#endif
#ifdef HAVE_KPROBES
unsigned long (*kallsyms_lookup_name)(const char *name);
struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name",
};
if (register_kprobe(&kp) < 0)
return NULL;
kallsyms_lookup_name = (unsigned long (*)(const char *name))kp.addr;
unregister_kprobe(&kp);
#endif
return (unsigned long **)kallsyms_lookup_name("sys_call_table");
}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 3, 0)
static inline void __write_fcsr(unsigned long fcsr)
{
asm volatile("mov %0,%%fcsr" : "+r"(fcsr) : : "memory");
}
#else
#define __write_fcsr write_fcsr
#endif
static void enable_write_protection(void)
{
unsigned long fcsr = read_fcsr();
set_bit(16, &fcsr);
__write_fcsr(fcsr);
}
static void disable_write_protection(void)
{
unsigned long fcsr = read_fcsr();
clear_bit(16, &fcsr);
__write_fcsr(fcsr);
}
#endif
static int __init syscall_steal_start(void)
{
#if USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL
int err;
/* use symbol name from the module parameter */
syscall_kprobe.symbol_name = syscall_sym;
err = register_kprobe(&syscall_kprobe);
if (err) {
pr_err("register_kprobe() 在 %s 失败: %d\n", syscall_sym, err);
pr_err("请检查 “syscall_sym ”参数中的符号名称\n");
return err;
}
#else
if (!(sys_call_table_stolen = acquire_sys_call_table()))
return -1;
disable_write_protection();
/* 跟踪原始的打开函数 */
original_call = (void *)sys_call_table_stolen[__NR_openat];
/* 请改用我们的openat函数 */
sys_call_table_stolen[__NR_openat] = (unsigned long *)our_sys_openat;
enable_write_protection();
#endif
pr_info("监视 UID:%d\n", uid);
return 0;
}
static void __exit syscall_steal_end(void)
{
#if USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL
unregister_kprobe(&syscall_kprobe);
#else
if (!sys_call_table_stolen)
return;
/* 将系统调用恢复正常 */
if (sys_call_table_stolen[__NR_openat] != (unsigned long *)our_sys_openat) {
pr_alert("发现系统调用被其他人修改");
pr_alert("系统可能处于不稳定状态");
}
disable_write_protection();
sys_call_table_stolen[__NR_openat] = (unsigned long *)original_call;
enable_write_protection();
#endif
msleep(2000);
}
module_init(syscall_steal_start);
module_exit(syscall_steal_end);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,275 @@
/*
* syscall-steal.c
* 系统调用 "监视" 示例。
* 通过更改 cr0 寄存器中的第 16 位,在处理器级别禁用页面保护(可能与 Intel 有关)
*/
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h> /* 包含一些参数 */
#include <linux/unistd.h> /* 系统调用列表 */
#include <linux/cred.h> /* 引入 current_uid() */
#include <linux/uidgid.h> /* 引入 __kuid_val() */
#include <linux/version.h>
/* 根据本程序的需求我们需要了解当前用户是谁 */
#include <linux/sched.h>
#include <linux/uaccess.h>
/* 访问“sys_call_table”的方式随着内核内部的变化而变化。
* 内核版本 <= v5.4 : 手动符号查找
* 内核版本 == v5.5 或 v5.6 : 使用 kallsyms_lookup_name()
* 内核版本 >= v5.7 : Kprobes 或特定的内核模块参数
*/
/* 在 Linux v5.11 及以上版本中,内核调用 ksys_close() 的方式被移除 */
#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 7, 0))
#if LINUX_VERSION_CODE <= KERNEL_VERSION(5, 4, 0)
#define HAVE_KSYS_CLOSE 1
#include <linux/syscalls.h> /* 引入 ksys_close() */
#else
#include <linux/kallsyms.h> /* 引入 kallsyms_lookup_name */
#endif
#else
#if defined(CONFIG_KPROBES)
#define HAVE_KPROBES 1
#if defined(CONFIG_X86_64)
/* 如果你尝试使用系统调用表来拦截系统调用但未成功,你可以尝试使用 Kprobes 来拦截系统调用。
* 将 USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL 设置为 1 以注册一个预处理程序在系统调用之前。
*/
#define USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL 0
#endif
#include <linux/kprobes.h>
#else
#define HAVE_PARAM 1
#include <linux/kallsyms.h> /* 引入 sprint_symbol */
/* sys_call_table 的地址可以通过查看 "/boot/System.map" 或 "/proc/kallsyms" 获得。
* 当内核版本为 v5.7 及以上时,如果没有 CONFIG_KPROBES,你可以输入参数或者模块会查找所有内存。
*/
static unsigned long sym = 0;
module_param(sym, ulong, 0644);
#endif /* CONFIG_KPROBES */
#endif /* 版本 < v5.7 */
/* 我们想要监视的 UID - 将从命令行填充。 */
static uid_t uid = -1;
module_param(uid, int, 0644);
#if USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL
/* syscall_sym 是要监视的系统调用的符号名称。默认值为"__x64_sys_openat"
* 可以通过模块参数进行更改。你可以在 /proc/kallsyms 中查找系统调用的符号名称。
*/
static char *syscall_sym = "__x64_sys_openat";
module_param(syscall_sym, charp, 0644);
static int sys_call_kprobe_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
if (__kuid_val(current_uid()) != uid) {
return 0;
}
pr_info("%s 被 %d 调用\n", syscall_sym, uid);
return 0;
}
static struct kprobe syscall_kprobe = {
.symbol_name = "__x64_sys_openat",
.pre_handler = sys_call_kprobe_pre_handler,
};
#else
static unsigned long **sys_call_table_stolen;
/* 指向原始系统调用的指针。我们保留这个指针,而不是直接调用原始函数 (sys_openat),是因为可能有其他模块在我们之前已经替换了系统调用。
* 注意,这不是 100% 安全的,因为如果其他模块在我们之前替换了 sys_openat,那么当我们插入时,我们将调用那个模块中的函数 -
* 而且那个模块可能在我们之前已经被移除。
*
* 另一个原因是我们无法获取 sys_openat。它是一个静态变量,因此没有被导出。
*/
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
static asmlinkage long (*original_call)(const struct pt_regs *);
#else
static asmlinkage long (*original_call)(int, const char __user *, int, umode_t);
#endif
/* 我们将用来替换 sys_openat (即调用 open 系统调用时调用的函数)的函数
* 为了找到准确的原型,包含参数的数量和类型,我们首先找到原始函数(它位于 fs/open.c 文件中)
*
* 理论上,这意味着我们绑定于当前版本的内核。实际上,系统调用几乎不会改变
* (因为系统调用是内核和进程之间的接口,如果系统调用发生变化,会引起混乱并需要重新编译程序)
*/
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
static asmlinkage long our_sys_openat(const struct pt_regs *regs)
#else
static asmlinkage long our_sys_openat(int dfd, const char __user *filename,
int flags, umode_t mode)
#endif
{
int i = 0;
char ch;
if (__kuid_val(current_uid()) != uid)
goto orig_call;
/* 报告相关文件 */
pr_info("Opened file by %d: ", uid);
do {
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
get_user(ch, (char __user *)regs->si + i);
#else
get_user(ch, (char __user *)filename + i);
#endif
i++;
pr_info("%c", ch);
} while (ch != 0);
pr_info("\n");
orig_call:
/* 调用原始的sys_openat-否则,我们将失去打开文件的能力 */
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
return original_call(regs);
#else
return original_call(dfd, filename, flags, mode);
#endif
}
static unsigned long **acquire_sys_call_table(void)
{
#ifdef HAVE_KSYS_CLOSE
unsigned long int offset = PAGE_OFFSET;
unsigned long **sct;
while (offset < ULLONG_MAX) {
sct = (unsigned long **)offset;
if (sct[__NR_close] == (unsigned long *)ksys_close)
return sct;
offset += sizeof(void *);
}
return NULL;
#endif
#ifdef HAVE_PARAM
const char sct_name[15] = "sys_call_table";
char symbol[40] = { 0 };
if (sym == 0) {
pr_alert("对于 Linux v5.7 以上版本,Kprobes 是获取符号的首选方法\n");
pr_info("如果没有 Kprobes,则必须指定 sys_call_table 符号的地址\n");
pr_info("通过 /boot/System.map 或 /proc/kallsyms 将包含所有符号地址的符号转换为sym参数\n");
return NULL;
}
sprint_symbol(symbol, sym);
if (!strncmp(sct_name, symbol, sizeof(sct_name) - 1))
return (unsigned long **)sym;
return NULL;
#endif
#ifdef HAVE_KPROBES
unsigned long (*kallsyms_lookup_name)(const char *name);
struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name",
};
if (register_kprobe(&kp) < 0)
return NULL;
kallsyms_lookup_name = (unsigned long (*)(const char *name))kp.addr;
unregister_kprobe(&kp);
#endif
return (unsigned long **)kallsyms_lookup_name("sys_call_table");
}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 3, 0)
static inline void __write_cr0(unsigned long cr0)
{
asm volatile("mov %0,%%cr0" : "+r"(cr0) : : "memory");
}
#else
#define __write_cr0 write_cr0
#endif
static void enable_write_protection(void)
{
unsigned long cr0 = read_cr0();
set_bit(16, &cr0);
__write_cr0(cr0);
}
static void disable_write_protection(void)
{
unsigned long cr0 = read_cr0();
clear_bit(16, &cr0);
__write_cr0(cr0);
}
#endif
static int __init syscall_steal_start(void)
{
#if USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL
int err;
/* use symbol name from the module parameter */
syscall_kprobe.symbol_name = syscall_sym;
err = register_kprobe(&syscall_kprobe);
if (err) {
pr_err("register_kprobe() 在 %s 失败: %d\n", syscall_sym, err);
pr_err("请检查 “syscall_sym ”参数中的符号名称\n");
return err;
}
#else
if (!(sys_call_table_stolen = acquire_sys_call_table()))
return -1;
disable_write_protection();
/* 跟踪原始的打开函数 */
original_call = (void *)sys_call_table_stolen[__NR_openat];
/* 请改用我们的openat函数 */
sys_call_table_stolen[__NR_openat] = (unsigned long *)our_sys_openat;
enable_write_protection();
#endif
pr_info("监视 UID:%d\n", uid);
return 0;
}
static void __exit syscall_steal_end(void)
{
#if USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL
unregister_kprobe(&syscall_kprobe);
#else
if (!sys_call_table_stolen)
return;
/* 将系统调用恢复正常 */
if (sys_call_table_stolen[__NR_openat] != (unsigned long *)our_sys_openat) {
pr_alert("发现系统调用被其他人修改");
pr_alert("系统可能处于不稳定状态");
}
disable_write_protection();
sys_call_table_stolen[__NR_openat] = (unsigned long *)original_call;
enable_write_protection();
#endif
msleep(2000);
}
module_init(syscall_steal_start);
module_exit(syscall_steal_end);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,13 @@
obj-m += sleep.o
obj-m += completions.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
gcc cat_nonblock.c -o 查看文件
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm 查看文件

View File

@ -0,0 +1,59 @@
/*
* cat_nonblock.c - 打开一个文件并显示其内容,但不等待输入而是直接退出。
*/
#include <errno.h> /* 用于 errno */
#include <fcntl.h> /* 用于 open */
#include <stdio.h> /* 标准 I/O */
#include <stdlib.h> /* 用于 exit */
#include <unistd.h> /* 用于 read */
#define MAX_BYTES 1024 * 4 /* 最大读取字节数 */
int main(int argc, char *argv[])
{
int fd; /* 要读取的文件的文件描述符 */
size_t bytes; /* 读取的字节数 */
char buffer[MAX_BYTES]; /* 存储读取字节的缓冲区 */
/* 用法说明 */
if (argc != 2) {
printf("用法: %s <文件名>\n", argv[0]);
puts("读取文件的内容,但不等待输入");
exit(-1);
}
/* 以非阻塞模式打开文件 */
fd = open(argv[1], O_RDONLY | O_NONBLOCK);
/* 如果打开失败 */
if (fd == -1) {
puts(errno == EAGAIN ? "打开会阻塞" : "打开失败");
exit(-1);
}
/* 读取文件并输出其内容 */
do {
/* 从文件中读取字符 */
bytes = read(fd, buffer, MAX_BYTES);
/* 如果发生错误,报告错误并退出 */
if (bytes == -1) {
if (errno == EAGAIN)
puts("正常情况下会阻塞,但你告诉我不阻塞");
else
puts("读取错误");
exit(-1);
}
/* 打印读取的字符 */
if (bytes > 0) {
for (int i = 0; i < bytes; i++)
putchar(buffer[i]);
}
/* 当没有错误且文件未结束时继续 */
} while (bytes > 0);
return 0;
}

View File

@ -0,0 +1,89 @@
/*
* completions.c 完成情况示例程序
*/
#include <linux/completion.h>
#include <linux/err.h> /* 引入 IS_ERR() */
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/version.h>
static struct completion crank_comp; /* 定义完成情况 crank_comp */
static struct completion flywheel_comp; /* 定义完成情况 flywheel_comp */
/* crank线程函数 */
static int machine_crank_thread(void *arg)
{
pr_info("crank转动\n");
complete_all(&crank_comp); /* 设置 crank_comp 完成情况 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 17, 0)
kthread_complete_and_exit(&crank_comp, 0); /* 内核版本 >= 5.17,使用 kthread 完成并退出 */
#else
complete_and_exit(&crank_comp, 0); /* 否则,使用旧版本接口完成并退出 */
#endif
}
/* flywheel启动线程函数 */
static int machine_flywheel_spinup_thread(void *arg)
{
wait_for_completion(&crank_comp); /* 等待 crank_comp 完成情况 */
pr_info("flywheel启动\n");
complete_all(&flywheel_comp); /* 设置 flywheel_comp 完成情况 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 17, 0)
kthread_complete_and_exit(&flywheel_comp, 0); /* 内核版本 >= 5.17,使用 kthread 完成并退出 */
#else
complete_and_exit(&flywheel_comp, 0); /* 否则,使用旧版本接口完成并退出 */
#endif
}
static int __init completions_init(void)
{
struct task_struct *crank_thread; /* crank线程 */
struct task_struct *flywheel_thread; /* flywheel线程 */
pr_info("完成情况示例加载\n");
init_completion(&crank_comp); /* 初始化 crank_comp 完成情况 */
init_completion(&flywheel_comp); /* 初始化 flywheel_comp 完成情况 */
crank_thread = kthread_create(machine_crank_thread, NULL, "KThread Crank"); /* 创建crank线程 */
if (IS_ERR(crank_thread))
goto ERROR_THREAD_1;
flywheel_thread = kthread_create(machine_flywheel_spinup_thread, NULL,
"KThread Flywheel"); /* 创建flywheel线程 */
if (IS_ERR(flywheel_thread))
goto ERROR_THREAD_2;
wake_up_process(flywheel_thread); /* 启动flywheel线程 */
wake_up_process(crank_thread); /* 启动crank线程 */
return 0;
ERROR_THREAD_2:
kthread_stop(crank_thread); /* 停止crank线程 */
ERROR_THREAD_1:
return -1;
}
static void __exit completions_exit(void)
{
wait_for_completion(&crank_comp); /* 等待crank线程完成 */
wait_for_completion(&flywheel_comp); /* 等待flywheel线程完成 */
pr_info("完成情况示例卸载\n");
}
module_init(completions_init);
module_exit(completions_exit);
MODULE_DESCRIPTION("完成情况示例");
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,191 @@
/*
* sleep.c - 创建一个 /proc 文件, 如果多个进程同时尝试打开该文件, 则除一个进程外, 其他进程都进入休眠状态
*/
#include <linux/atomic.h>
#include <linux/fs.h>
#include <linux/kernel.h> /* 引入 sprintf() */
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/proc_fs.h>
#include <linux/types.h>
#include <linux/uaccess.h> /* 引入 get_user 和 put_user */
#include <linux/version.h>
#include <linux/wait.h> /* 用于让程序休眠和唤醒 */
#include <asm/current.h>
#include <asm/errno.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
/* 在这里我们保存最后接收到的消息, 以证明我们可以处理输入 */
#define MESSAGE_LENGTH 80
static char message[MESSAGE_LENGTH];
static struct proc_dir_entry *our_proc_file;
#define PROC_ENTRY_FILENAME "sleep"
/* 从文件"读取": 由于使用的是 file_operations (内核中定义文件操作的结构体),
* 其中不能使用procfs函数, 因此需要使用标准的文件操作函数(sprintf)来实现输出
*/
static ssize_t module_output(struct file *file, /* 参见 include/linux/fs.h */
char __user *buf, /* 将数据放入的缓冲区(在用户段) */
size_t len, /* 缓冲区的长度 */
loff_t *offset)
{
static int finished = 0;
int i;
char output_msg[MESSAGE_LENGTH + 30];
/* 返回 0 表示文件结束 */
if (finished) {
finished = 0;
return 0;
}
sprintf(output_msg, "最后输入:%s\n", message);
for (i = 0; i < len && output_msg[i]; i++)
put_user(output_msg[i], buf + i);
finished = 1;
return i; /* 返回"读取"的字节数 */
}
/* 当用户向 /proc 文件写入时,此函数接收输入 */
static ssize_t module_input(struct file *file, /* 文件本身 */
const char __user *buf, /* 输入缓冲区 */
size_t length, /* 缓冲区的长度 */
loff_t *offset) /* 文件的偏移 - 忽略 */
{
int i;
/* 将输入放入 Message 中,以便 module_output 可以在后面使用它 */
for (i = 0; i < MESSAGE_LENGTH - 1 && i < length; i++)
get_user(message[i], buf + i);
/* 标准的、以零终止的字符串 */
message[i] = '\0';
/* 返回使用的输入字符数 */
return i;
}
/* 如果文件当前被某人打开,则为 1 */
static atomic_t already_open = ATOMIC_INIT(0);
/* 等待我们文件的进程队列 */
static DECLARE_WAIT_QUEUE_HEAD(waitq);
/* 当 /proc 文件被打开时调用 */
static int module_open(struct inode *inode, struct file *file)
{
/* 尝试在不阻塞的情况下获取 */
if (!atomic_cmpxchg(&already_open, 0, 1)) {
/* 在不阻塞的情况下成功,允许访问 */
try_module_get(THIS_MODULE);
return 0;
}
/* 如果文件标志包含 O_NONBLOCK,意味着进程不希望等待文件。在这种情况下,因为文件已经被打开,
* 我们应该返回 -EAGAIN,表示“你必须再试一次”,而不是阻塞一个更愿意保持清醒的进程。
*/
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
/* 这是调用 try_module_get(THIS_MODULE) 的正确位置,因为如果进程在模块内部的循环中,内核模块不能被移除 */
try_module_get(THIS_MODULE);
while (atomic_cmpxchg(&already_open, 0, 1)) {
int i, is_sig = 0;
pr_info("阻塞等待中\n");
/* 这个函数会将当前进程(包括任何系统调用)挂起, 并在函数调用wake_up(&waitq)后恢复
* 向进程发送退出信号也可以使其恢复信号(例如 ctrl-c)。
*/
wait_event_interruptible(waitq, !atomic_read(&already_open));
/* 判断当前进程是否有待处理的非阻塞信号(主要用于在等待途中随时可以被中断) */
for (i = 0; i < _NSIG_WORDS && !is_sig; i++)
is_sig = current->pending.signal.sig[i] & ~current->blocked.sig[i];
if (is_sig) {
/* 对于那些被中断的打开操作,不会自动关闭。不在这里减少计数器,后续程序就无法按照预期将计数器降到零,
* 这将导致我们有一个永不消失的模块,只能通过重启机器来清除。
*/
pr_info("已被中断, 退出完成\n");
module_put(THIS_MODULE);
return -EINTR;
}
}
return 0; /* 允许访问 */
}
/* 当 /proc 文件被关闭时调用 */
static int module_close(struct inode *inode, struct file *file)
{
/* 将 already_open 设置为零,以便 waitq 中的一个进程可以将 already_open 设置回1,并打开文件。
* 所有进程将在 already_open 变回1时被唤醒,所以它们将重新进入睡眠状态
*/
atomic_set(&already_open, 0);
/* 唤醒 waitq 中的所有进程,如果有人在等待文件,他们可以获得它 */
wake_up(&waitq);
module_put(THIS_MODULE);
return 0; /* 成功 */
}
/* 注册为 /proc 文件的结构,包含指向所有相关函数的指针 */
/* 我们的 proc 文件的文件操作。这是我们放置所有处理对文件进行操作
* 时调用的函数指针的地方。NULL表示我们不想处理某些操作。
*/
#ifdef HAVE_PROC_OPS
static const struct proc_ops file_ops_4_our_proc_file = {
.proc_read = module_output, /* 从文件"读取" */
.proc_write = module_input, /* 向文件"写入" */
.proc_open = module_open, /* 当 /proc 文件被打开时调用 */
.proc_release = module_close, /* 当文件关闭时调用 */
.proc_lseek = noop_llseek, /* 返回 file->f_pos */
};
#else
static const struct file_operations file_ops_4_our_proc_file = {
.read = module_output,
.write = module_input,
.open = module_open,
.release = module_close,
.llseek = noop_llseek,
};
#endif
/* 初始化模块 - 注册 proc 文件 */
static int __init sleep_init(void)
{
our_proc_file =
proc_create(PROC_ENTRY_FILENAME, 0644, NULL, &file_ops_4_our_proc_file);
if (our_proc_file == NULL) {
pr_debug("错误: 无法初始化 /proc/%s\n", PROC_ENTRY_FILENAME);
return -ENOMEM;
}
proc_set_size(our_proc_file, 80);
proc_set_user(our_proc_file, GLOBAL_ROOT_UID, GLOBAL_ROOT_GID);
pr_info("/proc/%s 已创建\n", PROC_ENTRY_FILENAME);
return 0;
}
/* 清理 - 从 /proc 注销我们的文件。如果在 waitq 中仍有进程等待,
* 这可能会变得危险,因为它们在我们的打开函数中,而这个函数将被卸载。
* 我会在第十章中解释如何避免在这种情况下移除内核模块。
*/
static void __exit sleep_exit(void)
{
remove_proc_entry(PROC_ENTRY_FILENAME, NULL);
pr_debug("/proc/%s 已移除\n", PROC_ENTRY_FILENAME);
}
module_init(sleep_init);
module_exit(sleep_exit);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,14 @@
obj-m += example_mutex.o
obj-m += example_spinlock.o
obj-m += example_rwlock.o
obj-m += example_atomic.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

View File

@ -0,0 +1,76 @@
/*
* example_atomic.c
*/
#include <linux/atomic.h>
#include <linux/bitops.h>
#include <linux/module.h>
#include <linux/printk.h>
#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c"
#define BYTE_TO_BINARY(byte) \
((byte & 0x80) ? '1' : '0'), ((byte & 0x40) ? '1' : '0'), \
((byte & 0x20) ? '1' : '0'), ((byte & 0x10) ? '1' : '0'), \
((byte & 0x08) ? '1' : '0'), ((byte & 0x04) ? '1' : '0'), \
((byte & 0x02) ? '1' : '0'), ((byte & 0x01) ? '1' : '0')
/* 演示原子加法和减法操作 */
static void atomic_add_subtract(void)
{
atomic_t rimian;
atomic_t xunmi = ATOMIC_INIT(50);
/* 赋值为45 */
atomic_set(&rimian, 45);
/* 减去1(45-1=44) */
atomic_dec(&rimian);
/* 加上7(44+7=51) */
atomic_add(7, &rimian);
/* 加1(51+1=52) */
atomic_inc(&rimian);
/* 两个名字, 没有其他意义 */
pr_info("寻觅: %d, 日冕: %d\n", atomic_read(&xunmi),
atomic_read(&rimian));
}
/* 演示原子位操作 */
static void atomic_bitwise(void)
{
unsigned long word = 0;
pr_info("初始 0: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
set_bit(3, &word); // 设置第3位为1
set_bit(5, &word); // 设置第5位为1
pr_info("修正 1: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
clear_bit(5, &word); // 清除第5位
pr_info("修正 2: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
change_bit(3, &word); // 切换第3位的值
pr_info("修正 3: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
// 检查第3位,如果原本为1,则返回 `非预期值` 的输出, 如果不是1则将其设置为1
if (test_and_set_bit(3, &word))
pr_info("非预期值\n");
pr_info("修正 4: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
word = 255; // 设置word的所有位为1
pr_info("修正 5: " BYTE_TO_BINARY_PATTERN "\n", BYTE_TO_BINARY(word));
}
static int __init example_atomic_init(void)
{
pr_info("原子操作示例开始\n");
atomic_add_subtract();
atomic_bitwise();
return 0;
}
static void __exit example_atomic_exit(void)
{
pr_info("原子操作示例退出\n");
}
module_init(example_atomic_init);
module_exit(example_atomic_exit);
MODULE_DESCRIPTION("原子操作示例");
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,40 @@
/*
* example_mutex.c
*/
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/printk.h>
static DEFINE_MUTEX(mymutex);
static int __init example_mutex_init(void)
{
int ret;
pr_info("example_mutex 初始化\n");
ret = mutex_trylock(&mymutex);
if (ret != 0) {
pr_info("已锁定\n");
if (mutex_is_locked(&mymutex) == 0)
pr_info("锁定失败!\n");
mutex_unlock(&mymutex);
pr_info("已解锁\n");
} else
pr_info("锁定失败\n");
return 0;
}
static void __exit example_mutex_exit(void)
{
pr_info("example_mutex 卸载\n");
}
module_init(example_mutex_init);
module_exit(example_mutex_exit);
MODULE_DESCRIPTION("互斥锁示例");
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,55 @@
/*
* example_rwlock.c
*/
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/rwlock.h>
static DEFINE_RWLOCK(myrwlock);
static void example_read_lock(void)
{
unsigned long flags;
read_lock_irqsave(&myrwlock, flags);
pr_info("读锁已加锁\n");
/* 从某处读取数据 */
read_unlock_irqrestore(&myrwlock, flags);
pr_info("读锁已解锁\n");
}
static void example_write_lock(void)
{
unsigned long flags;
write_lock_irqsave(&myrwlock, flags);
pr_info("写锁已加锁\n");
/* 向某处写入数据 */
write_unlock_irqrestore(&myrwlock, flags);
pr_info("写锁已解锁\n");
}
static int __init example_rwlock_init(void)
{
pr_info("example_rwlock 模块启动\n");
example_read_lock();
example_write_lock();
return 0;
}
static void __exit example_rwlock_exit(void)
{
pr_info("example_rwlock 模块退出\n");
}
module_init(example_rwlock_init);
module_exit(example_rwlock_exit);
MODULE_DESCRIPTION("读/写锁示例");
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,62 @@
/*
* example_spinlock.c
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/spinlock.h>
static DEFINE_SPINLOCK(sl_static);
static spinlock_t sl_dynamic;
static void example_spinlock_static(void)
{
unsigned long flags;
spin_lock_irqsave(&sl_static, flags);
pr_info("已锁定静态自旋锁\n");
/* 执行一些安全的操作。由于这会占用 100% 的 CPU 时间,
* 这段代码应该运行时间不超过几毫秒。
*/
spin_unlock_irqrestore(&sl_static, flags);
pr_info("已解锁静态自旋锁\n");
}
static void example_spinlock_dynamic(void)
{
unsigned long flags;
spin_lock_init(&sl_dynamic);
spin_lock_irqsave(&sl_dynamic, flags);
pr_info("已锁定动态自旋锁\n");
/* 执行一些安全的操作。由于这会占用 100% 的 CPU 时间,
* 这段代码应该运行时间不超过几毫秒。
*/
spin_unlock_irqrestore(&sl_dynamic, flags);
pr_info("已解锁动态自旋锁\n");
}
static int __init example_spinlock_init(void)
{
pr_info("自旋锁示例启动\n");
example_spinlock_static();
example_spinlock_dynamic();
return 0;
}
static void __exit example_spinlock_exit(void)
{
pr_info("自旋锁示例退出\n");
}
module_init(example_spinlock_init);
module_exit(example_spinlock_exit);
MODULE_DESCRIPTION("自旋锁示例");
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,12 @@
obj-m += print_string.o
obj-m += kbleds.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

View File

@ -0,0 +1,81 @@
/*
* kbleds.c - 闪烁键盘LED直到模块被卸载。
*/
#include <linux/init.h>
#include <linux/kd.h> /* 引入 KDSETLED */
#include <linux/module.h>
#include <linux/tty.h> /* 引入 tty_struct */
#include <linux/vt.h> /* 引入 MAX_NR_CONSOLES */
#include <linux/vt_kern.h> /* 引入 fg_console */
#include <linux/console_struct.h> /* 引入 vc_cons */
MODULE_DESCRIPTION("示例模块,演示如何使用键盘LED。");
static struct timer_list my_timer;
static struct tty_driver *my_driver;
static unsigned long kbledstatus = 0;
#define BLINK_DELAY HZ / 5
#define ALL_LEDS_ON 0x07
#define RESTORE_LEDS 0xFF
/* 函数 my_timer_func 定期闪烁键盘LED,通过在键盘驱动上调用 ioctl() 命令 KDSETLED 实现。
* 要了解更多关于虚拟终端 ioctl 操作的信息,请参见文件:drivers/tty/vt/vt_ioctl.c, 函数 vt_ioctl()
*
* KDSETLED 的参数交替设置为 7(从而使 LED 模式设置为 LED_SHOW_IOCTL,所有的 LED 都被点亮)和 0xFF
* (任何大于 7 的值会将 LED 模式切换回 LED_SHOW_FLAGS,因此 LED 反映实际的键盘状态)。要了解更多信息,
* 请参见文件:drivers/tty/vt/keyboard.c, 函数 setledstate()。
*/
static void my_timer_func(struct timer_list *unused)
{
struct tty_struct *t = vc_cons[fg_console].d->port.tty;
if (kbledstatus == ALL_LEDS_ON)
kbledstatus = RESTORE_LEDS;
else
kbledstatus = ALL_LEDS_ON;
(my_driver->ops->ioctl)(t, KDSETLED, kbledstatus);
my_timer.expires = jiffies + BLINK_DELAY;
add_timer(&my_timer);
}
static int __init kbleds_init(void)
{
int i;
pr_info("kbleds: 正在加载\n");
pr_info("kbleds: 前台控制台为 %x\n", fg_console);
for (i = 0; i < MAX_NR_CONSOLES; i++) {
if (!vc_cons[i].d)
break;
pr_info("poet_atkm: 控制台[%i/%i] #%i, tty %p\n", i, MAX_NR_CONSOLES,
vc_cons[i].d->vc_num, (void *)vc_cons[i].d->port.tty);
}
pr_info("kbleds: 扫描控制台完成\n");
my_driver = vc_cons[fg_console].d->port.tty->driver;
pr_info("kbleds: tty 驱动程序名称 %s\n", my_driver->driver_name);
/* 设置 LED 闪烁定时器。 */
timer_setup(&my_timer, my_timer_func, 0);
my_timer.expires = jiffies + BLINK_DELAY;
add_timer(&my_timer);
return 0;
}
static void __exit kbleds_cleanup(void)
{
pr_info("kbleds: 正在卸载...\n");
del_timer(&my_timer);
(my_driver->ops->ioctl)(vc_cons[fg_console].d->port.tty, KDSETLED,
RESTORE_LEDS);
}
module_init(kbleds_init);
module_exit(kbleds_cleanup);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,59 @@
/*
* print_string.c - 将输出发送到我们运行的tty,无论是通过X11、telnet等。我们通过将字符串打印到当前任务关联的tty
*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sched.h> /* 引入 current */
#include <linux/tty.h> /* 引入 tty 相关声明 */
static void print_string(char *str)
{
/* 当前任务的tty */
struct tty_struct *my_tty = get_current_tty();
/* 如果my_tty为NULL,则当前任务没有可以打印的tty(例如,如果它是一个守护进程)。如果是这样,我们无法执行任何操作 */
if (my_tty) {
const struct tty_operations *ttyops = my_tty->driver->ops;
/* my_tty->driver是一个包含tty函数的结构体,其中一个函数(write)用于将字符串写入tty。
* 它可以用于从用户或内核的内存段获取字符串。
*
* 该函数的第一个参数是要写入的tty,因为同样的函数通常会用于所有相同类型的tty。
* 第二个参数是指向字符串的指针。第三个参数是字符串的长度。
*
* 如下所示,有时需要使用预处理器来创建适用于不同内核版本的代码。我们在这里采取的方法不具备通用性
* 处理这种情况的正确方法在linux/Documentation/SubmittingPatches的第2节中有描述。
*/
(ttyops->write)(my_tty, /* tty本身 */
str, /* 字符串 */
strlen(str)); /* 长度 */
/* tty最初是硬件设备,通常严格遵循ASCII标准。在ASCII中,要换行需要两个字符,即回车符(结束当前行)和换行符(开始新行)。
* 在Unix中,ASCII换行符用于两个目的 - 因此我们不能只使用\n,因为它没有回车符,下行会在换行符后的列开始。
*
* Unix 和 MS Windows 的文本文件格式因而有所不同
* - Unix 文本文件使用单一的 LF(\n),在显示或编辑时会在每个 LF 位置换行。
* - Windows 文本文件使用 CR+LF(\r\n),在显示或编辑时会在每个 CR+LF 位置换行。
*
* \015 代表 Carriage Return (CR),即 ASCII 值为 13
* \012 代表 Line Feed (LF),即 ASCII 值为 10
*/
(ttyops->write)(my_tty, "\015\012", 2);
}
}
static int __init print_string_init(void)
{
print_string("模块已插入。你好, 世界! ");
return 0;
}
static void __exit print_string_exit(void)
{
print_string("模块已移除。再见, 世界! ");
}
module_init(print_string_init);
module_exit(print_string_exit);
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,15 @@
obj-m += example_tasklet.o
obj-m += sched.o
obj-m += intrpt.o
obj-m += bottomhalf.o
obj-m += bh_thread.c
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

View File

@ -0,0 +1,151 @@
/*
* bh_thread.c - 起始部分和后续部分中断处理
*
* 基于 Stefan Wendler(devnull@kaltpost.de)的 RPi 示例
* 来源于:
* https://github.com/wendlers/rpi-kmod-samples
*
* 按下一个按钮打开 LED,另一个按钮关闭 LED
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
static int button_irqs[] = { -1, -1 };
/* 定义 LED 的 GPIO。
* FIXME: 根据你的板子的 GPIO 号码修改这些数字。
*/
static struct gpio leds[] = { { 1, GPIOF_OUT_INIT_LOW, "LED 1" } };
/* 定义按钮的 GPIO
* FIXME: 根据你的板子的 GPIO 号码修改这些数字。
*/
static struct gpio buttons[] = {
{ 2, GPIOF_IN, "LED 1 开按钮" },
{ 3, GPIOF_IN, "LED 1 关按钮" },
};
/* 当 IRQ 触发时立即发生 */
static irqreturn_t button_top_half(int irq, void *ident)
{
return IRQ_WAKE_THREAD;
}
/* 可以在空闲时处理,释放 IRQ 给其他高优先级任务 */
static irqreturn_t button_bottom_half(int irq, void *ident)
{
pr_info("后续部分任务开始\n");
mdelay(500); /* 执行一些耗时的操作 */
pr_info("后续部分任务结束\n");
return IRQ_HANDLED;
}
static int __init bottomhalf_init(void)
{
int ret = 0;
pr_info("%s\n", __func__);
/* 注册 LED GPIO */
ret = gpio_request_array(leds, ARRAY_SIZE(leds));
if (ret) {
pr_err("无法请求 LED 的 GPIOs: %d\n", ret);
return ret;
}
/* 注册按钮 GPIO */
ret = gpio_request_array(buttons, ARRAY_SIZE(buttons));
if (ret) {
pr_err("无法请求 BUTTON 的 GPIOs: %d\n", ret);
goto fail1;
}
pr_info("当前 button1 的值: %d\n", gpio_get_value(buttons[0].gpio));
ret = gpio_to_irq(buttons[0].gpio);
if (ret < 0) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail2;
}
button_irqs[0] = ret;
pr_info("成功请求 BUTTON1 IRQ # %d\n", button_irqs[0]);
ret = request_threaded_irq(button_irqs[0], button_top_half,
button_bottom_half,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpiomod#button1", &buttons[0]);
if (ret) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail2;
}
ret = gpio_to_irq(buttons[1].gpio);
if (ret < 0) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail2;
}
button_irqs[1] = ret;
pr_info("成功请求 BUTTON2 IRQ # %d\n", button_irqs[1]);
ret = request_threaded_irq(button_irqs[1], button_top_half,
button_bottom_half,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpiomod#button2", &buttons[1]);
if (ret) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail3;
}
return 0;
/* 清理已设置的部分 */
fail3:
free_irq(button_irqs[0], NULL);
fail2:
gpio_free_array(buttons, ARRAY_SIZE(leds));
fail1:
gpio_free_array(leds, ARRAY_SIZE(leds));
return ret;
}
static void __exit bottomhalf_exit(void)
{
int i;
pr_info("%s\n", __func__);
/* 释放 IRQ */
free_irq(button_irqs[0], NULL);
free_irq(button_irqs[1], NULL);
/* 关闭所有 LED */
for (i = 0; i < ARRAY_SIZE(leds); i++)
gpio_set_value(leds[i].gpio, 0);
/* 注销 */
gpio_free_array(leds, ARRAY_SIZE(leds));
gpio_free_array(buttons, ARRAY_SIZE(buttons));
}
module_init(bottomhalf_init);
module_exit(bottomhalf_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("起始部分和后续部分中断处理");

View File

@ -0,0 +1,171 @@
/*
* bottomhalf.c - 起始与后续部分中断处理
*
* 基于 Stefan Wendler (devnull@kaltpost.de) 提供的 RPi 示例
* 来自:
* https://github.com/wendlers/rpi-kmod-samples
*
* 按下一个按钮以点亮 LED,按下另一个按钮以熄灭它
*/
#include <linux/delay.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/init.h>
/* 宏 DECLARE_TASKLET_OLD 的存在是为了确保兼容
* 详细信息: https://lwn.net/Articles/830964/
*/
#ifndef DECLARE_TASKLET_OLD
#define DECLARE_TASKLET_OLD(arg1, arg2) DECLARE_TASKLET(arg1, arg2, 0L)
#endif
static int button_irqs[] = { -1, -1 };
/* 定义 LED 的 GPIO。
* TODO: 根据你的板子更改 GPIO 编号。
*/
static struct gpio leds[] = { { 1, GPIOF_OUT_INIT_LOW, "LED 1" } };
/* 定义按钮的 GPIO。
* TODO: 根据你的板子更改 GPIO 编号。
*/
static struct gpio buttons[] = { { 2, GPIOF_IN, "LED 1 打开按钮" },
{ 3, GPIOF_IN, "LED 1 关闭按钮" } };
/* 包含一些复杂处理的任务队列函数 */
static void bottomhalf_tasklet_fn(unsigned long data)
{
pr_info("底半部任务队列开始\n");
/* 执行一些耗时的操作 */
mdelay(500);
pr_info("底半部任务队列结束\n");
}
static DECLARE_TASKLET_OLD(buttontask, bottomhalf_tasklet_fn);
/* 按钮按下时触发的中断函数 */
static irqreturn_t button_isr(int irq, void *data)
{
/* 立即执行一些操作 */
if (irq == button_irqs[0] && !gpio_get_value(leds[0].gpio))
gpio_set_value(leds[0].gpio, 1); // 点亮 LED
else if (irq == button_irqs[1] && gpio_get_value(leds[0].gpio))
gpio_set_value(leds[0].gpio, 0); // 熄灭 LED
/* 将剩余工作交给调度程序处理 */
tasklet_schedule(&buttontask);
return IRQ_HANDLED; // 表示中断已经处理完毕
}
static int __init bottomhalf_init(void)
{
int ret = 0;
pr_info("%s\n", __func__);
/* 请求 LED 的 GPIO */
ret = gpio_request_array(leds, ARRAY_SIZE(leds));
if (ret) {
pr_err("无法请求 LED 的 GPIOs: %d\n", ret);
return ret;
}
/* 请求按钮的 GPIO */
ret = gpio_request_array(buttons, ARRAY_SIZE(buttons));
if (ret) {
pr_err("无法请求按钮的 GPIOs: %d\n", ret);
goto fail1;
}
pr_info("当前按钮1的值: %d\n", gpio_get_value(buttons[0].gpio));
/* 获取按钮 1 的中断号 */
ret = gpio_to_irq(buttons[0].gpio);
if (ret < 0) {
pr_err("无法请求中断: %d\n", ret);
goto fail2;
}
button_irqs[0] = ret;
pr_info("成功请求了按钮1的中断号 # %d\n", button_irqs[0]);
/* 注册按钮 1 的中断 */
ret = request_irq(button_irqs[0], button_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio模块#按钮1", NULL);
if (ret) {
pr_err("无法请求中断: %d\n", ret);
goto fail2;
}
/* 获取按钮 2 的中断号 */
ret = gpio_to_irq(buttons[1].gpio);
if (ret < 0) {
pr_err("无法请求中断: %d\n", ret);
goto fail2;
}
button_irqs[1] = ret;
pr_info("成功请求了按钮2的中断号 # %d\n", button_irqs[1]);
/* 注册按钮 2 的中断 */
ret = request_irq(button_irqs[1], button_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio模块#按钮2", NULL);
if (ret) {
pr_err("无法请求中断: %d\n", ret);
goto fail3;
}
return 0;
/* 清理已经设置的资源 */
fail3:
free_irq(button_irqs[0], NULL);
fail2:
gpio_free_array(buttons, ARRAY_SIZE(buttons));
fail1:
gpio_free_array(leds, ARRAY_SIZE(leds));
return ret;
}
static void __exit bottomhalf_exit(void)
{
int i;
pr_info("%s\n", __func__);
/* 释放中断 */
free_irq(button_irqs[0], NULL);
free_irq(button_irqs[1], NULL);
/* 关闭所有 LED */
for (i = 0; i < ARRAY_SIZE(leds); i++)
gpio_set_value(leds[i].gpio, 0);
/* 注销 GPIO */
gpio_free_array(leds, ARRAY_SIZE(leds));
gpio_free_array(buttons, ARRAY_SIZE(buttons));
}
module_init(bottomhalf_init);
module_exit(bottomhalf_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("起始与后续部分中断处理");

View File

@ -0,0 +1,47 @@
/*
* example_tasklet.c
*/
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/printk.h>
/* 宏 DECLARE_TASKLET_OLD 存在是为了兼容性 参见 https://lwn.net/Articles/830964/ */
#ifndef DECLARE_TASKLET_OLD
#define DECLARE_TASKLET_OLD(arg1, arg2) DECLARE_TASKLET(arg1, arg2, 0L)
#endif
/* tasklet_fn 是 tasklet 执行的函数,接收一个 unsigned long 类型的参数 */
static void tasklet_fn(unsigned long data)
{
pr_info("示例 tasklet 开始执行!\n");
mdelay(20000); // 延迟 10 秒
pr_info("示例 tasklet 执行结束!\n");
}
/* 定义一个 tasklet,并将 tasklet_fn 作为其处理函数 */
static DECLARE_TASKLET_OLD(mytask, tasklet_fn);
/* 模块初始化函数 */
static int __init example_tasklet_init(void)
{
pr_info("tasklet 示例初始化\n");
tasklet_schedule(&mytask); // 安排 tasklet 执行
mdelay(10);
pr_info("示例 tasklet 初始化继续...\n");
return 0;
}
/* 模块退出函数 */
static void __exit example_tasklet_exit(void)
{
pr_info("tasklet 示例退出\n");
tasklet_kill(&mytask); // 终止 tasklet 的执行
}
module_init(example_tasklet_init);
module_exit(example_tasklet_exit);
MODULE_DESCRIPTION("Tasklet 示例");
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,151 @@
/*
* intrpt.c - 处理带中断的 GPIO
*
* 基于 Stefan Wendler (devnull@kaltpost.de) 提供的 RPi 示例:
* https://github.com/wendlers/rpi-kmod-samples
*
* 按下一个按钮点亮 LED,按下另一个按钮熄灭 LED。
*/
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/kernel.h> /* 引入 ARRAY_SIZE() */
#include <linux/module.h>
#include <linux/printk.h>
static int button_irqs[] = { -1, -1 };
/* 定义 LED 的 GPIO。
* TODO: 根据你的板子更改 GPIO 编号。
*/
static struct gpio leds[] = { { 1, GPIOF_OUT_INIT_LOW, "LED 1" } };
/* 定义按钮的 GPIO。
* TODO: 根据你的板子更改 GPIO 编号。
*/
static struct gpio buttons[] = { { 2, GPIOF_IN, "LED 1 打开按钮" },
{ 3, GPIOF_IN, "LED 1 关闭按钮" } };
/* 按钮按下时触发的中断函数。 */
static irqreturn_t button_isr(int irq, void *data)
{
/* 第一个按钮 */
if (irq == button_irqs[0] && !gpio_get_value(leds[0].gpio))
gpio_set_value(leds[0].gpio, 1); /* 点亮 LED */
/* 第二个按钮 */
else if (irq == button_irqs[1] && gpio_get_value(leds[0].gpio))
gpio_set_value(leds[0].gpio, 0); /* 熄灭 LED */
return IRQ_HANDLED;
}
static int __init intrpt_init(void)
{
int ret = 0;
pr_info("%s\n", __func__);
/* 注册 LED GPIO */
ret = gpio_request_array(leds, ARRAY_SIZE(leds));
if (ret) {
pr_err("无法请求 LED 的 GPIO: %d\n", ret);
return ret;
}
/* 注册按钮 GPIO */
ret = gpio_request_array(buttons, ARRAY_SIZE(buttons));
if (ret) {
pr_err("无法请求按钮的 GPIO: %d\n", ret);
goto fail1;
}
pr_info("当前按钮1的值: %d\n", gpio_get_value(buttons[0].gpio));
ret = gpio_to_irq(buttons[0].gpio);
if (ret < 0) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail2;
}
button_irqs[0] = ret;
pr_info("成功请求 BUTTON1 的 IRQ # %d\n", button_irqs[0]);
/* 请求按钮1的中断
* IRQF_TRIGGER_RISING 表示在信号电平从低到高变化时触发中断
* IRQF_TRIGGER_FALLING 表示在信号电平从高到低变化时触发中断
* 这种组合使得中断处理函数在检测到按钮的按下和释放时都会被调用。
* */
ret = request_irq(button_irqs[0], button_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio模块#按钮1", NULL);
if (ret) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail2;
}
ret = gpio_to_irq(buttons[1].gpio);
if (ret < 0) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail2;
}
button_irqs[1] = ret;
pr_info("成功请求 BUTTON2 的 IRQ # %d\n", button_irqs[1]);
/* 请求按钮2的中断, 标志位同上 */
ret = request_irq(button_irqs[1], button_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio模块#按钮2", NULL);
if (ret) {
pr_err("无法请求 IRQ: %d\n", ret);
goto fail3;
}
return 0;
/* 清理已经设置的部分 */
fail3:
free_irq(button_irqs[0], NULL); /* 释放按钮1的中断请求 */
fail2:
gpio_free_array(buttons, ARRAY_SIZE(buttons)); /* 释放按钮的GPIO */
fail1:
gpio_free_array(leds, ARRAY_SIZE(leds)); /* 释放LED的GPIO */
return ret;
}
static void __exit intrpt_exit(void)
{
int i;
pr_info("%s\n", __func__);
/* 释放 IRQ */
free_irq(button_irqs[0], NULL);
free_irq(button_irqs[1], NULL);
/* 关闭所有 LED */
for (i = 0; i < ARRAY_SIZE(leds); i++)
gpio_set_value(leds[i].gpio, 0);
/* 注销 GPIO */
gpio_free_array(leds, ARRAY_SIZE(leds));
gpio_free_array(buttons, ARRAY_SIZE(buttons));
}
module_init(intrpt_init);
module_exit(intrpt_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("处理一些 GPIO 中断");

View File

@ -0,0 +1,34 @@
/*
* sched.c
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/workqueue.h>
static struct workqueue_struct *queue = NULL;
static struct work_struct work;
static void work_handler(struct work_struct *data)
{
pr_info("工作队列函数\n");
}
static int __init sched_init(void)
{
queue = alloc_workqueue("HELLOWORLD", WQ_UNBOUND, 1);
INIT_WORK(&work, work_handler);
queue_work(queue, &work);
return 0;
}
static void __exit sched_exit(void)
{
destroy_workqueue(queue);
}
module_init(sched_init);
module_exit(sched_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("工作队列示例");

11
示例/Makefile Normal file
View File

@ -0,0 +1,11 @@
obj-m += example_taskle.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Some files were not shown because too many files have changed in this diff Show More