文章目录
- 前言
- 入门篇
- 一、常见的指令以及权限的理解
- 1. 初始Linux:
- 2.Linux下的基本指令
- 2.1 用户的创建和删除:
- 2.2 ls指令
- 2.3 pwd指令
- 2.4 cd指令
- 2.5 touch指令
- 2.6 mkdir指令
- 2.7 rmdir指令和rm指令
- 2.8 man指令
- 2.9 cp指令
- 2.10 mv指令
- 2.11 cat指令(tac指令)
- 2.12 more指令
- 2.13 less指令(重要)
- 2.14 head指令(查看文件头的内容)
- 2.15 tail指令(查看文件尾的内容)
- 2.16 怎么查看文件中间的内容
- 2.17 时间相关的指令
- 2.18 cal指令
- 2.19 find指令(非常重要)
- 2.20 grep指令
- 2.21 zip/unzip指令
- 2.22 tar指令(重要)
- 2.23 bc指令
- 2.24 uname 指令
- 2.25 重要的几个热键【Tab】,【Ctrl】+ C,【Ctrl】+ D,【ctrl】+ R
- 2.26 file指令和history指令
- 2.27 关机和重启
- 2.28 扩展命令
- 2.29 工具类和其他指令(此项为临时记录,未详细整理)
- 3. Linux权限的概念
- 二、Linux环境基础开发工具使用
- 三、Linux进程概念
- 四、Linux进程控制
- 五、基础IO
- 六、进程间的通信
- 七、进程信号
- 八、Linux多线程
前言
这篇是学习笔记:记录Linux(CentOS7.6)学习过程中的要点疑点和难点。 笔记简介:这篇笔记写于2022/09/15。
入门篇
一、常见的指令以及权限的理解
1. 初始Linux:
Linux是一种自由和开放源代码的类UNIX操作系统,该操作系统的内核由林纳斯托瓦兹在1991年首次发布,之后,在加上用户空间的应用程序之后,就成为了Linux操作系统。严格来讲,Linux只是操作系统内核本身,但通常采用“Linux内核”来表达该意思。而Linux则常用来指基于Linux内核的完整操作系统,它包括GUI组件和许多其他实用工具。
GNU通用公共许可协议(GNU General Public License,简称GNU GPL或GPL),是一个广泛被使用的自由软件许可协议条款,最初由理查德斯托曼为GNU计划而撰写,GPL给予了计算机程序自由软件的定义, 任何基于GPL软件开发衍生的产品在发布时必须采用GPL许可证方式,且必须公开源代码。
Linux是自由软件和开放源代码软件发展中最著名的例子。只要遵循GNU通用公共许可证,任何个人和机构都可以自由地使用Linux的所有底层源代码,也可以自由地修改和再发布。随着Linux操作系统飞速发展,各种集成在Linux上的开源软件和实用工具也得到了应用和普及,因此,Linux也成为了开源软件的代名词。
Linux版本:
(1)内核版本(技术):各种各样的内核版本。
(2)发行版本:Ubuntu、CentOS、Redhat(红帽)、Kail等。
(3)发行版广义上就是:Linux内核 + 外壳程序。
(4)外壳程序:常用的就是shell、bash。
XShell工具:
XShell 是一个远程终端软件,我们使用 XShell 远程登录 Linux。
操作系统(OS)概念,定位:
操作系统使计算机更好用!这是操作系统的根本要义!
操作系统是在软件(对上)和硬件(对下)之间的一种“承上启下的身份”!
对下:各种软硬件资源的管理
对上:给用户提供一个良好的运行环境:稳定、高效、安全。
2.Linux下的基本指令
指令语法说明:
【】:表示可选择项。
… :表示此项可以有多个。
2.1 用户的创建和删除:
1.登录root账户,可以使用命令whoami查看当前登录的账户。
2.使用命令adduser name添加管理员,添加的管理员可以在home目录里看到。
3.使用命令passwd name设置密码(设置密码时不回显),输入两次确认。
4.删除用户登录root账户,使用userdel -r name删除。
2.2 ls指令
语法: ls【选项】【目录或文件】
功能: 对于目录,该命令列出该目录下的所有子目录与文件。对于文件,将列出文件名以及其他信息。
常用选项:
-a 列出目录下的所有文件,包括以 . 开头的隐含文件。
-d 将目录象文件一样显示,而不是显示其下的文件。 如:ls –d 指定目录
-i 输出文件的 i 节点的索引信息。 如 ls –ai 指定文件
-k 以 k 字节的形式表示文件的大小。ls –alk 指定文件
-l 列出文件的详细信息。
-n 用数字的 UID,GID 代替名称。 (介绍 UID, GID)
-F 在每个文件名后附上一个字符以说明该文件的类型,“*”表示可执行的普通文件;“/”表示目录;“@”表示符号链接;“|”表示FIFOs;“=”表示套接字(sockets)。(目录类型识别)
-r 对目录反向排序。
-t 以时间排序。
-s 在l文件名后输出该文件的大小。(大小排序,如何找到目录下最大的文件)
-R 列出所有子目录下的文件。(递归)
-1 一行只输出一个文件。
2.3 pwd指令
语法: pwd
功能: 显示用户当前所在的目录
常用选项:
无
2.4 cd指令
Linux系统中,磁盘上的文件和目录被组成一棵目录树,每个节点都是目录或文件。
(几乎所有的操作系统,管理文件的方式,基本都是采用多叉树的形式。)
语法: cd 目录名
功能: 改变工作目录。将当前工作目录改变到指定的目录下。
常用选项:
cd . . : 返回上级目录
cd /home/litao/linux/ : 绝对路径(从根目录开始,具有唯一性)
cd . ./day02/ : 相对路径(从当前目的开始,不具有唯一性)
cd ~ :进入用户家目
cd - :返回最近访问目录
2.5 touch指令
语法: touch 【选项】… 文件…
功能: touch命令参数可更改文档或目录的日期时间,包括存取时间和更改时间,或者新建一个不存在的文件。
常用选项:
-a 或–time=atime或–time=access或–time=use只更改存取时间。
-c 或–no-create 不建立任何文档。
-d 使用指定的日期时间,而非现在的时间。
-f 此参数将忽略不予处理,仅负责解决BSD版本touch指令的兼容性问题。
-m 或–time=mtime或–time=modify 只更改变动时间。
-r 把指定文档或目录的日期时间,统统设成和参考文档或目录的日期时间相同。
-t 使用指定的日期时间,而非现在的时间。
2.6 mkdir指令
语法: mkdir 【选项】 dirname…
功能: 在当前目录下创建一个名为 “dirname”的目录
常用选项:
-p, --parents 可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不存在的目录,即一次可以建立多个目录;
比如:mkdir –p test/test1 : 递归建立多个目录。
2.7 rmdir指令和rm指令
2.7.1 rmdir指令
rmdir是一个与mkdir相对应的命令。mkdir是建立目录,而rmdir是删除命令。
语法: rmdir 【-p】【dirName】
适用对象: 具有当前目录操作权限的所有使用者
功能: 删除空目录
常用选项:
-p 当子目录被删除后如果父目录也变成空目录的话,就连带父目录一起删除。
2.7.2 rm指令
语法: rm【-f-i-r】【dirName/dir】
适用对象: 所有使用者
功能: 删除文件或目录,rm命令可以同时删除文件或目录
常用选项:
-f 即使文件属性为只读(即写保护),亦直接删除(强制删除)
-i 删除前逐一询问确认
-r 删除目录及其下所有文件(递归删除)
温馨提示:千万不要在root下执行:rm -rf /* 哦
2.8 man指令
Linux的命令有很多参数,我们不可能全记住,我们可以通过查看联机手册获取帮助。
语法: man 【选项】命令
功能: 访问Linux手册页的命令
常用选项:
-k 根据关键字搜索联机帮助。
num 只在第num章节找。
-a 将所有章节的都显示出来,比如 man printf 它缺省从第一章开始搜索,知道就停止,用a选项,当按下q退出,他会继续往后面搜索,直到所有章节都搜索完毕。
解释一下,man手册分为8章
1 是普通的命令
2 是系统调用,如open,write之类的(通过这个,至少可以很方便的查到调用这个函数,需要加什么头文件)
3 是库函数,如printf,fread4是特殊文件,也就是/dev下的各种设备文件
5 是指文件的格式,比如passwd, 就会说明这个文件中各个字段的含义
6 是给游戏留的,由各个游戏自己定义
7 是附件还有一些变量,比如向environ这种全局变量在这里就有说明
8 是系统管理用的命令,这些命令只能由root使用,如ifconfig
2.9 cp指令
语法: cp 【选项】源文件或目录 目标文件或目录
功能: 复制文件或目录,拷贝到目标文件时可以直接输入新名字重命名
说明: cp指令用于复制文件或目录,如同时指定两个以上的文件或目录,且最后的目的地是一个已经存在的目录,则它会把前面指定的所有文件或目录复制到此目录中。若同时指定多个文件或目录,而最后的目的地并非一个已存在的目录,则会出现错误信息
常用选项:
-f 或 --force 强行复制文件或目录, 不论目的文件或目录是否已经存在
-i 或 --interactive 覆盖文件之前先询问用户
-r递归处理,将指定目录下的文件与子目录一并处理。若源文件或目录的形态,不属于目录或符号链接,则一律视为普通文件处理
-R 或 --recursive递归处理,将指定目录下的文件及子目录一并处理
2.10 mv指令
语法: mv 【选项】源文件或目录 目标文件或目录
功能:
(1) 视mv命令中第二个参数类型的不同(是目标文件还是目标目录),mv命令将文件重命名或将其移至一个新的目录中。
(2)当第二个参数类型是文件时,mv命令完成文件重命名,此时,源文件只能有一个(也可以是源目录名),它将所给的源文件或目录重命名为给定的目标文件名。
(3)当第二个参数是已存在的目录名称时,源文件或目录参数可以有多个,mv命令将各参数指定的源文件均移至目标目录中。
说明: mv命令是move的缩写,可以用来移动文件或者将文件改名(move (rename) files),是Linux系统下常用的命令,经常用来备份文件或者目录。
常用选项:
-f :force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖
-i :若目标文件 (destination) 已经存在时,就会询问是否覆盖!
将rm改造成mv:
vim ~/.bashrc #修改这个文件
mkdir -p ~/.trash
alias rm=trash
alias ur=undelfile
undelfile() {
mv -i ~/.trash/\$@ ./
}
trash() {
mv $@ ~/.trash/
}
2.11 cat指令(tac指令)
语法: cat 【选项】… 【文件】…(tac语法一致)
功能: 显示文件的内容(tac是逆序输出显示文件内容)。
说明: 单独cat指令,默认从标准输入(键盘)读取内容并显示。
常用选项:
-b 对非空输出行编号
-n 对输出的所有行编号
-s 不输出多行空行
2.12 more指令
语法: more【选项】文件
功能: 分段显示文件内容。
说明: 只能按回车向后查看内容,按q退出。
常用选项:
-num 显示num行内容
2.13 less指令(重要)
语法: less【参数】… 文件…
功能: less与more类似,但使用less可以随意浏览文件,而more仅能向前移动,却不能向后移动,而且less在查看之前
不会加载整个文件,按q退出。
说明:
(1)less 工具也是对文件或其它输出进行分页显示的工具,应该说是linux正统查看文件内容的工具,功能极其强大。
(2)less 的用法比起 more 更加的有弹性。在 more 的时候,我们并没有办法向前面翻, 只能往后面看
(3)但若使用了 less 时,就可以使用 [pageup][pagedown] 等按键的功能来往前往后翻看文件,更容易用来查看一个文件的内容!
(4)除此之外,在 less 里头可以拥有更多的搜索功能,不止可以向下搜,也可以向上搜。
常用选项:
-i 忽略搜索时的大小写
-N 显示每行的行号
/字符串:向下搜索“字符串”的功能
?字符串:向上搜索“字符串”的功能
n:重复前一个搜索(与 / 或 ? 有关)
N:反向重复前一个搜索(与 / 或 ? 有关)
2.14 head指令(查看文件头的内容)
语法: head【选项】… 【文件】…
功能: head 用来显示档案的开头至标准输出中,默认head命令打印其相应文件的开头10行。
说明: head 与 tail 就像它的名字一样的浅显易懂,它是用来显示开头或结尾某个数量的文字区块,head 用来显示档案的
开头至标准输出中,而 tail 想当然尔就是看档案的结尾。
常用选项:
-num 显示num行内容
2.15 tail指令(查看文件尾的内容)
语法: tail【选项】… 【文件】…
功能: 用于显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件。
说明: tail 命令从指定点开始将文件写到标准输出.使用tail命令的-f选项可以方便的查阅正在改变的日志文件,tail -f filename会把filename里最尾部的内容显示在屏幕上,并且不但刷新,使你看到最新的文件内容.
常用选项:
-f 循环读取
-num 显示num行内容
2.16 怎么查看文件中间的内容
**需求:**想要查看100-120行的内容。
在实现这个需求前,先学习一下输出和输入重定向和通道的知识。
2.16.1 输出重定向和输入重定向(了解知识)
echo “hello linux” 把字符串默认显示到标准输出(显示器)。
echo “hello linux” > log.txt 把字符串输出(写入)到log.txt文件中,如果目标文件不存在,会自动创建。这个就是输出重定向。
输出重定向: 本来应该显示到标准输出的内容,重定向写入到了文件中。
输出重定向的性质: 会清空原来的数据,重新写入。
echo “hello linux” >> log.txt 把字符串追加写入到文件中。这个是追加输出重定向。
追加输出重定向的性质: 不会清空原来的数据,在原来的数据上追加数据。
cat < log.txt 把log.txt文件中的数据提取并显示到显示器。这个叫做输入重定向。
输入重定向: 本来应该从标准输入读取的内容,重定向到文件中读取内容。
2.16.2 管道 “|”(了解知识)
管道: 把一个命令处理完的结果,经过管道流入下一个命令处理,特别像流水线数据处理。
管道的性质:
(1)管道是有入口和出口的。
(2)使用管道时,默认隐式的发生了重定向。
实现方法:
方法1:
head -120 log.txt > temp.txt 先把前120行提取到一个临时文件里 。
tail -20 temp.txt 再显示临时文件的后20行。
方法2:
head -120 log.txt | tail -20 使用head把前120行数据先提取出来,再通过管道“|”流入tail再继续处理
cat log.txt | head -120 | tail -20 这种组合也是可以的
2.17 时间相关的指令
date显示
date 指定格式显示时间: date +%Y-%m-%d-%H:%M:%S
date 用法:date [OPTION]… [+FORMAT]
(1)在显示方面,使用者可以设定欲显示的格式,格式设定为一个加号后接数个标记,其中常用的标记列表如下
%H : 小时(00…23)
%M : 分钟(00…59)
%S : 秒(00…61)
%X : 相当于 %H:%M:%S
%d : 日 (01…31)
%m : 月份 (01…12)
%Y : 完整年份 (0000…9999)
%F : 相当于 %Y-%m-%d
(2)在设定时间方面
date -s //设置当前时间,只有root权限才能设置,其他只能查看。
date -s 20080523 //设置成20080523,这样会把具体时间设置成空00:00:00
date -s 01:01:01 //设置具体时间,不会对日期做更改
date -s “01:01:01 2008-05-23″ //这样可以设置全部时间
date -s “01:01:01 20080523″ //这样可以设置全部时间
date -s “2008-05-23 01:01:01″ //这样可以设置全部时间
date -s “20080523 01:01:01″ //这样可以设置全部时间
(3)时间戳
时间->时间戳:date +%s
时间戳->时间:date -d@1508749502
Unix时间戳(英文为Unix epoch, Unix time, POSIX time 或 Unix timestamp)是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。
2.18 cal指令
语法: cal 【选项 】【年/月/日】
功能: 用于查看日历等时间信息,如只有一个参数,则表示年份(1-9999),如有两个参数,则表示月份和年份
说明: cal命令可以用来显示公历(阳历)日历。公历是现在国际通用的历法,又称格列历,通称阳历。“阳历”又名“太阳历”,系以地球绕行太阳一周为一年,为西方各国所通用,故又名“西历”。
常用选项:
-3 显示系统前一个月,当前月,下一个月的月历
-j 显示在当年中的第几天(一年日期按天算,从1月1号算起,默认显示当前月在一年中的天数)
-y 显示当前年份的日历
2.19 find指令(非常重要)
语法: find 路径 选项
功能: 用于在文件树种查找文件,并作出相应的处理(可能访问磁盘)
说明:
(1)Linux下find命令在目录结构中搜索文件,并执行指定的操作。
(2)Linux下find命令提供了相当多的查找条件,功能很强大。由于find具有强大的功能,所以它的选项也很多,其中大部分选项都值得我们花时间来了解一下。
(3)即使系统中含有网络文件系统( NFS),find命令在该文件系统中同样有效,只你具有相应的权限。
(4)在运行一个非常消耗资源的find命令时,很多人都倾向于把它放在后台执行,因为遍历一个大的文件系统可能会花费很长的时间(这里是指30G字节以上的文件系统)。
常用选项:
-name 按照文件名查找文件。
2.20 grep指令
语法: grep【选项 】搜索字符串 文件
功能: 在文件中搜索字符串,将找到的行打印出来
常用选项:
-i :忽略大小写的不同,所以大小写视为相同
-n :顺便输出行号
-v :反向选择,亦即显示出没有 ‘搜寻字符串’ 内容的那一行
2.21 zip/unzip指令
语法: zip/unzip 压缩文件.zip 【选项】 目录或文件
功能: zip将目录或文件压缩成zip格式,unzip是解压
常用选项:
-r 递 归处理,将指定目录下的所有文件和子目录一并处理
-d 解压到指定路径
举例:
将test2目录压缩:zip test2.zip test2/*
解压到tmp目录:unzip test2.zip -d /tmp
2.22 tar指令(重要)
语法: tar 【选项】文件或目录 …
功能: 打包/解包,不打开它,直接看内容
常用选项:
-c :建立一个压缩文件的参数指令(create 的意思);
-x :解开一个压缩文件的参数指令!
-t :查看 tarfile 里面的文件!
-z :是否同时具有 gzip 的属性?亦即是否需要用 gzip 压缩?
-j :是否同时具有 bzip2 的属性?亦即是否需要用 bzip2 压缩?
-v :压缩的过程中显示文件!这个常用,但不建议用在背景执行过程!
-f :使用档名,请留意,在 f 之后要立即接档名喔!不要再加参数!
-C : 解压到指定目录
案例:
范例一:将整个 /etc 目录下的文件全部打包成为
/tmp/etc.tar
[root@linux ~]# tar -cvf /tmp/etc.tar /etc<==仅打包,不压缩!
[root@linux ~]# tar -zcvf /tmp/etc.tar.gz /etc <==打包后,以 gzip 压缩
[root@linux ~]# tar -jcvf /tmp/etc.tar.bz2 /etc <==打包后,以 bzip2 压缩
特别注意,在参数 f 之后的文件档名是自己取的,我们习惯上都用 .tar 来作为辨识。
如果加 z 参数,则以 .tar.gz 或 .tgz 来代表 gzip 压缩过的 tar file ~
如果加 j 参数,则以 .tar.bz2 来作为附档名啊~
上述指令在执行的时候,会显示一个警告讯息:
『`tar: Removing leading `/" from member names`』那是关於绝对路径的特殊设定。
范例二:查阅上述 /tmp/etc.tar.gz 文件内有哪些文件?
[root@linux ~]# tar -ztvf /tmp/etc.tar.gz
由於我们使用 gzip 压缩,所以要查阅该 tar file 内的文件时,就得要加上 z 这个参数了!这很重要的!
范例三:将 /tmp/etc.tar.gz 文件解压缩在 /usr/local/src 底下
[root@linux ~]# cd /usr/local/src
[root@linux src]# tar -zxvf /tmp/etc.tar.gz
在预设的情况下,我们可以将压缩档在任何地方解开的!以这个范例来说,
我先将工作目录变换到 /usr/local/src 底下,并且解开 /tmp/etc.tar.gz ,
则解开的目录会在 /usr/local/src/etc 呢!另外,如果您进入 /usr/local/src/etc
则会发现,该目录下的文件属性与 /etc/ 可能会有所不同喔!
范例四:在 /tmp 底下,我只想要将 /tmp/etc.tar.gz 内的 etc/passwd 解开而已[root@linux ~]# cd /tmp
[root@linux tmp]# tar -zxvf /tmp/etc.tar.gz etc/passwd
我可以透过 tar -ztvf 来查阅 tarfile 内的文件名称,如果单只要一个文件,
就可以透过这个方式来下达!注意到! etc.tar.gz 内的根目录 / 是被拿掉了!
范例五:将 /etc/ 内的所有文件备份下来,并且保存其权限!
[root@linux ~]# tar -zxvpf /tmp/etc.tar.gz /etc
这个 -p 的属性是很重要的,尤其是当您要保留原本文件的属性时!
范例六:在 /home 当中,比 2005/06/01 新的文件才备份
[root@linux ~]# tar -N “2005/06/01” -zcvf home.tar.gz /home
范例七:我要备份 /home, /etc ,但不要 /home/dmtsai
[root@linux ~]# tar --exclude /home/dmtsai -zcvf myfile.tar.gz /home/* /etc
范例八:将 /etc/ 打包后直接解开在 /tmp 底下,而不产生文件!
[root@linux ~]# cd /tmp
[root@linux tmp]# tar -cvf - /etc | tar -xvf -
这个动作有点像是 cp -r /etc /tmp 啦~依旧是有其有用途的!
要注意的地方在於输出档变成 - 而输入档也变成 - ,又有一个 | 存在~
这分别代表 standard output, standard input 与管线命令啦!
2.23 bc指令
语法: bc
功能: bc命令可以很方便的进行浮点运算
说明: bc就是计算器,进入bc后,输入表达式,自动输出结果。
2.24 uname 指令
语法: uname 【选项】
功能: uname用来获取电脑和操作系统的相关信息。
说明: uname可显示linux主机所用的操作系统的版本、硬件的名称等基本信息。
常用选项:
-a或–all 详细输出所有信息,依次为内核名称,主机名,内核版本号,内核版本,硬件名,处理器类型,硬件平台类型,操作系统名称
2.25 重要的几个热键【Tab】,【Ctrl】+ C,【Ctrl】+ D,【ctrl】+ R
功能:
【Tab】按键—具有『命令补全』和『档案补齐』的功能。
【Ctrl】+ C按键—让当前的程序『停掉』。
【Ctrl】+ D按键—通常代表着:『键盘输入结束(End Of File, EOF 戒 End OfInput)』的意思;另外,他也可以用来取代exit。
【ctrl】+ R按键–输入历史命令相关的字符,会检索出来。
2.26 file指令和history指令
语法: file 【选项】 文件
功能: 查看文件的具体信息。
常用选项:
-c 详细显示指令执行过程,便于排错或分析程序执行的情形。
-z 尝试去解读压缩文件的内容。
使用 sudo分配权限
(1)修改/etc/sudoers 文件分配文件
chmod 740 /etc/sudoers
vi /etc/sudoer
格式:接受权限的用户登陆的主机 =(执行命令的用户) 命令
(2)使用 sudo 调用授权的命令
sudo –u 用户名 命令
语法: history
功能: 把你使用过的历史命令列举出来。
2.27 关机和重启
语法: shutdown 【选项】和reboot【选项】
shutdown常用选项:
-h : 将系统的服务停掉后,立即关机。
-r : 在将系统的服务停掉之后就重新启动
-t sec : -t 后面加秒数,亦即『过几秒后关机』的意思
2.28 扩展命令
◆ 安装和登录命令:login、shutdown、halt、reboot、install、mount、umount、chsh、exit、last;
◆ 文件处理命令:file、mkdir、grep、dd、find、mv、ls、diff、cat、ln;
◆ 系统管理相关命令:df、top、free、quota、at、lp、adduser、groupadd、kill、crontab;
◆ 网络操作命令:ifconfig、ip、ping、netstat、telnet、ftp、route、rlogin、rcp、finger、mail、 nslookup;
◆ 系统安全相关命令:passwd、su、umask、chgrp、chmod、chown、chattr、sudo ps、who;
◆ 其它命令:tar、unzip、gunzip、unarj、mtools、man、unendcode、uudecode。
2.29 工具类和其他指令(此项为临时记录,未详细整理)
ll:等于ls -l
which:查看当前命令的路径
alias:起别名
nano:编辑工具,类似win记事本
whoami:查看当前登录的用户名称
stat:查看文件的三个时间(Access、Modify、Change)
tree的安装:在root账户安装
yum install -y tree
yum install -y epel-release
man的安装:在root账户安装
yum install -y man-pages
echo:后面讲
3. Linux权限的概念
3.1 shell命令以及运行原理
Linux严格意义上说的是一个操作系统,我们称之为“核心(kernel)“ ,但我们一般用户,不能直接使用kernel。而是通过kernel的“外壳”程序,也就是所谓的shell,来与kernel沟通。如何理解?为什么不能直接使用kernel?
从技术角度,Shell的最简单定义:命令行解释器(command Interpreter)主要包含:
将使用者的命令翻译给核心(kernel)处理。
同时,将核心的处理结果翻译给使用者。
对比windows GUI,我们操作windows 不是直接操作windows内核,而是通过图形接口,点击,从而完成我们的操作(比如进入D盘的操作,我们通常是双击D盘盘符.或者运行起来一个应用程序)。
shell 对于Linux,有相同的作用,主要是对我们的指令进行解析,解析指令给Linux内核。反馈结果在通过内核运行出结果,通过shell解析给用户。
帮助理解:如果说你是一个闷骚且害羞的程序员,那shell就像媒婆,操作系统内核就是你们村头漂亮的且有让你心动的MM小花。你看上了小花,但是又不好意思直接表白,那就让你你家人找媒婆帮你提亲,所有的事情你都直接跟媒婆沟通,由媒婆转达你的意思给小花,而我们找到媒婆姓王,所以我们叫它王婆,它对应我们常使用的bash。
总结:shell是命令行解释器的统称,是一个大的类,而bash则是shell的一种具体的工具表现。
3.1.1 进程(此项为临时记录,未详细整理)
shell本身就是一个进程。
ps axj :查看系统进程。
3.2 Linux权限的概念
Linux下有两种用户:超级用户(root)、普通用户。
超级用户:可以再linux系统下做任何事情,不受限制
普通用户:在linux下做有限的事情。
超级用户的命令提示符是“#”,普通用户的命令提示符是“$”。
3.2.1 su指令
命令: su 【用户名】
功能: 切换用户。
说明:
要从root用户切换到普通用户user,则使用 su user。 要从普通用户user切换到root用户则使用 su root(root可以省略,加上-是切换到root根目录,不加则保持当前路径),此时系统会提示输入root用户的口令。
使用su命令每次切换用户,系统都会新建bash。
建议当频繁重复切换多次用户时,使用exit或者Ctrl+D退出当前用户返回到上次登录的用户,如果上次没有登陆过用户,则shell退出。
3.2.2 sudo指令
命令: sudo 要执行的命令
功能: 普通用户临时权限提升,以root身份执行。
说明: root需要将操作的普通用户添加到sudoers file,当前普通用户才可以使用sudo
配置普通用户使用sudo
(1)在root用户下修改/etc/sudoers 文件分配文件
vim /etc/sudoers
大约在100行处,按照root格式复制一行,把想要添加的用户名替换,wq!保存即可。
3.3 Linux权限管理
权限:一件事情是否允许被特定的人做。权限=人+事物的属性。
3.3.1 文件访问者的分类(人)
(1)文件和文件目录的所有者:u—User(中国平民 法律问题)
(2)文件和文件目录的所有者所在的组的用户:g—Group(不多说)
(3)其它用户:o—Others (外国人)
3.3.2 文件类型和访问权限(事物属性)
Linux系统中,不以文件的后缀作为区分文件类型的方式。但是gcc/g++等编译器是需要识别文件后缀的。
(1)文件类型
d:文件夹
-:普通文件(文本、各种动静态库、可执行程序、源程序等)
l:软链接(类似Windows的快捷方式)
b:块设备文件(硬盘、光驱等)
p:管道文件
c:字符设备文件(键盘、屏幕等串口设备)
s:套接口文件
(2)基本权限
(1)读(r/4):Read对文件而言,具有读取文件内容的权限;对目录来说,具有浏览该目录信息的权限
(2)写(w/2):Write对文件而言,具有修改文件内容的权限;对目录来说具有删除移动目录内文件的权限
(3)执行(x/1):execute对文件而言,具有执行文件的权限;对目录来说,具有进入目录的权限
(4)“—”表示不具有该项权限
3.3.3 文件权限值的表示方法
Linux系统下,一切皆是文件。
(1)字符表示方法
(2)8进制数值表示方法
3.3.4 文件访问权限的相关设置方法
3.3.4.1 chmod
语法: chmod 【参数】 权限 文件名
功能: 设置文件的访问权限
权限值的格式:
①用户表示符+/-=权限字符
+:向权限范围增加权限代号所表示的权限
-:向权限范围取消权限代号所表示的权限
=:向权限范围赋予权限代号所表示的权限
用户符号:
u:拥有者
g:拥有者同组用
o:其它用户
a:所有用户
实例:
chmod u+w /home/abc.txt
chmod o-x /home/abc.txt
chmod a=x /home/abc.txt
chmod u+rwx,g-rwx,o-rwx /home/abc.txt
②三位8进制数字表示实例:
chmod 664 /home/abc.txt //664->rw-rw-r–
chmod 640 /home/abc.txt //640->rw-r-----
说明: 只有文件的拥有者和root才可以改变文件的权限
常用选项:
R -> 递归修改目录文件的权限
3.3.4.2 chown
语法: chown【参数】 用户名 文件名
功能: 修改文件的拥有者,也可同时修改文件的拥有者和所属组。
说明: 普通用户修改文件拥有者,一般需要sudo;root可以随意修改文件的拥有者
常用选项:
-R 递归修改文件或目录的所属组
同时修改拥有者和所属组语法: chown 用户名(拥有者):用户名(所属组) 文件名
3.3.4.3 chgrp
语法: chgrp【参数】 用户组名 文件名
功能: 修改文件的所属组
说明: 普通用户修改文件所属组,一般需要sudo,前提是文件属于当前用户;root可以随意修改文件的所属组。值得注意的是:当文件属于当前用户的时候,可以任意更改所属组。
常用选项:
-R 递归修改文件或目录的所属组
3.3.4.4 umask
语法: umask 权限值
功能:
查看或修改文件掩码
新建文件夹默认权限=0666
新建目录默认权限=0777
但实际上你所创建的文件和目录,看到的权限往往不是上面这个值。原因就是创建文件或目录的时候还要受到umask的影响。假设默认权限是mask,则实际创建的出来的文件权限是: mask & ~umask
说明: 将现有的存取权限减去权限掩码后,即可产生建立文件时预设权限。超级用户默认掩码值为0022,普通用户默认为0002。用户自定义掩码,只在本次登陆有效。
3.4 目录的权限
可执行权限: 如果目录没有可执行权限, 则无法cd到目录中.
可读权限: 如果目录没有可读权限, 则无法用ls等命令查看目录中的文件内容.
可写权限: 如果目录没有可写权限, 则无法在目录中创建文件, 也无法在目录中删除文件.
现实中会有这样的情况:让一个用户可以在某个目录里创建文件和写入,但是不允许删除。
于是,问题来了:
如果用户具有目录的w权限,那么目录里的文件不管对这个用户有什么样的权限,都可以随便被删除,不管文件是不是属于当前用户。显然这是不科学的。
为了解决这个不科学的问题, Linux引入了粘滞位的概念。
3.5 粘滞位
语法: chmod +t 目录
说明: 粘滞位只能设置目录,一般是限制other权限的
实例:
[root@localhost ~]# chmod +t /home/ # 加上粘滞位
[root@localhost ~]# ls -ld /home/
drwxrwxrwt. 3 root root 4096 9月 19 16:00 /home/
[root@localhost ~]# su - litao
[litao@localhost ~]$ rm /home/abc.c #litao不能删除别人的文件
rm:是否删除有写保护的普通空文件 “/home/abc.c”?y
rm: 无法删除"/home/abc.c": 不允许的操作
当一个目录被设置为"粘滞位"(用chmod +t),则该目录下的文件只能由:
(1)超级管理员删除
(2)该目录的所有者删除
(3)该文件的所有者删除
3.6 关于权限的总结
(1)目录的可执行权限是表示你可否在目录下执行命令。
(2)如果目录没有-x权限,则无法对目录执行任何命令,甚至无法cd 进入目录, 即使目录仍然有-r 读权限(这个地方很容易犯错,认为有读权限就可以进入目录读取目录下的文件)。
(3)而如果目录具有-x权限,但没有-r权限,则用户可以执行命令,可以cd进入目录。但由于没有目录的读权限。
(4)所以在目录下,即使可以执行ls命令,但仍然没有权限读出目录下的文档。
二、Linux环境基础开发工具使用
1. Linux软件包管理器 yum
(1)什么是软件包:
(1)在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序.
(2)但是这样太麻烦了, 于是有些人把一些常用的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放在一个服务器上, 通过包管理器可以很方便的获取到这个编译好的软件包, 直接进行安装.
(3)软件包和软件包管理器, 就好比 “App” 和 “应用商店” 这样的关系.
(4)yum(Yellow dog Updater, Modified)是Linux下非常常用的一种包管理器. 主要应用在Fedora, RedHat, Centos等发行版上。
(2)关于 lrzsz:
安装指令:
yum install -y lrzsz
这个工具用于 windows 机器和远端的 Linux 机器通过 XShell 传输文件.
安装完毕之后可以通过拖拽的方式将文件上传过去.
(3)yum注意事项:
关于 yum 的所有操作必须保证主机(虚拟机)网络畅通!
可以通过 ping 指令验证
ping www.baidu.com
在centos里,只能有一个yum在运行。
(4)查看软件包:
通过 yum list 命令可以罗列出当前一共有哪些软件包. 由于包的数目可能非常之多, 这里我们需要使用 grep 命令只筛选出我们关注的包. 例如:
yum list
yum list | grep 软件名
注意事项:
(1)软件包名称: 主版本号.次版本号.源程序发行号-软件包的发行号.主机平台.cpu架构.
(2)“x86_64” 后缀表示64位系统的安装包, “i686” 后缀表示32位系统安装包. 选择包时要和系统匹配.
(3)“el7” 表示操作系统发行版的版本. “el7” 表示的是 centos7/redhat7. “el6” 表示 centos6/redhat6.
(4)最后一列, base 表示的是 “软件源” 的名称, 类似于 “小米应用商店”, “华为应用商店” 这样的概念.
(5)如何安装软件:
yum install 软件名
yum install -y 软件名
yum 会自动找到都有哪些软件包需要下载, 这时候敲 “y” 确认安装。执行命令的时候加上-y选项可以直接安装,无需确认。
出现 “complete” 字样, 说明安装完成。
注意事项:
(1)安装软件时由于需要向系统目录中写入内容, 一般需要 sudo 或者切到 root 账户下才能完成.
(2)yum安装软件只能一个装完了再装另一个. 正在yum安装一个软件的过程中, 如果再尝试用yum安装另外一个软件, yum会报错.
(6)如何卸载软件:
yum remove 软件名
yum remove -y 软件名
(7)更新yum源:
找到地址:/etc/yum.repos.d/CentOS-Base.repo
centos-base.repo文件:稳定源
centos-epel.repo文件:测试源
2. Linux编辑器-vim使用
vi/vim的区别简单点来说,它们都是多模式编辑器,不同的是vim是vi的升级版本,它不仅兼容vi的所有指令,而且还有一些新的特性在里面。例如语法加亮,可视化操作不仅可以在终端运行,也可以运行于x window、 mac os、windows。我们课堂上,统一按照vim来进行讲解。
2.1 vim的基本概念
vim的三种模式(其实有好多模式,目前掌握这3种即可),分别是命令模式(command mode)、插入模式(Insert mode)和底行模式(last line mode),各模式的功能区分如下:
(1)正常/普通/命令模式(Normal mode):
控制屏幕光标的移动,字符、字或行的删除,移动复制某区段及进入Insert mode下,或者到 last line mode
(2)插入模式(Insert mode):
只有在Insert mode下,才可以做文字输入,按「ESC」键可回到命令行模式。该模式是我们后面用的最频繁的编辑模式。
(3)末行模式(last line mode):
文件保存或退出,也可以进行文件替换,找字符串,列出行号等操作。 在命令模式下,shift+: 即可进入该模式。要查看你的所有模式:打开vim,底行模式直接输入:help vim-modes
我这里一共有12种模式:six BASIC modes和six ADDITIONAL modes.
2.2 vim的基本操作
(1)进入vim,在系统提示符号输入vim及文件名称后,就进入vim全屏幕编辑画面:
vim test.cpp
不过有一点要特别注意,就是你进入vim之后,是处于[正常模式],你要切换到[插入模式]才能够输入文字。
(2)正常模式切换至插入模式:
输入a、输入i、输入o都可以。
(3)正常模式切换至末行模式:
「shift + ;」, 其实就是输入「:」
(4)退出vim及保存文件,在[正常模式]下,按一下「:」冒号键进入「Last line mode」,例如:
: w (保存当前文件)
: wq (输入「wq」,存盘并退出vim)
: q! (输入q!,不存盘强制退出vim)
2.3 vim的正常模式命令集
(1)进入插入模式
按「i」切换进入插入模式「insert mode」,按“i”进入插入模式后是从光标当前位置开始输入文件;
按「a」进入插入模式后,是从目前光标所在位置的下一个位置开始输入文字;
按「o」进入插入模式后,是插入新的一行,从行首开始输入文字。
(2)从插入模式切换为命令模式
按「ESC」键。
(3)移动光标
vim可以直接用键盘上的光标来上下左右移动,但正规的vim是用小写英文字母「h」、「j」、「k」、「l」,分别控制光标左、下、上、右移一格
按「G」:移动到文章的最后
按[shift+g]:进入文本末端(和G一样)
按「 $ 」(shift + 4):移动到光标所在行的“行尾”
按「^」(shift + 6):移动到光标所在行的“行首”
按「w」:光标跳到下个字的开头
按「e」:光标跳到下个字的字尾
按「b」:光标回到上个字的开头
按「#l」:光标移到该行的第#个位置,如:5l,56l
按[gg]:进入到文本开始
按「ctrl」+「b」:屏幕往“后”移动一页
按「ctrl」+「f」:屏幕往“前”移动一页
按「ctrl」+「u」:屏幕往“后”移动半页
按「ctrl」+「d」:屏幕往“前”移动半页
(4)删除文字
「x」:每按一次,删除光标所在位置的一个字符。
「#x」:例如,「6x」表示删除光标所在位置的“后面(包含自己在内)”6个字符。
「X」:大写的X,每按一次,删除光标所在位置的“前面”一个字符。
「#X」:例如,「20X」表示删除光标所在位置的“前面”20个字符。
「dd」:删除/剪切光标所在行。
「#dd」:从光标所在行开始删除/剪切#行。
(5)复制
「yw」:将光标所在之处到字尾的字符复制到缓冲区中。
「#yw」:复制#个字到缓冲区。
「yy」:复制光标所在行到缓冲区。
「#yy」:例如,「6yy」表示拷贝从光标所在的该行“往下数”6行文字。
「p」:将缓冲区内的字符贴到光标所在位置。注意:所有与“y”有关的复制命令都必须与“p”配合才能完成复制与粘贴功能。
「#p」:重复粘贴#次。
(6)替换
「r」:替换光标所在处的字符。
「#r」:从当前光标所在位置往后替换n个字符。
「R」:替换光标所到之处的字符,直到按下「ESC」键为止(vim第四种模式—替换模式)。
(7)撤销上一次操作
「u」:如果您误执行一个命令,可以马上按下「u」,回到上一个操作。按多次“u”可以执行多次回复。
「ctrl + r」: 撤销的恢复。
(8)更改
「cw」:更改光标所在处的字到字尾处。
「c#w」:例如,「c3w」表示更改3个字。
「shift」+「~」:大小写互转。
(9)跳至指定的行
「ctrl」+「g」:列出光标所在行的行号。
「#G」:例如,「15G」,表示移动光标至文章的第15行行首。
(10)批量注释/去注释
命令模式下
「ctrl + v」: 批量选择。通过hjkl来选中区域。
「I + //」:添加注释。选中区域后,按大写I然后按//,再按Esc。
「d」:删除注释。选中区域后,按d就可以删除了。
(11)不退出vim执行Linux指令
按Esc进入命令模式
执行命令:!指令
2.4 vim末行模式命令集
在使用末行模式之前,请记住先按「ESC」键确定您已经处于正常模式,再按「:」冒号即可进入末行模式。
(1)列出行号
「set nu」: 输入「set nu」后,会在文件中的每一行前面列出行号。
「set nonu」: 取消显示行号。
(2)跳到文件中的某一行
「#」:「#」号表示一个数字,在冒号后输入一个数字,再按回车键就会跳到该行了,如输入数字15,再回车,就会跳到文章的第15行。
(3)查找字符
「/关键字」: 先按「/」键,再输入您想寻找的字符,如果第一次找的关键字不是您想要的,可以一直按「n」会往后寻找到您要的关键字为止。
「?关键字」:先按「?」键,再输入您想寻找的字符,如果第一次找的关键字不是您想要的,可以一直按「n」会往前寻找到您要的关键字为止。
问题:/ 和 ?查找有和区别?操作实验一下
(4)保存文件
「w」: 在冒号输入字母「w」就可以将文件保存起来
(5)离开vim
「q」:按「q」就是退出,如果无法离开vim,可以在「q」后跟一个「!」强制离开vim。
「wq」:一般建议离开时,搭配「w」一起使用,这样在退出的时候还可以保存文件。
(6)扩展
「!」:强制执行,如:w!、q!、wq!。
「vs 文件」:多文件操作, ctrl + w + w 多文件切换光标。
2.5 vim操作总结
(1)学习常用三种模式:正常模式、插入模式、底行模式。
(2)vim并不只有三种模式,一共有12种模式。
(3)vim主要操作:打开,关闭,查看,查询,插入,删除,替换,撤销,复制等等操作。
2.6 简单vim配置
2.6.1 配置文件的位置
(1)在目录 /etc/ 下面,有个名为vimrc的文件,这是系统中公共的vim配置文件,对所有用户都有效。
(2)而在每个用户的主目录下,都可以自己建立私有的配置文件,命名为:“.vimrc”。例如,/root目录下,通常已经存在一个.vimrc文件,如果不存在,则创建之。
(3)切换用户成为自己执行 su ,进入自己的主工作目录,执行 cd ~
(4)打开自己目录下的.vimrc文件,执行 vim .vimrc
常用配置选项,用来测试:
设置语法高亮: syntax on
显示行号: set nu
设置缩进的空格数为4: set shiftwidth=4
2.6.2 使用工具插件配置vim
推荐:VimForCpp: 快速将vim打造成c++ IDE
3. Linux编译器-gcc/g++使用
gcc和g++在使用上是一样的,gcc是C语言的编译器,g++是C++的编译器。
3.1 复习C语言编译原理
(1)预处理(头文件展开、宏替换、去注释、条件编译等)
(2)编译(生成汇编代码)
(3)汇编(生成机器可识别代码–二进制代码)
(4)链接(生成可执行文件或库文件)
3.2 gcc如何完成
格式 gcc [选项] 要编译的文件 [选项] [目标文件]
3.2.1 预处理(头文件展开、宏替换、去注释、条件编译等)
(1)预处理功能主要包括宏定义,文件包含,条件编译,去注释等。
(2)预处理指令是以#号开头的代码行。
(3)实例: gcc –E hello.c –o hello.i
(4)选项“-E”,该选项的作用是让 gcc 在预处理结束后停止编译过程。
(5)选项“-o”是指目标文件,“.i”文件为已经过预处理的C原始程序。
3.2.2 编译(生成汇编代码)
(1)在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。
(2)用户可以使用“-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。
(3)实例: gcc –S hello.i –o hello.s
3.2.3 汇编(生成机器可识别代码–二进制代码)
(1)汇编阶段是把编译阶段生成的“.s”文件转成目标文件
(2)读者在此可使用选项“-c”就可看到汇编代码已转化为“.o”的二进制目标代码了
(3)实例: gcc –c hello.s –o hello.o
命令:【op 文件】:可以以十六进制查看二进制文件
3.2.4 链接(生成可执行文件或库文件)
(1)在成功编译之后,就进入了链接阶段。
(2)实例: gcc hello.o –o hello
命令:【ldd】:可以查看执行程序的链接库
3.2.5 重要的概念:函数库
(1)我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢?
(2)最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用
3.2.5.1 函数库一般分为静态库和动态库两种
(1)静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为“.a”
(2)动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为“.so”,如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件,如下所示。 gcc hello.o –o hello
(3)gcc默认生成的二进制程序,是动态链接的,这点可以通过 file 命令验证。
3.2.6 gcc选项
-E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
-S 编译到汇编语言不进行汇编和链接
-c 编译到目标代码
-o 文件输出到 文件
-static 此选项对生成的文件采用静态链接
-g 生成调试信息。GNU 调试器可利用该信息。
-shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
-O0
-O1
-O2
-O3 编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
-w 不生成任何警告信息。
-Wall 生成所有警告信息。
gcc常见选项:作业题里的
-c 汇编完成后停止,不进行链接
-E 预处理完成后停止,不进行编译
-S 编译完成后停止,不进行汇编
-o 用于指定目标文件名称
-g 生成debug程序。向程序中添加调试符号信息
3.2.7 gcc选项帮助记忆
ESc对应的iso后缀的文件
4. Linux调试器-gdb使用
4.1 背景
(1)程序的发布方式有两种,debug模式和release模式
(2)Linux gcc/g++出来的二进制程序,默认是release模式
(3)要使用gdb调试,必须在源代码生成二进制程序的时候, 加上 -g 选项
4.2 常用指令
gdb普遍不受大家喜欢,也不做重点学习。但是也要了解,在特殊场景可以用起来gdp。
list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。
list/l 函数名:列出某个函数的源代码。
r或run:运行程序。
n 或 next:单条执行。
s或step:进入函数调用
break(b) 行号:在某一行设置断点
break 函数名:在某个函数开头设置断点
info break :查看断点信息。
finish:执行到当前函数返回,然后挺下来等待命令
print§:打印表达式的值,通过表达式可以修改变量的值或者调用函数
p 变量:打印变量值。
set var:修改变量的值
continue(或c):从当前位置开始连续而非单步执行程序
run(或r):从开始连续而非单步执行程序
delete breakpoints:删除所有断点
delete breakpoints n:删除序号为n的断点
disable breakpoints:禁用断点
enable breakpoints:启用断点
info(或i) breakpoints:参看当前设置了哪些断点
display 变量名:跟踪查看一个变量,每次停下来都显示它的值
undisplay:取消对先前设置的那些变量的跟踪
until X行号:跳至X行
breaktrace(或bt):查看各级函数调用及参数
info(i) locals:查看当前栈帧局部变量的值
quit:退出gdb
5. Linux项目自动化构建工具-make/Makefile
5.1 介绍
(1)会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力
(2)一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作
(3)makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
(4)make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
(5)make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。
5.2 实例代码
hello:hello.o
gcc hello.o -o hello
hello.o:hello.s
gcc -c hello.s -o hello.o
hello.s:hello.i
gcc -S hello.i -o hello.s
hello.i:hello.c
gcc -E hello.c -o hello.i
.PHONY:clean
clean:
rm -f hello.i hello.s hello.o hello
依赖关系:
上面的文件 hello ,它依赖 hell.o
hello.o , 它依赖 hello.s
hello.s , 它依赖 hello.i
hello.i , 它依赖 hello.c
5.4 原理
make是如何工作的,在默认的方式下,也就是我们只输入make命令。
(1)make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
(2)如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“hello”这个文件,并把这个文件作为最终的目标文件。
(3)如果hello文件不存在,或是hello所依赖的后面的hello.o文件的文件修改时间要比hello这个文件新(可以用 touch 测试),那么,他就会执行后面所定义的命令来生成hello这个文件。
(4)如果hello所依赖的hello.o文件不存在,那么make会在当前文件中找目标为hello.o文件的依赖性,如果找到则再根据那一个规则生成hello.o文件。(这有点像一个堆栈的过程)
(5)当然,你的C文件和H文件是存在的啦,于是make会生成 hello.o 文件,然后再用 hello.o 文件声明make的终极任务,也就是执行文件hello了。
(6)这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
(7)在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。
(8)make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
5.5 项目清理
(1)工程是需要被清理的
(2)像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。
(3)但是一般我们这种clean的目标文件,我们将它设置为伪目标,用 .PHONY 修饰,伪目标的特性是,总是被执行的。
6. Linux第一个小程序-进度条
6.1 回车和换行概念(\r \n)
(1)回车(\r)概念:回到当前行的最开始。
(2)换行(\n)概念:列不变,新起一行。
6.2 行缓冲区概念
(1)代码1执行结果:先输出字符串,再等待三秒结束程序。
int main()
{
printf("hello Makefile!\n");
sleep(3);
return 0;
}
(2)代码2执行结果:先等待三秒,在输出字符串后结束程序。
int main()
{
printf("hello Makefile!");
sleep(3);
return 0;
}
(3)代码3执行结果:先输出字符串,再等待三秒结束程序。
int main()
{
printf("hello Makefile!");
fflush(stdout);
sleep(3);
return 0;
}
结论:
通过上面三个例子,首先代码2肯定不是先执行sleep再执行printf,为什么先等待呢?因为没有“\n”,字符串会暂时保存起来,保存到用户C语言级别的缓冲区。
显示器设备刷新的策略是行刷新,“\n”即进行刷新。
代码3没有“\n”也能先输出字符串再等待,因为使用了fflush函数进行刷新。
6.3 进度条代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define NUM 100
int main(){
char bar[NUM+1];
memset(bar,'\0',sizeof(bar));
const char* lable = "|/-\\";
int a = 0;
while(a<=100){
printf("[%-100s][%3d%%][%c]\r", bar, a,lable[a%4]);
fflush(stdout);
bar[a] = '#';
a++;
usleep(50000);
}
printf("\n");
return 0;
}
7. 使用git-简介
学习笔记:Git的入门到熟练使用
安装git:
使用 git -v 查看当前系统是否已经安装git,如果没有执行安装命令安装就行。
yum install git
怎么把自己的代码上传到gitee?
推荐使用gitee平台:
(1)注册登录新建仓库。
(2)复制仓库的HTTPS连接
(3)下载项目到本地(路径根据自己的习惯选定)
执行下命令
git clone [url]
这里的 url 就是刚刚建立好的仓库链接
(4)查看本地代码和本地仓库之间的关系
git status
(5)将代码放到刚才下载好的目录中
git add [文件名]
ps:也可以直接cp文件夹到仓库目录中,再add文件夹
(6)提交改动到本地,提交的时候应该注明提交日志(log), 描述改动的详细内容.
git commit -m “log”
(7)同步到远端服务器上
git push
扩展:
配置免密码提交
三、Linux进程概念
1. 冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成:
(1)输入单元:包括键盘, 鼠标,扫描仪, 写板等
(2)输出单元:显示器,打印机等
(3)存储器(内存):内存是体系结构的核心设备
(4)中央处理器(CPU):含有运算器和控制器等
关于冯诺依曼,必须强调几点:
(1)这里的存储器指的是内存
(2)不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
(3)外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
(4)一句话,所有设备都只能直接和内存打交道。
2. 操作系统(Operator System)
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
(1)内核(进程管理,内存管理,文件管理,驱动管理)
(2)其他程序(例如函数库,shell程序等等)
设计OS的目的:
(1)与硬件交互,管理所有的软硬件资源
(2)为用户程序(应用程序)提供一个良好的执行环境
定位:
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
2.2 总结
计算机管理硬件:
(1)描述起来,C语言用struct结构体。
(2)组织起来,用链表或其他高效的数据结构。
(3)一句话,先描述再组织。实现对目标的管理转化成为对数据的管理。
系统调用和库函数概念:
(1)在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
(2)系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
系统调用接口和各类编程语言库函数有什么区别:
承上启下:
操作系统是怎么进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!
3. 进程
3.1 基本概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
ps:曾经我们所有的启动程序的过程,本质都是在系统上面创建进程
3.2 描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
进程控制块:一般是与进程相关的内核数据结构+进程的代码和数据。
task_struct是PCB的一种:
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类:
(1)标示符: 描述本进程的唯一标示符,用来区别其他进程。
(2)状态: 任务状态,退出代码,退出信号等。
(3)优先级: 相对于其他进程的优先级。
(4)程序计数器: 程序中即将被执行的下一条指令的地址。
(5)内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
(6)上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。进程在运行期间是有切换的,进程可能存在大量的临时数据,暂时在CPU的寄存器中保存。在单CPU的情况下,用户感受到的多个进程同时运行,本质上是通过CPU的快速切换完成的。
(7) I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
(8)记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
(9)其他信息
3.3 组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
3.4 查看进程
进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
大多数进程信息同样可以使用top和ps这些用户级工具来获取
top:显示linux的进程
ps:报告当前进程的快照
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
3.5 通过系统调用获取进程标示符
进程id(PID)
父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
在命令行上运行的命令,基本上父进程都是bash。
指令kill pid:可以通过pid关闭进程、
指令kill -l :查看信号
3.6 通过系统调用创建进程-fork初识
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
如何理解fork创建子进程?
(1)在OS角度,fork创建进程的方式和普通方式创建进程的方式是没有差别的。
(2)fork本质是创建进程,系统会多一个进程,默认情况下fork创建的子进程会继承父进程的代码和数据。内核数据结构task_struct也会以父进程为模板,初始化子进程的task_struct。
(3)fork之后,子进程和父进程代码是共享的。父子代码只有一份。代码是不可以被修改的。
(4)fork之后,默认情况下数据也是共享的,前提是父子进程都不被修改。进程是具有独立性的,通过“写时拷贝”来完成进程数据的独立性,如果要修改进程其中的数据,操作系统就会新建一个空间拷贝原来的数据,然后进行修改,进而不会影响另一个进程。
fork返回值:
(1)失败返回值小于0。
(2)成功返回的返回值是:
①父进程返回子进程的pid。
②子进程返回0。
fork 之后通常要用 if 进行分流:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if (ret < 0) {
perror("fork");
return 1;
}
else if (ret == 0) { //child
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}
else { //father
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
3.7 进程状态
Linux内核源代码:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
ps:进程在运行的时候,有可能运行需要,可能会在不同的队列里,在不同的队列里,所处的状态是不一样的。最简单的例子:用scanf录入键盘数据,scanf在运行队列排队的状态是R,当CPU执行到scanf时,要等待用户输入,此时CPU不可能让scanf在CPU里等待,所以需要把scanf换个状态S然后丢到等待队列里,等待完成用户输入完成,然后在回到运行队列里。
D磁盘休眠状态(Disk sleep): 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
ps:D状态一般是在长时间执行对磁盘的读写操作,当进程需要对磁盘长时间进行读写操作的时候会一直处于等待队列,时间久了难免会被操作系统杀掉,D状态就相当于“免死金牌”,处于D状态时,是不能被杀掉的!
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行(此时进程会在后台运行)。
X死亡状态(dead): 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程状态查看命令:
ps aux / ps axj
3.7.1 Z(zombie)-僵尸进程
(1)僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
(2)僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
(3)所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
创建一个维持30秒的僵死进程例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id > 0) { //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}
else {
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
编译并在另一个终端下启动监控
开始测试
看到结果
扩展资料:ptrace系统调用追踪进程运行,有兴趣研究一下
僵尸进程危害
(1)进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。
(2)维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
(3)那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
(4)内存泄漏。
(5)如何避免?后面讲
3.8 孤儿进程
(1)父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
(2)父进程先退出,子进程就称之为“孤儿进程”
(3)孤儿进程被1号init进程领养,当然要有init进程回收喽。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id == 0) {//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else {//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
3.9 进程优先级
基本概念:
(1)cpu资源分配的先后顺序,就是指进程的优先权(priority)。
(2)优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
(3)还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程:
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
其中几个重要的信息:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI 和 NI
(1)PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
(2)那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
(3)PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
(4)这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
(5)所以,调整进程优先级,在Linux下,就是调整进程nice值
(6)nice其取值范围是-20至19,一共40个级别。
PRI 对比 NI
(1)需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
(2)可以理解nice值是进程优先级的修正修正数据
用top命令更改已存在进程的nice:
top
进入top后按“r”–>输入进程PID–>输入nice值
nice值为什么要是一个相对比较小的范围呢?
优先级再怎么设置,也只是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进程“饥饿问题”。
调度器:
较为均衡的让每个进程享受到CPU资源。
其他概念:
(1)竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
(2)独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
(3)并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
(4)并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
4. 环境变量
基本概念:
(1)环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
(2)如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
(3)环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
常见环境变量:
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录).
SHELL : 当前Shell,它的值通常是/bin/bash。
查看环境变量:
echo $NAME //NAME:你的环境变量名称
测试PATH:
在Linux下,编译好的可执行程序,执行的时候往往需要带上路径+程序名,比如: ./test 。
可以通过一条指令,把test程序所在的路径添加到环境变量PATH中:export PATH=$PATH:test程序所在路径
。
这样就不需要带路径,可以直接执行了。
测试HOME:
和环境变量相关的命令:
(1)echo: 显示某个环境变量值
(2)export: 设置一个新的环境变量
(3)env: 显示所有环境变量
(4)unset: 清除环境变量
(5) set: 显示本地定义的shell变量和环境变量
ps:
export 本地变量 ->可以把本地转成全局变量
本地变量定义和初始化:变量名=val
环境变量的组织方式:
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
通过代码如何获取环境变量:
命令行第三个参数:
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
int i = 0;
for (; env[i]; i++) {
printf("%s\n", env[i]);
}
return 0;
}
通过第三方变量environ获取:
#include <stdio.h>
int main(int argc, char* argv[])
{
extern char** environ;
int i = 0;
for (; environ[i]; i++) {
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
通过系统调用获取或设置环境变量:
常用getenv和putenv函数来访问特定的环境变量:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
环境变量通常是具有全局属性的:
环境变量通常具有全局属性,可以被子进程继承下去:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* env = getenv("MYENV");
if (env) {
printf("%s\n", env);
}
return 0;
}
直接查看,发现没有结果,说明该环境变量根本不存在:
导出环境变量:
export MYENV="hello world"
再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!
5. 程序地址空间
研究背景:
kernel 2.6.32
32位平台
C语言的控价布局图:
通过代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 0;
}
else if (id == 0) { //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else { //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出结果:
//与环境相关,观察现象即可
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8
我们发现,输出出来的变量值和地址是一模一样的,这很好理解,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 0;
}
else if (id == 0) { //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
g_val = 100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else { //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出结果:
//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
(1)变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
(2)但地址值是一样的,说明,该地址绝对不是物理地址!
(3)在Linux地址下,这种地址叫做 虚拟地址
(4)我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
(5)OS必须负责将 虚拟地址 转化成 物理地址 。
地址空间本质是内核中的一种数据类型:struct mm_struct{}
Linux下虚拟地址也叫线性地址!
6. 进程的地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
分页和虚拟地址空间:
上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
为什么要有地址空间?
1.通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存以及各个进程的数据安全!
2.将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和os进行内存管理操作,进行软件上面的分离!
3.站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的!
总结:
OS最终这样设计的目的,达到一个目标,每个进程都认为自己试独占系统资源的!
进程 = PCB+地址空间+页表+MMU+映射关系
7. Linux2.6内核进程调度队列
上图是Linux2.6内核中进程队列的数据结构,之间关系也已经给大家画出来,方便理解。
一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的负载均衡问题
优先级
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级:0~99(不关心)
活动队列
(1)时间片还没有结束的所有进程都按照优先级放在该队列
(2)nr_active: 总共有多少个运行状态的进程
(3)queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
(4)从该结构中,选择一个最合适的进程,过程是怎么的呢?
①从0下表开始遍历queue[140]
② 找到第一个非空队列,该队列必定为优先级最高的队列
③拿到选中队列的第一个进程,开始运行,调度完成!
④遍历queue[140]时间复杂度是常数!但还是太低效了!
(5)bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
过期队列
(1)过期队列和活动队列结构一模一样
(2)过期队列上放置的进程,都是时间片耗尽的进程
(3)当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
(1)active指针永远指向活动队列
(2)expired指针永远指向过期队列
(3)可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
(4)没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
总结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!
四、Linux进程控制
1. fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
(1)分配新的内存块和内核数据结构给子进程
(2)将父进程部分数据结构内容拷贝至子进程
(3)添加子进程到系统进程列表当中
(4)fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
int main(void)
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ((pid = fork()) == -1)perror("fork()"), exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork函数返回值
子进程返回0,
父进程返回的是子进程的pid。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
2. 进程终止
进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
(1)从main返回
(2)调用exit
(3)_exit
异常退出:
ctrl + c,信号终止
_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。
exit函数
#include <unistd.h>
void exit(int status);
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
实例:
int main()
{
printf("hello");
exit(0);
}
运行结果:
[root@localhost linux] # . / a.out
hello[root@localhost linux]#
int main()
{
printf("hello");
_exit(0);
}
运行结果:
[root@localhost linux] # . / a.out
[root@localhost linux]#
return退出
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
打印输出当前系统返回值的含义:
for(int i = 0; i< 150; ++i){
printf("%d: %s\n", i, strerror(i));
}
printf(“%s”, strerror);
3. 进程等待
3.1 进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法
杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退
出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
3.3 获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特
位):
测试代码:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void)
{
pid_t pid;
if ((pid = fork()) == -1)
perror("fork"), exit(1);
if (pid == 0) {
sleep(20);
exit(10);
}
else {
int st;
int ret = wait(&st);
if (ret > 0 && (st & 0X7F) == 0) { // 正常退出
printf("child exit code:%d\n", (st >> 8) & 0XFF);
}
else if (ret > 0) { // 异常退出
printf("sig code : %d\n", st & 0X7F);
}
}
}
测试结果:
[root@localhost linux]# . / a.out #等20秒退出
child exit code : 10
[root@localhost linux]# . / a.out #在其他终端kill掉
sig code : 9
具体代码实现
(1)进程的阻塞等待方式:
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0) { //child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(257);
}
else {
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
运行结果:
[root@localhost linux] # . / a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is : 1.
(2)进程的非阻塞等待方式:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0) { //child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(1);
}
else {
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if (ret == 0) {
printf("child is running\n");
}
sleep(1);
} while (ret == 0);
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
4. 进程程序替代
4.1 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数
以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动
例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
4.2 替换函数
其实有六种以exec开头的函数,统称exec函数:
#include <unistd.h>`
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
4.3 函数解释
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
4.3 命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
exec调用举例如下:
#include <unistd.h>
int main()
{
char* const argv[] = { "ps", "-ef", NULL };
char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在
man手册第3节。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子:
5. 实现一个简易的shell
考虑下面这个与shell典型的互动:
[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左
向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结
束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(wait)
根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。
实现代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{
memset(command, 0x00, MAX_CMD);
printf("minishell$ ");
fflush(stdout);
if (scanf("%[^\n]%*c", command) == 0) {
getchar();
return -1;
}
return 0;
}
char** do_parse(char* buff)
{
int argc = 0;
static char* argv[32];
char* ptr = buff;
while (*ptr != '\0') {
if (!isspace(*ptr)) {
argv[argc++] = ptr;
while ((!isspace(*ptr)) && (*ptr) != '\0') {
ptr++;
}
}
else {
while (isspace(*ptr)) {
*ptr = '\0';
ptr++;
}
}
}
argv[argc] = NULL;
return argv;
}
int do_exec(char* buff)
{
char** argv = { NULL };
int pid = fork();
if (pid == 0) {
argv = do_parse(buff);
if (argv[0] == NULL) {
exit(-1);
}
execvp(argv[0], argv);
}
else {
waitpid(pid, NULL, 0);
}
return 0;
}
int main(int argc, char* argv[])
{
while (1) {
if (do_face() < 0)
continue;
do_exec(command);
}
return 0;
}
在继续学习新知识前,我们来思考函数和进程之间的相似性
exec/exit就像call/return
一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的
操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程
序之内的模式扩展到程序之间。如下图:
一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来
返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。
五、基础IO
软链接:ln -s 源文件名 新文件名
硬链接:ln 源文件名 新文件名
inode
AMC
动态库/静态库
C/C++动态库/静态库的安装:
六、进程间的通信
七、进程信号
信号done: