LINUX学习笔记

Linux

一、Linux基础

1.1、基础知识

主键盘快捷键:
上:crtl+p —previous
下:crtl+n —next
左:crtl+b —backward
右:crtl+f —forwardd
Del:ctrl+d —delete光标后面的
Home:crtl+a —the first letter
End:ctrl+e —end
Backspace:backspace —delete光标前面的

sudo su —切换为root用户;
exit —可以退出root用户

目录和文件: “所见皆文件”
在这里插入图片描述
如上图,系统目录下:
bin —存放二进制可执行文件;binary
boot —存放开机启动例程;
dev --存放外接设备文件(屏幕,键盘 ,。。。),在其中的外界设备是不能直接使用的,需要自己挂载(类似windows下的分配盘符)。
etc —存放当前系统的用户信息和相关配置文件信息;
lib —库文件
media/mnt —挂载、卸载磁盘相关;
opt/proc —linux系统编程进程相关;process
root — 管理员宿主目录
usr — 存放第三方的库和文件,用户资源管理目录
home —家目录(除了root用户的其他目录的家目录)
sbin — super binary ,该目录下也存放一些可以被执行的二进制文件,但是必须要有super权限的用户才能执行。
tmp — 表示临时的,存放系统的临时文件
var —存放的程序和系统的日志文件目录

linux文件类型
普通文件: -,搜索时用f替换
目录文件: d —directory
字符设备文件:c — char
块设备文件:b —block
软连接:l —link
管道文件:p — pipe
套接字文件:s ----socket
未知文件。

常见命令格式: 指令 [选项] 操作对象 ,只能一个指令主题,多个选项,多个对象

1.2、基础命令

ls
路径分为相对路径(相对于参照物(当前工作路径))和绝对路径。

//相对路径
./  //表示当前目录下   --- 等价于不写
../  //表示上一级目录下
//绝对路径
//   直接使用/从根目录开始
ls 	//显示所有文件(不包括隐藏)
ls -a	//显示所有文件(包括隐藏)
ls -r	// reverse,逆序显示文件(不包括隐藏)
ls -d	//显示目录
ls -R	//recursion,递归显示所有文件详细信息
ls -l filename	//显示文件详细属性
ls -l dname 	//显示该目录下文件的详细信息
ls -f	//显示文件类型,后缀
ls -h	//显示可读性更高的文件的大小,文件夹大小一般为4k
ls -s //显示大小
/*详细信息,分别是: 文件类型,权限(所有者的读写可执行,同组用户的读写可执行,其他人的读写可执行),硬连接技术,所有者,所属组,大小,时间,文件名*/

ls列出的结果颜色说明:蓝色表示文件夹,黑色表示文件,绿色表示其权限为拥有所有权限。

cd

cd dn 	//跳转指定目录
cd ~ //跳转到用户根目录
cd / //跳转到系统根目录
cd .. //退回上一级
cd - //返回上一次目录

which
查看指定命令所在路径

which 命令

pwd
查看当前所在路径 print working directory
mkdir
创建目录 make directory ,可以用路径/目录名在指定目录下穿件

mkdir ./a/b/c/d  //不能成功
mkdir -p ./a/b/c/d     //可以一次性创建多层目录
mkdir a b c //一次性创建多个
mkdir a -m 777 //---指定权限

rm
删除文件或目录

-f	//忽略警告信息force
-i	//删除前询问
-r	//递归删除文件夹 经常使用-rf
//删除具有公共特性的文件,若当前目录下有 aa11 , aa22, aa33三个文件
rm -f aa*  //*称为通配符,只要前面匹配即可.

rmdir
移除空目录
mv
用于改名或移动文件

mv aaa bbb
mv bbb /src
mv ~/ws ./   //移动根目录下的ws文件夹到 当前目录下
 a

touch
创建空文件 可以是 直接打文件名,也可以是路径/文件名,创建到指定路径下

touch test	//创建空文件
touch -a  	//仅修改访问时间
touch -m 	//仅修改更改时间
touch -d 	//同时修改访问和更改时间
touch -t	//要修改的时间【YYMMDDhhmm】
touch -d "2 days ago"	test

cp

//copy文件:  cp +文件名+目标目录 
cp test /home   // 将当前目录下的test文件 复制到/home
cp ./user/test /home   //将user目录下的文件test复制到/home

//复制出新文件:cp + 文件 + 文件
cp test test1
//拷贝目录:cp -a +目录+目标目录
cp -a ./a ./c  //将当前目录下的目录a复制到c目录下 

-p	//保留原始文件属性
-d	//若对象为“链接文件”,则保留链接文件的属性
-r	//递归持续复制 ,可以用于文件夹复制,这里r,R一样
-i	//若存在则询问是否覆盖
-a	//相当于-pdr

cat
读取并显示文件:cat 文件名;
倒着显示文件内容:tac 文件名

cat 文件名
cat -n //显示行号
cat -b //显示行号(不包括空行)
cat -A //显示不可见的符号,空格,tab等
cat 路径/文件 路径/文件 ... > 合并后的路径/文件名  //合并多个文件为新文件,原文件不会改变(前面是打开多个文件,配合输出重定向)


od:

//od命令用于查看特殊格式的文件
od -t a 文件名	//默认字符
od -t c			//ASCII
od -t o			//8进制
od -t d			//十进制
od -t x			//16
od -t f			//浮点数

输出重定向:
一般命令的输出都会显示在终端中,有些时候需要将一些命令的执行结果保存在文件中进行使用,则需要使用到输出重定向技术。

>:  覆盖输出,会覆盖掉原先的文件内容
>>: 追加输出,会在原始文件末尾继续添加

1.3、进阶指令

df:
查看磁盘空间

// disk file
df 
df -h //较高的可读性显示,经常使用这个

free:
查看内存使用情况

free
free -m //以m为单位
free -g //以g为单位

head:
用于查看纯文本的前n行,不指定就显示前10行

head -n 20 文件名 //显示前20行    
//等价于 head -20 文件名
head -n -10 文件名 	//	不显示最后10行
// -c    --显示前多少字节

tail:
用于查看纯文本后N行,不指定就显示后10行

tail -n 20 文件名	//显示后面20行
tail -f 文件名	//持续刷新显示内容(不能是用户手动添加),用于查看日志

less:
以较少的内容进行输出,按下辅助键查看更多。

less 文件名   //输入q退出
//数字+回车,跳转到指定行
//回车+上下键,上下翻

more:
用于查看纯文本(较长的)


more -数字 文件名	//预先显示的行数,默认一行
more -d 文件名	//显示提示语句和报错信息

wc:
用于统计行数,字数,字节数


wc -l //只显示行数
wc -w	//只显示单词数,以空格区分
wc -c 	//只显示字节数

date:
表示操作时间和日期(读取和设置)

date //显示时间
date +%F     //2022-04-20 full day
date "+%F %T"   //2022-04-20 09:10:26  Time,中间的连接是空格,也可以是其他的
date "+%Y-%m-%d"  //注意大小写   //2022-04-20
date "+%Y-%m-%d %H:%M:%S" //2022-04-20 09:10:26
//+是读取,""使得内容成为整体

//获取之前时间或之后时间
date  -d "-1 day" "+%Y-%m-%d %H:%M:%S"
date  -d "+1 year" "+%Y-%m-%d %H:%M:%S"

// %F 完整年月日, %T 完整时分秒, %Y 四位年份, %m 两位月份(带前导0),%d 表示日期, %H 小时, %M 分钟, %S 秒

cal:
操作日历calendar

cal  //当前月的日历   == cal -s
cal -1	//当前月的日历
cal -3 //前一个月+当前+后一个月
cal -y //一年的日历
cal -m 2 //单独显示同年2月

clear :
清除(不是真正的)终端中已经存在的命令和信息.等价于ctrl+L
grep :
按文件内容找文件

grep 'xxx' ./
grep -r 'xxx' ./    //递归
grep -r 'xxx' ./ -n   //显示具体行数

管道 | :
作用:过滤,特殊,扩展处理;不能单独使用,必须配合前面指令,起辅助左右

//过滤案例,需要通过管道查询出根目录下包含字母‘y’的文档名称
ls /|grep y
//以管道为分隔符,前面的命令有输出,后面需要先输入,在过滤,最后在输出,通俗的讲就是前面的输出就是后面的输入;
//grep指令:主要用于过滤

//特殊,通过管道的操作实现less命令的等价效果(了解就行)
cat test|less

//扩展处理:请统计某个目录下的文档总个数
ls / | wc -w

1.4、高阶指令

hostname:
操作服务器的主机名(读取,设置(少))

hostname       //输出完整的主机名
hostname -f      //输出当前的主机名中的FQDN(全限定域名)

id :
用于查看用户的一些基础信息(用户id,用户组id等)
不指定用户,就显示当前用户

id
id 用户名

验证 id 给出的信息是否准确? 验证用户信息 :etc/passwd;验证用户组信息:etc/group
whoami:
查看当前登录用户,一般用于shell脚本
ps :
用来监控工作进程 processes snapshot(快照)

ps   //只显示可以交互的进程?
ps -aux    //显示所有的进程--常用
ps aux | grep 'xsdsd'   //结合使用,检索进程结果集
ps -e //显示全部进程, 等价于'A'
ps -f //显示全部的列
ps -ajx  //常用显示进程id,父进程id,等
//UID 该进程执行用户的id;PID 进程id; PPID 父进程id;如果一个程序的父进程找不到了,则该程序的进程称为孤儿进程。
//  C cpu的占用率,百分数表示;STIME 进程启动时间;TTY 终端设备,发起该进程的设备识别符号,显示的问号,则表示不是由终端发起。
//TIME 进程的执行时间;CMD 该进程的名称或者对应的路径

top:
查看系统状态

//进入
top
/*表头id ; PID 进程id;USER 该进程对应的用户;PR 优先级;VIRT:虚拟内存(申请多少就是多少);RES:常驻内存(实际使用);
SHR:共享内存(该进程调用其他进程的消耗) 计算一个进程的实际内存 = RES-SHR;  S:表示进程状态(sleeping,S表示睡眠,R表示运行);&CPU:表示CPU占用百分比;%MEN :表示内存的占用百分百;
TIME+:执行的时间;COMMAND: 进程的名字或者路径;
*/

//运行中快捷键
/*
M:将结果按照内存从高到低
P:将结果按照cpu从高到低
1:若有多个cpu时,切换是否显示各个cpu的详细信息
*/
//退出
q

du -sh:
查看文件夹的真实大小,-s ,只显示汇总的大小,-h,表示以较高可读性显示
Disk Usage

du -sh 路径     //统计该路径下的文件夹大小

find:
----查找与检索文件
-maxdepth num 可以指定搜索深度
-type 按类型搜索 文件用f
-name 按文件名搜索
-size 按文件大小 (k,M,G)
-atime,mtime,ctime 分别表示按最后访问,属性修改,内容修改
-amin,mmin,cmmin

find ./ -maxdepth 2 -name "*.jpg"
find ./ -type l
find ./ -size +20M -size -50M   //k

-exec 将find搜索的结果执行某一指定命令

  • ok 以交互形式,将find搜索结果执行某一指定命令
find /user/ -name "*tmp*" -exec ls -l {} \;
//将找到的结果执行某个操作(不询问)
find /user/ -name "*tmp*" -ok rm -r {} \; //会询问

service:
用于控制一些软件的服务启动,停止,重启

//显示正在运行的服务
service --status-all
service --status-all|more
service --status-all|less

service 服务名 start/stop/restart
//需要启动本机安装的Apache(网站服务器软件),其服务名:httpd
service httpd start

kill:
杀死进程

kill 进程id
killall 进程名称

ifconfig:
操作网卡的指令

ifconfig

reboot:
重新启动计算机

reboot
reboot -w   //模拟重启,但是不重启(只写开关机的日志信息)

shutdown:
关机(远程服务器慎用)

shutdown -h now         //立即关机
shutdown -h 15:25        //定时关机
//取消关机
shutdown -c    或者ctrl+c
//除了shutdown 其他关机命令: init 0;halt;poweroff

uptime
输出计算机的持续在线时间
uname
获取计算机操作系统的相关信息

uname   //获取操作系统的类型LINUX
uname -a   //获取详细信息

netstat -tnlp
查看网络连接状态

netstat -tnlp
//-t 表示只列出TCP协议;-n 表示将地址从字母组合转换为ip地址,将协议转换为端口号显示;-l表示过滤出“状态state”列中其值为LISTEN(监听)的连接;-p表示显示进程的pid和进程名称

man
查找帮助命令

man 指令   //退出按q

习题
如何在命令行快速删除光标前/后的内容? 前 : ctrl+u ; 后:ctrl + k
如何删除/tem下所有A开头的文件? rm -f /tem/A*
系统文件需要备份,如何把/etc/passwd备份到/tem目录下?cp /etc/passwd /tem/
如何查看系统最后创建的3个用户? tail -3 /etc/passwd
什么命令可以统计当前系统一共有多少个用户? wc -l /etc/passwd
如何创建/tem/test.conf文件? touch /tem/test.conf
如何通过vim编辑打开/tem/test.conf文件? vim /tem/test.conf
如何查看/etc/passwd的头3行和尾3行? head -3 /etc/passwd , tail -3 /etc/passwd
如何一次性创建目录/test/1/2/3/4? mkdir -p /test/1/2/3/4
如何快速返回当前用户家命令? cd ~ , cd
如何查看/etc所占的磁盘空间? du -sh /etc
如何删除/tem下所有文件? rm -rf /tmp/*
尝试启动Apache的服务,并且检查是否成功启动? service httpd start , ps -ef |grep httpd
杀死Apache的进程? kill all httpd

1.5、vim使用

1.5.1、vim基本知识

vim的重点:光标的移动、模式切换、删除、查找、替换、复制、粘贴、撤销命令的使用。
vim的三种模式:命令模式、编辑模式、末行模式
①命令模式下,不能对文件直接编辑的,可以输入快捷键进行一些操作(删除行、复制行、移动光标、粘贴等等);—打开文件默认进入模式
②编辑模式下,可以对文件内容进行编辑
③末行模式下,可以在末行输入命令来对文件进行操作(搜索、替换、保存、退出、撤销、高亮、…)

vim打开文件的方式:

//1 打开指定的文件
vim 文件路径
//2 打开指定的文件,并且将光标移动到指定行
vim +number 文件路径  
// 3 打开指定的文件,并且高亮显示关键词
vim +/关键词 文件路径
// 同时打开多开文件
vim 文件路径1 文件路径2 文件路径3

1.5.2、模式间的相互切换

命令模式——————>末行模式 : 输入: : 或 /为查找或搜索
末行模式——————>命令模式 : 输入: 一下或两下Esc或直接删除:
命令模式——————>编辑模式: 输入: 按i、a等
编辑模式——————>命令模式: 输入: 按一下Esc

1.5.3、命令模式

注意:该模式是打开文件的第一个看到的模式(打开文件即可进入)
1、光标移动
光标移动到行首: shift + ^(字母上的6)
光标移动到行尾: shift + $
光标移动到首行: gg
光标移动到末行: G
向上翻屏 : ctrl+b or pageup
向下翻屏 : ctrl+f or pagedown
2、复制操作
复制光标所在行:yy 在想要粘贴的位置按下 p键
以光标所在行为准,向下复制指定行数(包含光标所在行): 数字 + yy 在想要粘贴的位置按下 p键
可视化的复制:ctrl + v 然后按方向键选中所需要复制的区块,按下yy键进行复制,在按p键粘贴,Esc退出
3、剪切/删除
剪切/删除光标所在行,下一行上移:dd (严格属与剪切,只剪切不粘贴,就是删除),在想要粘贴的位置按下 p键
剪切/删除光标所在行为准,向下剪切/删除指定行: 数字 + dd
剪切/删除光标所在行,但是下一行不上移:D
4、撤销/恢复
撤销 : :+u 或者 u
恢复: ctrl +r 取消之前的撤销操作
5、光标的快速移动
快速移动到指定行: 数字+G
以当前光标为准向上/下/左/右移动n行/字符: 数字+方向键
末行模式下的快速移动方式:移动到指定行 : :数字+回车

1.5.4、末行模式

1、保存操作: :w :x —修改了就保存,没修改就是退出
2、另存操作: :w 路径
3、退出操作: :q
4、保存并退出: :wq
5、强制: :q! 表示强制退出,刚才做的修改不做保存
6、调用外部命令: :!命令 ----可以在不退出的情况下执行外部命令
7、搜索/查找: /关键词
在搜索结果中切换上下结果:N/n
需要取消高亮::nohl ---------- no highlight
8、替换:g—global % —整个文档
: s/需要被替换的关键词/新的内容 ----替换光标所在行的第一处符合条件的内容
: s/需要被替换的关键词/新的内容/g -----替换光标所在行的所有符合条件的内容
: %s/需要被替换的关键词/新的内容 -----替换整个文档中每行第一个符合条件的内容
: %s/需要被替换的关键词/新的内容/g ------替换整个文档中所有符合条件的内容
9、显示行号: :set nu
10、取消行号: :set nonu
11、使用vim同时打开多个文件,在末行模式下切换文件:
查看当前已经打开文件的名称: :files ----结果中%a 表示当前正在打开的文件,#表示上一个打开的文件。
切换文件的方式:
①: :open 文件名
②: :bn ----切换到下一个(back next); :bp -----切换到上一个(back previous)

1.5.5、编辑模式

2个重点的进入方式:i (insert,在光标所在字符前开始插入),a(after,在光标所在字符后开始插入)
退出: 按一下Esc

1.5.6、实用功能

①代码着色:
显示: :syntax on
关闭显示: :syntax on
②计算器:
当在编辑文件是突然需要使用计算器去计算,则此时需要用计算器,但是需要退出,vim自身集成了一个简易的计算器。
解决办法:进入编辑模式,按ctrl+R,然后输入’=',此时光标会变到最后一行,输入需要计算的内容,按下回车。

1.5.7、扩展内容

①vim的配置
1、在文件打开的时候在末行模式下输入的配置(临时的)
2、个人配置文件(~/.vimrc 没有就新建一个)
打开配置文件后就可以编辑配置,如显示行号:set nu

3、全局配置文件(vim自带)(/etc/vimrc) 配置如上。
*** 个人配置文件的优先级高于全局配置文件。

②异常退出
什么是异常退出:在编辑完文件后,没有正常退出。
解决办法:将交换文件(.文件名.swp)删除即可:rm -f .文件名.swp
③别名机制
作用:相当于创建属于自己的自定义命令。依靠一个别名映射文件:在~/.bashrc中加入映射。然后source ~/.bashrc

alias cls='clear'

④退出方式
之前退出编辑文件的方法: :q或者 :wq
还支持一个保存退出的方法: :x 在文件没有修改的情况下,表示退出,在修改后,表示保存退出。原因在于,如果文件没有被修改,但是按:wq进行退出的话,则文件的修改时间会更新;但是使用:x的话,就不会更新文件的修改时间。建议使用:x 注意是小写x,不要使用X,不用使用X,不要使用X,X表示对文件加密操作。

1.6、linux自有服务

自有服务,即不需要用户独立去安装软件的服务,当系统安装好后就可以直接使用的服务

1.6.1、运行模式

在linux中存在一个进程:init(initialize,初始化),进程id是1,该进程存在一个对应的配置文件:inittab(系统运行级别配置文件,位置在/etc/inittab)(ubuntu下没找到)
相关命令: init 0 ----关机; init 6 ----重启; init 3 —切换到不带桌面的模式;init 5 —切换到图像界面

1.6.2、设置主机名

查看用户名: hostname
hostname -f
临时设置主机名(需要切换用户使其生效)
hostname 设置的主机名
hostname ttt
su
永久设置主机名(需要重启)
1、找到配置文件/etc/hostname    ---(主机名的配置文件)
2、修改其中的HOSTNAME
3、找到/etx/hosts 
4、添加新主机名

1.6.3、chkconfig

作用:相当于windows下安全卫士,电脑管家之类的安全辅助工具提供的“开机启动项”的一个管理服务。在linux下不是所有软件安装完成后都有开机启动服务。

开机启动服务查询 ---- ubuntu好像没有了
chkconfig --list
删除服务
chkconfig --del 服务名
添加服务
chkconfig --add 服务名
设置服务在某个级别下开机启动
chkconfig --level 35 服务名 on/off ---在级别35下默认启动

1.6.4、ntp服务

作用:ntp主要 用于对计算机的时间同步管理操作

一次性同步时间(简单)
ntpdate 时间服务器的域名或ip地址
设置时间同步服务  服务名:ntpd
启动服务
service ntpd start
开机启动
chkconfig --level 35 ntpd on

1.6.5、防火墙服务

作用:防范一些网络攻击,有软件防火墙和硬件防火墙
选择性让请求通过,从而保证网络安全

ubuntu下 使用
sudo ufw status 查看防火墙状态
sudo ufw enable/disable  开启/关闭防火墙
sudo ufw reload 重启
sudo ufw allow 21  //开启端口21
sudo ufw delete allow 21           //关闭21端口
sudo ufw allow 8001/tcp            //指定开放8001的tcp协议
sudo ufw delete allow 8001/tcp        //关闭端口

1.6.6、cron/crontab 计划任务

作用:操作系统不可能24小时有人在操作,有时候想要在指定时间点去执行时间,需要交给计划任务去执行操作。

crontab 选项
-l     //list,列出指定用户的计划任务列表
-e     // edit 编辑指定用户的计划任务列表
-u     // user 指定用户名,若不指定,表示当前用户
-r     // remove 删除指定用户的所有计划任务列表

//编辑规则
以行为单位,一行就是一个计划
分 时 日 月 周 需要执行的命令
*表示取值范围中每一个数字
- 做连续区间表达式;1-7表示17
/ 表示每多少个;每10分钟一次,在分的位置*/10
, 表示多个取值;
分:0-59
时:0-23
日:1-31
月:1-12
周:0-60表示星期天)
//每月1,10,22日的4:45重启network服务
45 4 11022 * * service network restart
//每周六,周日的1:10重启network服务
10 1 * * 0,6 service network restart
//每天18:00到23:00之间每隔30分钟重启network服务
*/30 18-23 * * * service network restart
//每隔两天的上午8点到11点的第3和15分钟执行一次network重启
315 8-11 */2 * * service network restart

crontab权限问题:本身是任何用户都可以创建自己的计划任务。但是超级管理员可以通过配置来设置某些用户不能设置计划任务。配置文件: /etc/cron.deny ----ubuntu中没有

1.7、用户和用户组

Linux是一个多用户多任务的操作系统,任何一个要使用系统资源的用户,必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统。用户的账号一方面可以帮助系统管理员对使用系统的用户进行跟踪,并控制他们对系统资源的访问;另一方面也可以帮助用户组织文件,并为用户提供安全性保护。每个用户账号都拥有一个唯一的用户名和各自的密码。用户在登录时键入正确的用户名和密码后,就能够进入系统和自己的主目录。要想实现用户账户的管理,要完成的工作主要有如下几个方面:用户账号的添加,删除,修改以及密码的管理;用户组的管理。
注意3个文件:
/etc/passwd ------存储用户的关键信息
/etc/group --------存储用户组的关键信息
/etc/shadow ---------存储用户的密码信息

1.7.1、用户管理

①添加用户

useradd 选项 用户名
选项:
-g :表示指定用户的用户主组,选项的值可以是用户组的id或用户名
-G :表示指定用户的用户附加组,选项的值可以是用户组的id或用户名
-u : uid,用户的id(用户的标识符),系统会默认从500按顺序开始。
-c comment : 添加注释
useradd zhangsan    -----成功后,/etc/passwd中最后一行会添加,或者查看是否存在同名的家目录
在不添加选项的时候,执行useradd之后会执行一系列操作,创建同名的家目录和用户组

认识passwd
用户名:密码:用户id:用户组id:注释:家目录:解释器shell
用户名:创建新用户名称,后期登录使用
密码:此密码位置一般情况下都是’X’,表示密码的占位
用户id:用户的标识符
用户组id:该用户所属的主组id;
注释:解释该用户是做什么用的;
家目录:用户登录进入系统之后默认的位置
解释器shell:等待用户进入系统之后,用户输入指令之后,该解释器会收集用户输入的指令,传递给内核处理

②修改用户

usermod 选项 用户名
-g
-G
-u
-l :修改用户名

usermod -g 500 -G 501
usermod -l newname oldname

③设置密码
linux不允许没有密码的用户登录系统,因此前面创建的用户目前处于锁定状态,需要设置完密码后才可以登录计算机。

passwd 用户名

如何切换用户?

su 用户名      ---如果不指定用户名则表示切换到root用户

从root往普通用户系统不需要密码,但是反之则需要;
切换用户之后前后工作路径不会变;
普通用户没有办法访问root用户家目录,但是反之可以。
④删除用户

userdel 选项 用户名
-r : 删除用户同时,删除其家目录
不能删除当前登录的用户;解决方法:kill掉该用户相关的进程

所有和用户相关的命令(passwd外)只有root管理员有权限执行。

1.7.2、用户组管理

每一个用户都有一个用户组,系统可以对一个用户组下的所有用户集中管理。不同的linux系统对用户组的规定有所不同,如linux下用户属于与它同名的用户组,这个用户组在创建用户是同时创建。用户组的管理涉及到用户组的添加、删除、修改,实际是对/etc/group文件的更新。
/etc/group文件结构:
用户组名:密码:用户组id:组内用户名
密码:X表示占位符,虽然用户组可以设置密码,但是绝大部分的情况下不设置;
组内用户名:表示附加组是该组的用户名称

①用户组的添加

groupadd 选项 用户组名
-g :类似用户添加里的-u,表示选择自己设置一个自定义用户组id数字,如果不指定,默认从500之后开始递增;

②用户组的编辑

groupmod 选项 用户组名
-g : 选择设置一个自定义的用户组id
-n : 表示设置新的用户组名称

③用户组的删除

groupdel 用户组名

如果删除的某个组,是某个用户的主组时,则不允许删除。若确实需要删除,则移出所有的组内用户。

1.8、网络设置

网卡配置文件的位置:/etc/sysconfig/network-scipts/ 下的 ifcfg-eth0和ifcfg-lo
ifcfg-网卡名称(etho) 文件格式:
ONBOOT : 是否开机启动
BOOTPROTO : IP地址分配方式,dhcp动态主机配置协议dynamic host configuration protocol
HWADDR:硬件地址,MAC地址

网卡的重启(所有网卡):

service network restart ----某些肯定不能用
在有的分支版本可能没有service命令来快速操作服务,但是有一个共性的目录:/etc/init.d ,这个目录放着很多对服务的快捷方式。
/etc/init.d.network restart ------更常用

扩展:如果修改网卡的配置文件,但是配置文件的目录层次很深,此时可以在浅的目录中创建一个快捷方式(软连接),方便以后查找。

ln -s 原始文件的路径 快捷方式的路径
ln -s /etc/sysconfig/network-scipts/ifcfg-eth ~/ifcfg-eth0

停止单个网卡: ifdown 网卡名
开启单个网卡: ifup 网卡名
实际工作中的时候不要随意禁网卡。

1.9、SSH服务

secure shell 安全外壳协议,该协议有两个常用的作用:远程连接协议和远程文件传输协议。
协议使用端口号:默认22;可以被修改,若需要,则去修改ssh的配置文件/etc/ssh/ssh_config , 端口范围:0-65536;不能使用别的服务已经占用的端口
服务的启动/停止/重启:

service sshd start/stop/restart

/etc/init.d/sshd start/stop/restart

1.9.1、远程终端

终端工具:帮助连接远程终端,常见终端工具有:Xshell,secureCRT,Putty。以Putty为例。
首先在查询服务器的ip地址,以及在客户端ping一下,是否可以连通,以及确保服务器支持ssh,若不支持,sudo apt-get install ssh即可。打开putty,输入ip地址,端口号一般默认,不需要修改,第一次登陆会询问是否信任,选择是即可。

1.9.2、ssh服务文件传输

可视化的界面传输工具:Filezilla

1.10、Linux权限管理操作

linux权限操作与用户、用户组是兄弟操作
权限概述:linux系统一般将文件可存/取,访问的身份分为3个类别:owner、group、others,且3种身份各有read、write、execute等权限。
权限介绍:在多用户计算机系统的管理中,权限是指某个特定的用户具有特定的系统资源使用权力,像是文件夹,特定系统的指令的使用或存储量的限制。

读权限:对应文件夹来说,读权限影响用户能否列出目录结构;对于文件而言,读权限影响用户是否可以查看文件内容。
写权限:对于文件夹来说,写权限影响用户是否可以在文件夹下创建,删除,复制,移动文档;对于文件来说,写权限影响用户是否可以编辑文件内容。
执行权限:一般对文件而言:特别是脚本文件,执行权限影响用户是否可以执行

身份介绍:
owner身份(文件所有者,默认为文档的创建者)
Group身份(与文件所有者同组的用户)
other身份(其他人)
root用户(超级用户)上述对它不影响

1.10.1、查看权限

ls -l 路径    //等价于ll
//如下:文件权限是第一列的。总共10位
//第一位:文档类型,若是文件夹d,文件-,软连接l。。。
//第二位到第四位:分别表示文档所有者的读(r/-),写(w/-),执行权限(x/-);-表示不可
//第五位到第七位:分别表示与所有者同组的用户的读(r/-),写(w/-),执行权限(x/-);-表示不可
//第八位到第十位:分别表示other用户的读(r/-),写(w/-),执行权限(x/-);-表示不可

在这里插入图片描述

1.10.2、设置权限

chmod 选项 权限模式 文档
常用选项:
-r 递归设置权限(当文档为文件夹时)
权限模式:该文档需要设置的权限信息
文档:可以是文件/文件夹
如果想要给文档设置权限,操作者要么是root用户,要么是文档的所有者。
u  ---表示所有者(user)
g ---表示所有者同组用户
o ----表示others
a ----表示给所有人设置权限
如果不写,则默认给所有用户设置
权限分配方式:
+   ----表示给具体用户增加权限
-   ----表示删除用户的权限
=   -----表示将权限设置为具体的值

chmod u+x,g+rx,o+r 文档名

显示绿色是拥有可执行权限

在这里插入图片描述

//1
chmod u+x,g+x,o+wx a.x //对文件a.x添加可执行权限(如上图)
//2   数字设定法
r -- 4, w -- 2 , x --- 1;-
---  0

...
//example,希望所有者只读4,同组用户读写可执行4+2+1=7,其他可执行1
chmod 471 a.x

/*在写权限时,一般可读
一个用户对某个文件夹有读/执行权限,对该文件夹下的一个文件具有读/写/可执行权限:则可以打开,编辑该文件,但是不能删除该文档,同时也不能在该文件夹下创建,移动,复制到此,重命名操作,因为他没有文件夹的写权限。如前面关于文件夹的写权限介绍一般。
删除或移动一个文件,与文件的权限没关系,而与文件所在目录的写权限有关。
*/

属主:所属的用户(文件的主人)第三列
属组:所属的用户组;第四列
这两个信息在文档创建时候会使用创建者的信息(用户名,用户所属的主组名)
在这里插入图片描述

//chown:更改某个文件或目录的属主和属组: chown [option] 文档,chgrp [option] 文档
-r      递归
//假设现在有zhangsan,wangwu两个用户,a.c的当前所有者是zhangsan,怎么修改为wangwu呢?
//修改文件所属用户
chown wangwu a.c
//修改文件所属组
chgrp gg a.c
//*******可以一次性设置用户组和用户********
chown nobody:nogroup a.c

tree 安装命令:sudo apt-get install tree; 目录结构树形显示
软连接和硬链接:
软连接创建: ln -s file file.s 给文件file创建一个软连接file.s,软连接相当于Windows下的快捷方式。相对路径创建,如果移动,复制到其他目录下,就无法使用。故一般使用绝对路径创建。ln -s /home/xxx/xxx/file file.soft 软连接文件大小和他的路径信息相关。另外文件和文件的软连接的权限是一样。但两者权限不是一个意思(各是各的)。

硬连接:ln file file.hard ;对同一个文件或者对应的任意一个硬连接进行操作(写,改),都会改变。删除是各删除各的;操作系统给每一个文件赋予一个唯一的Inode,当有相同Inode的文件时,会操作同步,删除时,只能将硬连接计数减一,减为0时,Inode被释放。

1.11、拓展

有时候在普通用户下,想执行一些操作,如reboot,init等,时,是操作不成功的。但是有时在特殊情况下又需要执行,又不能让root用户告诉密码给普通用户,该怎么解决呢?
可以使用sudo命令来进行权限设置。可以让root事先定义哪些特殊命令是谁可以执行的。必须提前配置,默认是什么都没有的。配置文件在/etc/sudoers
直接使用vim打开是只读。
故在root用户下,使用vimsudo /etc/sudoers 是可以的
在这里插入图片描述
配置文件模板如上所示:
sudo 表示用户名
ALL 表示允许登录的主机(地址白名单)
(ALL) 表示以谁的身份执行,ALL表示root
ALL 表示当前用户可以执行的命令,多个命令可以使用,分隔。
用户查看自己的权限:sudo -l

MAC地址:网卡的物理地址,网卡设备的编号。默认情况下是全球唯一的

ping    主机地址(ip/主机名/域名)
//作用:检测当前主机与目标主机之间的连通性(不是百分百准确,有些服务器是禁ping)
netstat -tnlp         / netstat -an
//作用:表示查看网络的连接信息。(-t tcp协议;-n 将字母转化为数字;-l 列出状态是监听;-p 显示进程相关信息; -a 表示全部)
traceroute 主机地址
//作用:查找当前主机与目标主机之间的所有网关(路由器,会给沿途各个路由器发送icmp数据包,路由器可能不会响应)非内置命令 windows下 tracert
arp -a			//查看本地MAC地址缓存表
arp -d 主机地址		//删除指定的缓存记录
//地址解析协议(ARP(Address Resolution Protocol))根据IP地址获取(MAC)物理地址协议
/*
当一个主机发送数据时,首先查看本机MAC地址缓存中有没有目标主机的MAC地址,如果有就使用缓存中的结果,如果没有,ARP协议就会发出一个广播包,该广播包要求
查询目标主机ip地址对应的MAC地址,拥有该ip地址的主机就会回应,回应中包括了目标主机的MAC地址,这样发送方就得到了目标主机的MAC地址,如果目标主机不在本
地子网中,则ARP解析到的MAC地址是默认网关的MAC地址
*/
tcpdump 
//作用:抓包,抓取数据包
常用: 
tcpdump 协议 port 端口
tcpdump 协议 port 端口 host 地址
tcpdump -i 网卡设备名

二、shell

什么是shell?
Shell(外壳)是用一个C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,也是一种程序设计语言。
Shell是一种应用程序,这个程序提供了一个界面,用户通过这个界面访问操作系统的内核服务。
什么是脚本?
脚本简单的说就是一条条的文字命令,这些命令是可以看见的。常见的脚本:JavaScript(前端),VBScript,ASP, JSP,PHP(后端),python,SQL(数据库操作语言),Shell,Python
Shell属于Linux的内置脚本。程序开发的效率非常高,依赖于功能强大的命令可以迅速的完成开发任务(批处理),语法简单。
Linux中有很多类型的shell,不同的shell具备不同的功能,Linux中默认的shell是/bin/bash (重点)

2.1、shell入门

编写规范:

#!/bin/bash       //指定shell解释器
shell相关指令

文件的命名规范: 文件名.sh
使用流程:
①创建.sh文件 touch/vim
②编写shell代码
③执行shell脚本 脚本必须得有执行权限

shell脚本的执行方式:
①采用bashsh + 脚本的相对或绝对路径(不需要赋予脚本执行权限)
②采用输入脚本的绝对路径或相对路径来执行脚本(必须具有可执行权限)
③在脚本的路径前面加 . 或者source
区别:前两种都在当前shell中打开了一个子shell来执行脚本内容,当脚本执行结束后,则关闭子shell,回到了父shell。第三种,可以使脚本在当前shell里执行,而无需打开子shell。打开与不打开子shell的区别在于环境变量的继承关系,如在子shell中设置的当前变量,父shell是不可见的。

案例1:创建test.sh,输出helllo world
输出命令:echo 输出的内容是字母加数字需要’“”、纯数字可以不用

#!/bin/bash
echo 'hello world!'

在这里插入图片描述
注意:在这里运行时,要使用./test.sh,运行其他二进制的程序也是一样的。直接写成test.sh,linux会去path(环境变量)里寻找有没有叫test.sh的。

案例2:使用root用户账号创建并执行test2.sh,实现创建一个shelltest用户,并在其家目录中创建新文件try.html

#!/bin/bash
useradd -d /home/shelltest shelltest
touch /home/shelltest/try.html

Shell脚本分为简单的写法(简单命令的堆积)和复杂写法(程序的设计)

2.2、shell 进阶

2.2.1、变量

声明的变量是局部的,只能在当前的shell使用,可以使用 export 变量名将当前局部变量提升为全局变量,但是该全局变量只是其子shell可以访问,在子shell修改不会影响到当前shell下该变量的值。

#定义: 
class_name="xxx"
#使用:
echo $class_name     #一定要在变量名前加$符号

变量名的规范:
变量名后面的等号左右不能有空格,其他与c++变量规范相同
单双引号的区别:
双引号:可以识别变量,还可以实现转义
单引号:不能识别变量

案例:定义一个变量,输出当前时间,要求格式为“年-月-日 时-分-秒”

#!/bin/bash
Date=`date '+%F %T'`	
echo $Date

注意:反引号(Esc下面的那个),当在脚本中需要执行一些指令并且将执行结果赋值给变量的时候需要使用“反引号”

2.2.2、只读变量(静态变量)

#!/bin/bash
a=10
readonly a
a=20
echo $a

//输出10

2.2.3、接受用户输入

语法: read -p 提示信息 变量名 -t 指定读取值时等待的时间(秒),如果不写,则一直等待

#!/bin/bash
read -p "请输入需要创建的文件路径: " filename
touch $filename
echo "文件创建成功!"
ls -l $filename

2.2.4、删除变量

语法:unset 变量名,注意:静态变量无法使用unset撤销

#!/bin/bash
b=20
echo $b
unset b
echo $b

2.2.5、条件判断语句

if else语法

#if
if condition
then
	command1
	command2
fi
#单行写法
if condition;then command;fi
#if-else
if condition
then
	command1
	command2
else
	command
fi
#if - else if - else
if condition1
then
	command1
elif condition2
then
	command2
else
	commandN
fi

case 语句

case $变量名 in
value1)
	command
;;    #相当于break
value2)
	command
;;
*)   #default
	command
;;
esac

case $1 in
1)
	echo "one"
;;
2)
	echo "two"
*)
	echo "other"
;;
esac

for语句

for ((初始值;循环条件条件;变量变化))
do
	command
done

sum=0
for((i=0;i<=100;i++))
do
	sum=$[$sum + $i]
done
echo $sum

for 变量 in vaule value2 value3
do
	command
done

sum=0
for i in {1..100}
do
	sum=$[$sum + $i]
done
echo $sum

while循环语句

while [ 条件判断表达式 ]
do
	command
done

i=1
sum=0
while [ $i -le 100 ]
do
	sum=$[$sum + $i]    # let sum+=i
	i=$[$i+1]			# let i++
done
echo $sum



2.2.6、算数运算符

注意:表达式和运算符必须要有空格

a=20
b=10
//+
`expr $a + $b`
//-
`expr $a - $b`
//*
`expr $a \* $b`
// /
`expr $a / $b`
// %
`expr $a % $b`
//=
a=$b
//== ,[]两边必须空格
[ $a == $b ]
// !=
[ $a != $b ]

//将运算结果赋值给变量
m=$((expr $a / $b))
m=`expr $a / $b`

这样的运算太麻烦了,故提供了 $((运算表达式)) 或者 $[运算表达式]

s=$((3+4-3))
echo $s
t=$[(3+2)*5]
echo $t

2.2.7、关系运算符

可以使用 test condition 再用$?捕获返回结果来判断,麻烦了。

注意:只支持数字,不支持字符串,除非字符串的值是数字。注意[ ] 左右的空格

//判断两个数是否相等 equal,不相等 ne
[ $a -eq $b ] 
[ %a -ne $b ]
//判断左边的数是否大于右边的 great
[ $a -gt $b ]
//判断左边的数是否小于右边的 little
[ $a -lt $b ]
//判断左边的数是否大于等于右边的
[ $a -ge $b ]
//判断左边的数是否小于等于右边的
[ $a -le $b ]

2.2.8、逻辑运算符

注意[ ] 左右的空格

//非运算
[ ! false ]
// 或 也可以使用 ||
[ $a -lt 20 -o $b -gt 100 ]
[ $a -lt 20 ] || [ $b -gt 100 ]

// 与 也可以使用 &&
[ $a -lt 20 -a $b -gt 100 ]
[ $a -lt 20 ] && [ $b -gt 100 ]

2.2.9、字符串运算符

注意[ ] 左右的空格

//字符串相等判断
[ $a = $b ]
[ $a != $b ]
//字符串长度为0判断
[ -z $a ]    
// 字符串长度不为0判断
[ -n $a ]
//字符串不为空判断
[ $a ]

2.2.10、文件测试运算符

文件测试运算符用于检测Unix/Linux文件的各种属性
//检测文件是否是块设备文件
[ -b file ]
//检测文件是否是字符设备文件
[ -c file ]
//检测文件是否存在并且为目录
[ -d file ]
// 检测文件是否存在并且为普通文件
[ -f file ]
// 检测文件是否设置了SGID位
[ -g file ]
// 检测文件是否设置了粘着位(Sticky Bit)
[ -k file]
//检测文件是否是有名管道
[ -p file ]
//检测文件是否设置了SUID位
[ -u file ]
//检测文件是否可读
[ -r file ]
//检测文件是否可写
[ -w file ]
//检测文件是否可执行
[ -x file ]
//检测文件是否为空
[ -s file ]
// 检测文件(目录)是否存在
[ -e file ]

2.3、shell脚本附带选项

问题描述:在linux shell中如何处理tail -n 10 access,log这样的命令行选项?
步骤:
1、调用tail指令
2、系统把后续选项传递给tail
3、tail先去打开指定文件
4、取出最后10行
问题:自己写的shell是否可以传递选项呢?
可以,传递方式如上述一样,关键是shell怎么接收?
传递:./test a b c
接收:在脚本中可使用"$1"来表示a,"$2"来表示b,依次类推。$1 $2 就是变量
$0 代表脚本名称,$1代表第一个参数,$2代表第2个参数,依次类推。10以上使用 ${10}
$# 可以获取输入参数的个数,常用于循环,判断参数的个数是否正确以及加强脚本的健壮性。
$* 获取命令行所有的参数,将所有参数看成一个整体。
$@ 获取命令行所有参数,但是将每个参数区别对待。
$? 最后一次执行命令的返回状态,如果该值为0,则证明上一条命令正确执行,如果非0(由命令自己来确定),则上一条命令不正确。

#!/bin/bash
echo $1 $2 $3

在这里插入图片描述
练习:创建自定义指令“user”,可以直接执行,要求可以执行以下指令
#user -add 用户名 //----添加用户
#user -delete 用户名 //----删除用户及其家目录

//创建test7.sh
#!/bin/bash
if [ $1 = '-add' ]
then
	useradd $2
else
	userdel -r $2
fi

题目要求是指令,所以可以去~/.bashrc文件中添加别名

2.4、函数

2.4.1、系统函数

#前面的命令都看做是一个系统函数
#在介绍两个
#basename string suffix  suffix可写可不写,写了就去掉后缀,该函数常用于获取文件名。通过字符串的删减得到文件名
basename /home/www/test.txt .txt #test
basename /home/www/test.txt  #test.txt

#dirname string  该函数是为了得到路径名,但其实也是对字符串的操作,并不会判断路径的有效性
dirname /home/www/test.txt   #/home/www

2.4.2、自定义函数

#[]是可选内容,基本语法如下:
[function] funname[()]
{
	action;
	[return int;]
}

注意:
1.必须在调用函数前,先声明函数,shell脚本是逐行运行的
2.函数的返回值,只能通过%?来获取,可显式加 return num,若不见,就以最后一条命令的运行结果返回。return后的数值必须在0-255

function add(){
 sum=$[$1 + $2]
 echo $sum
}
read -p "请输入第一个整数: " a
read -p "请输入第二个整数: " b
sum=$(add $a $b)
echo $sum

2.4.3、实例:归档文件

实现一个每天对指定目录进行归档备份的脚本,输入一个目录名称(末尾不带/),将目录下所有文件按天归档保存,并将归档日期附加在归档文件名上,放在/root/archive下。
使用归档命令 tar,-c选项表示归档,-z表示同时进行压缩,得到文件后缀名为.tar.gz

#!/bin/bash
#判断参数个数是否为1
if [ $# -ne 1 ]
then 
	echo "输入参数个数不正确,请输入归档目录!"
	exit
fi
#获取归档路径名称
if [ -d $1 ]
then
	echo
else
	echo
	echo "目录不存在!"
	echo
	exit
fi
DIR_NAME=$(basename $1)
DIR_PATH=$(cd $(dirname $1);pwd)
#获取日期
DATE=$(date +%y%m%d)
#定义生成的归档文件名
FILE=archive_${DIR_NAME}_$DATE.tar.gz
DEST=/root/archive/$FILE
#开始归档文件
echo "开始归档"
echo

tar -czf $DEST $DIR_PATH/$DIR_NAME
if [ $? -eq 0 ]
then
	echo
	echo "归档成功"
	echo "归档文件名: $DEST"
else
	echo "归档失败"
fi
exit

2.5、正则表达式

正则表达式使用单个字符串来描述、匹配一系列符合某个语法规则。在很多文本编辑器里,正则表达式通常被用来检索、替换哪些符合某个模式的文本。在Linux中,grep,sed,awk等文本处理工具都支持通过正则表达式进行模式匹配。

# 特殊字符 ^ 匹配一行的开头
ls | grep ^a  
# 特殊符号 $ 匹配一行的结尾
ls | grep x$
#  ^$ 匹配空
#  特殊字符 . 匹配一个任意字符
ls | grep b..x
#  特殊符号 * 不能单独使用,与前一个字符连起来用,表示匹配前一个字符0次或多次
ls | grep b*x
# .*  匹配任意
#字符区间 []
[2,4]   #匹配2或4
[0-9]   #匹配0-9任意数字
[0-9]*   #匹配任意长度数字字符串
[a-z]   #匹配任意小写字母
[a-z]*	#匹配任意长度小写字母字符串
[a-c,f-g] #匹配a-c或f-g的任意字符
# 特殊符号 {} 可以来指定次数 ,,属于扩展的正则表达式
echo "13423234543" | grep -E ^1[3,4,5,7,8][0-9]{9}$ #指定匹配9次数字

2.6、文本处理工具

2.6.1、cut

可以在文件中负责剪切数据。cut命令从文件的每一行剪切字节、字符字段并将这些输出。
基本用法
cut [选项参数] filename
说明:默认分隔符是制表符
选项参数

选项参数功能
-f列号,提取第几列
-d分隔符,按照指定分隔符分割列,默认是制表符
-c按字符进行切割 后加n表示取第几列
//新建文件 cut.txt 内容如下
/*
shan he
xi nan
ni  wo
hao bang
*/
cut -d " " -f 1 cut.txt //截取第一列
cut -d " " -f 1,2 cut.txt  //截取1,2列
cut -d " " -f 1-2 cut.txt //截取1,2列
cut -d " " -f 2- cut.txt  //截取第二列和后面的列

//得到ip
ifconfig ens33 | grep netmask | cut -d " " -f 10

2.6.2、awk

一个强大的文本分析工具,把文件逐行读入,以空格为默认分隔符将每行切片,切开的部分在进行分析处理

基本用法
awk [选项参数] ‘/正则表达式1/{action1}’ ’ /正则表达式2/{action2}'filename
表示awk在数据中进行匹配,然后执行相应的命令
选项参数说明

选项参数功能
-F指定输入文件分隔符
-v赋值一个用户定义变量
//搜索passwd文件以root关键字开头的所有行,并输出该行的第一列和第七列,中间以,分割
cat /etc/passwd | awk -F ":" '/^root/ {print $1","$7}'

//只显示/etc/passwd的第一列和第七列,以逗号分割,且在所有行前添加列名user,shell  。在最后一行添加 dahaige,/bin/zuishuai
cat /etc/passwd | awk -F ":" 'BEGIN{print "user,shell"} {print $1","$7} END{print "dahaige,/bin/zuishuai"} '
/*BEGIN{},END{}相当于函数一般在最前面和最后面执行*/

//将passwd文件中的用户id加1输出
cat /etc/passwd | awk -v i=1 -F : '{print $3+i}'

内置变量

变量说明
FILENAME文件名
NR已读的记录数(行号)
NF浏览记录的域的个数(切割后列的个数)
//统计passwd文件名,每行行号和每行列数
cat /etc/passwd | awk -F : '{print "filename:" FILENAME",ROW:" NR",COL"NF}'

//查询ifconfig命令输出结果的空行所在行数
ifconfig | awk '/^$/{print NR}'
//切割IP
ifconfig ens33 | awk '/netmask/{print %2}'   //前面的空格自动省略的。

2.6.3、综合案例:发送消息

实现一个向某个用户快速发送消息的脚本,输入用户名作为第一个参数,后面直接跟要发送的消息。脚本需要检测用户是否登录在系统中,是否打开消息功能,以及当前发送小时是否为空。

#!/bin/bash

# 查看用户是否登录
login_user=$(who | grep -i -m 1 $1 | awk '{print $1}') 
#who查询当前登录用户有哪些,-i是忽略大小写,-m 1 是取第一行
if [ -z $login_user ]
then
	echo "$1 不在线"
	echo "脚本退出"
	exit
fi
#查看用户是否开启消息功能
is_allowed=$(who | grep -i -m 1 $1 | awk '{print $2}') 

if [ is_allowed != "+" ]
then
	echo "$1 没有开启消息功能"
	echo "脚本退出"
	exit
fi

if [ -z $2 ]
then
	echo "没有消息发送"
	echo "脚本退出"
	exit
fi

whole_msg=$(echo $* | cut -d " " -f 2-)

#获取用户登录终端
user_terminal=$(who | grep -i -m 1 $1 | awk '{print $2}') 

# 发送消息
echo $whole_msg | write $login_user $user_terminal
if [ $? != 0 ]
then
	echo " 发送失败"
else
	echo " 发送成功 "
fi
exit

三、系统编程

gcc编译步骤

4步骤: 预处理、编译、汇编、连接
-I 指定头文件所在目录位置
-c 只做预处理、编译、汇编 得到二进制文件!!!
-g 编译时添加调试语句。主要支持gdb调试
-Wall 显示所有警告信息
-D 向程序中‘动态’注册宏定义。#define ....
-o 给生成文件取名

3.1、相关知识

3.1.1、动态库和静态库

对于静态库,每一个调用他的文件会各自使用各自的,会编译进各自的可执行文件中。
对于动态库,会加载进内存中,同时段肯定只有这一个,所有使用他的文件指向它。也叫共享库
区别:调用静态库的函数和调用自身函数是一样的。而调用动态库就慢一些,需要去内存中取。
静态库:对空间要求较低,而时间要求较高的核心程序中。
动态库:对时间要求较低,而空间要求较高的核心程序中。

静态库制作及使用步骤
1.将.c生成.o文件

gcc -c add.c -o add.o

2.使用ar制作静态库

ar rcs lib库名.a add.o xxx.o    //称呼为库名.a 不加lib

小插曲:使用gcc编译时,如果编译阶段错误,会报具体行号,如果连接错误,会看到collect2:error:ld.....
3.使用静态库

gcc 程序名.c 静态库(lib库名.a) -o test  //前面的程序用到静态库中的函数。
gcc main.c -I 路径/lib库名.a -o test
gcc main.c -I 路径 -L 库民 -o test

动态库制作及使用步骤
1.将.c生成.o文件,(生成与位置无关的代码 -fPIC)

gcc -c add.c -o add.o -fPIC

2.使用 gcc -shared 制作动态库

gcc -shared -o lib库名.so add.o sub.o div.o

3.编译可执行程序,指定所使用的动态库,-L 指定库路径 , -l 指定库名

gcc test.c -o a.out -l 库名 -L 路径

貌似还需要添加动态库路径

3.1.2、gdb 调试

基础指令:
	-g:使用该参数编译可以执行文件,得到调试表;
	gdb a.out   ;
	list: list 1 列出源码,根据源码指定 行号设置断点;
	b: b 20  在20行位置设置断点;
	run/r:运行程序;
	next/n:下一条指令(会越过函数);
	step/s:下一条指令(会进入函数);
	print/p p i 查看变量i的值;
	continue:继续执行断点后续指令;
	quit:退出gdb当前调试;
其他指令:
	run:使用run查找段错误出现位置;
	finish:结束当前函数调用,返回到函数调用点;
	start:单步执行,运行程序,停在第一行执行语句;
	set args:设置main函数命令行参数;(在执行运行执行),也可以使用run +参数;
	info b: 查看断点表;
	b 20 if i = 5:设置条件断点;
	ptype: 查看变量类型;
	bt:列出当前程序存活的栈帧(和函数调用息息相关,调用函数,产生对应栈帧,存放相应的变量,形参等,调用会,释放)
	frame 1:根据栈帧编号,切换栈帧;
	display i : 设置跟踪变量
	undisplay 1:取消跟踪变量(编号)

3.1.3、makefile

一个规则

目标:依赖条件
	(一个tab缩进)命令
hello:hello.c
	gcc hello.c -o hello
*************************************************
①若想生成目标,则检查规则中的依赖条件是否存在,若不存在,则寻找是否有规则来生成该依赖文件;
hello:hello.o
	gcc hello.o -o hello
hello.o:hello.c
	gcc hello.c -o hello.o
*************************************************
②检查规则中的目标是否更新,必须先检查它的所有依赖,依赖中有一个被更新,则目标必须被更新;
a.out:hello.c add.c sub.c div1.c
	gcc hello.c add.c sub.c div1.c -o a.out
这样问题是,修改其中一个,整个都需要重新编译。
*************************************************
a.out:hello.o add.o sub.o div1.o
	gcc hello.o add.o sub.o div1.o -o a.out
hello.o:hello.c
	gcc -c hello.c -o hello.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
div1.o:div1.c
	gcc -c div1.c -o div1.o

makefile文件的默认目标是第一行的目标,所以上述文件的第一个目标不能移动到后面,若移动了,就只会生成改了后的第一个目标。

可以在最开始加上  ALL:  a.out    指定makefile的最终目标.就可以随便移动。

二个函数

src = $(wildcard *.c)           //通配符
找到当前目录下所有后缀为.c的文件,赋值给src。

obj = $(patsubst %.c,%.o,$(src))      //将参数3中包含参数1的部分替换为参数2.
把src变量里面所有后缀为.c的文件替换成.o

src = $(wildcard *.c)
obj = $(patsubst %.c,%.o,$(src))
ALL:a.out
a.out : $(obj)
	gcc $(obj) -o a.out
hello.o:hello.c
	gcc -c hello.c -o hello.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
div1.o:div1.c
	gcc -c div1.c -o div1.o
clean:										//没有依赖
	-rm -rf $(obj)           //前面-是强制执行,即使目标没有,也不会报错   ,顺序执行到结束  
//可以使用make clean ,执行clean目标

三个自动变量

$@  : 表示规则命令中的目标
$<  : 表示规则命令中的第一个依赖条件
$^  :表示规则命令中的所有条件,组成一个列表,以空格分开,如果这个列表有重复项会去重。如应用于模式规则中,可以将依赖条件列表中的依赖依次取出,套用模式规则。
src = $(wildcard *.c)
obj = $(patsubst %.c,%.o,$(src))
ALL:a.out
a.out : $(obj)
	gcc $^ -o $@
hello.o:hello.c
	gcc -c $< -o $@
add.o:add.c
	gcc -c $< -o $@
sub.o:sub.c
	gcc -c $< -o $@
div1.o:div1.c
	gcc -c $< -o $@

模式规则

%.o:%.c
	gcc -c $< -o $@
************************
src = $(wildcard *.c)
obj = $(patsubst %.c,%.o,$(src))
ALL:a.out
a.out : $(obj)
	gcc $^ -o $@
%.o:%.c
	gcc -c $< -o $@

静态模式规则

$(obj):%.o:%.c   //指定模式规则给谁用,因为可能多个
	gcc -c $< -o %@

伪目标

.PHONY clean ALL  /不管条件满足与否,都要执行后面两个对象

src = $(wildcard *.c)
obj = $(patsubst %.c,%.o,$(src))
myargs = -Wall -g
ALL:a.out
a.out : $(obj)
	gcc $^ -o $@ $(myargs)
%.o:%.c
	gcc -c $< -o $@ $(myargs)
clean:
	-rm -rf $(obj) a.out
.PHONY clean ALL

3.2、系统调用

3.2.1、常用函数

1. open()函数--打开/创建一个文件
int open(const char* pathname,int flags);
int open(const char* pathname,int flags,mode_t mode);

	返回值,成功就返回文件描述符,失败返回-1,设置errno。
	pathname:表示要打开或创建的文件,
	flags:表示打开方式,O_RDONLY,O_WRONLY,O_RDWR,O_APPEND,O_CREAT,O_TRUNC等,
	mode:表示文件的权限(读写可执行,用421的组合来表示,如777,都是读写可执行),但是文件的权限也受umask(775)影响(与操作)。
2. close()函数---关闭一个文件描述符
int close(int fd)
3. strerror()函数  ---与errno(理解为一个全局变量,好多函数出错会修改它,是一个整数)相关,将errno翻译成相应错误的字符串描述。
char* strerror(int errnum);
4. read()函数
ssize_t read(int fd,void* buf,size_t count)   

	返回值:成功就返回读到的字节数,失败返回-1,设置errno,若返回值是-1,且errno=EAGIN或EWOULDBLOCK说明不是read失败,
而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。

	fd:是文件描述符,
	buf:是存数据的缓存区,
	count:是缓存区大小。
5. write()函数
ssize_t write(int fd,const void* buf,size_t count);
	返回值:成功就返回写入的字节数,失败返回-1,设置errno,
	fd:是文件描述符,
	buf:是待写出数据的缓存区,
	count:是数据大小。
6.fcntl()函数 -----改变一个【已经打开】的文件的访问属性
int fcntl(int fd,int cmd,...);
	掌握:F_GETFL、F_SETFL
	
	int flags = fcntl(fd,F_GETFL);获得fd的当前属性信息
	flags |= O_NONBLOCK;//需要注意可以改的就后几个,读写改不了
	fcntl(fd,F_SETFL,flags);//设置fd的非阻塞
7.lseek函数----文件偏移
	Linux中可以使用系统函数lseek函数来修改文件偏移量(读写位置)
	每个打开文件都记录着当前读写位置,刚刚打开的文件读写位置为0,表示文件开头,通常读写多少个字节就会偏移多少个字节。
但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置偏移到新的文件末尾。
lseek函数和标准I\O库的fseek函数类似,可以移动当前读写位置。

	int fseek(FIFE* stream,long offset,int whence);
	返回值:成功返回0,失败-1
		特别的是超出文件末尾位置返回0;往回超出头位置,返回-1;

	off_t lseek(int fd,off_t offset,int whence);
	返回值:失败返回-1,成功返回值是较文件起始位置向后偏移量。
    特别的是lseek允许超过文件结尾设置偏移量,文件会因此被拓展

		offset:偏移量
		whence:起始位置,SEEK_SET(起始位置)、SEEK_CUR(当前位置)、SEEK_END(结尾位置)
注意:文件的读和写使用同一偏移位置
应用场景:
	1.文件的读和写使用同一偏移位置
	2.使用lseek获取、拓展文件大小.   
			int ret = lseek(fd,0,SEEK_END);
	3.拓展文件大小:要想使文件大小真的拓展,必须引起IO。 
			lseek(fd,111,SEEK_END);//增加111字节 
			write(fd,"\0",1);//会有文件空洞,可以添加任意字符。
		-----使用truncate()函数直接拓展文件 
			int ret = truncate("name.txt",520);
// lseek()函数使用
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>

int main(void){
    int fd,n;
    char msg[] = "It's a test for lseek\n";
    char ch;
    fd = open("lseek.txt",O_RDWR|O_CREAT,0644);
    if(fd<0){
        perror("open  error");
        exit(1);
    }
    write(fd,msg,strlen(msg));//使用fd对已打开文件进行写操作,等价读写位置位于文件结尾处
    lseek(fd,0,SEEK_SET);//修改文件读写指针位置,位于文件开头。
    while((n=read(fd,&ch,1))){
        if(n<0){
            perror("read error");
            exit(1);
        }
        write(STDOUT_FILENO,&ch,n);//将文件内容按字节读出,写到屏幕
    }
    close(fd);
}

预读入缓输出机制
在这里插入图片描述
文件描述符
PCB进程控制块:本质 结构体
成员:文件描述符表。
文件描述符:0.1.2…1023 每次自动获取表中可用的最小的,前三个是标准输入STDIN_FILENO,标准输出STDOUT_FILENO,标准错误STDERR_FILENO。

阻塞和非阻塞:是设备、网络文件的属性
产生阻塞的场景:读设备文件(/dev/ttv—终端文件),读网络文件。(读常规文件不会阻塞)

3.2.2、文件存储

了解inode、dentry、数据存储、文件系统

inode :本质是一个结构体,存储文件的属性信息,权限、类型、大小、时间、用户。。。。也叫文件属性管理结构,大多数的inode存储在磁盘上。少量常用、近期使用的inode会缓存在内存中。
dentry:目录项,本质也是结构体,重要成员变量有两个(文件名,inode,。。。),而文件内容保存在磁盘上。
数据存储:
文件系统:

3.2.3、文件操作函数

	1. stat()函数---->获取文件属性(从inode结构体中获取)(可以穿透符号链接)
		int stat(const char *path,struct stat *buf);
			返回值:成功返回0,失败返回01,设置errno。
			参数:
				path:文件路径
				buf:inode结构体指针(传出参数)里面包含了文件信息。
				
	2. lstat()函数---->获取文件属性(不能穿透符号连接)
		int lstat(const char *path,struct stat *buf);
		
	3. link()函数----->创建硬连接
		int link(const char *oldpath,const char* newpath);
		
	4. unlink()函数----->删除一个文件的目录项
		int unlink(const char* path);

	Linux下删除文件的机制:不断将硬链接-1,直到为0,才将会被操作系统释放(具体时间看系统内部调度算法)
	unlink()函数特征:清除文件时,如硬链接数等于0了,没有dentry对应,但是文件任然不会马上释放。
要等到所有打开该文件的进程关闭该文件,系统才会挑时间将文件释放掉。
	某种意义上,我们删除文件,只是让文件具备了被释放的条件。
-----所以我们可以先open一个文件,在unlink删除,之后在write,read也是可以的,这样在进程结束后,文件就不存在,只在进程期间存在。

隐式回收:当进程结束运行时,所有该进程打开的文件都会被关闭,申请的内存空间会被释放。

readlink()函数--读取符号链接文件本身(不是指向文件),得到链接所指向的文件名
ssize_t readlink(const char* path,char *buf,size_t bufsize);
	成功返回实际读到的字节数,读到的内容在buf,内容大小在bufsize
readlink命令: readlink 符号链接文件
rename()函数--重命名一个文件
int rename(const char *oldpath,const char* newpath);
	成功返回0,失败返回-1,设置errno

3.2.4、目录操作函数

1. getcwd()函数----获取进程当前工作目录
char* getcwd(char*buf,size_t size);
	成功,buf保存当前进程工作目录位置,失败返回NULL
	
2. chdir()函数----改变当前进程的工作目录
int chdir(const char* path);
	成功返回0,失败-1,设置errno

注意:目录文件也是文件,其文件内容是该目录下所有子文件的目录项dentry。

namerwd
文件文件内容可查看(cat,more…)内容可修改(vim)可运行
目录目录可以被浏览(ls,tree)创建,删除,修改(mv,touch,mkdir)可以被打开,进入
3. opendir()函数---打开目录
DIR* opendir(const char *name);
	成功返回指向目录结构体指针,失败返回NULL
4. closedir()函数---关闭目录
int closedir(DIR* dirp);
	成功返回0,失败-1设置errno
5. readdir()函数---读取目录,需要循环遍历取值
struct dirent* readdir(DIR* dirp);
	成功返回目录项结构体指针,失败返回NULL设置errno
例子:
	DIR* dp;
	dp = opendir(argv[1]);
	struct dirent *sdp;
	while((sdp=readdir(dp))!=NULL){...}
struct dirent {
 	ino_t d_ino;       /* Inode number */
	off_t d_off;       /* Not an offset; see below */
	unsigned short d_reclen;    /* Length of this record */
	unsigned char  d_type;      /* Type of file; not supported by all filesystem types */
	char d_name[256]; /* Null-terminated filename */
 };

3.2.5、递归目录

//递归目录
1.判断命令行参数,获取用户查询的目录名
	没有参数就是当前目录
2.判断用户指定的是否是目录,若不是目录,直接打印出来--封装为函数(后面还要多次用)
3.读目录--->封装为函数
	dp = opendir(dir);
	while((dname=readdir())){
		普通文件,直接打印;
		目录:拼接出绝对路径 sprintf(path,"%s/%s",dir,d_name);
			 递归调用自己
	}


#include<stdio.h>
#include<stdalign.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<dirent.h>
#include <sys/types.h>
#include<sys/stat.h>
void isFile(const char *name);
void read_dir(const char *dir){
    char path[256];
    DIR* dp;
    struct dirent* sdp;
    dp  = opendir(dir);
    if(dp == NULL){
        perror("opendir error");
        return;
    }
    while((sdp = (struct dirent*)readdir(dp))){
        if(strcmp(sdp->d_name,".") ==0||strcmp(sdp->d_name,"..")==0) continue;

        sprintf(path,"%s/%s",dir,sdp->d_name);
        isFile(path);
    }
    closedir(dp);
    return;

}
void isFile(const char *name){
    int ret = 0;
    struct stat sb;
    ret = lstat(name,&sb);
    if(ret == -1){
        perror("stat error");
        return;
    }
    if(S_ISDIR(sb.st_mode)){
        read_dir(name);
    }else{
        printf("%10s\t\t%ld\n",name,sb.st_size);
    }

}
int main(int argc, char const *argv[])
{
    if(argc==1){
        isFile(".");
    }else{
        isFile(argv[1]);
    }
    return 0;
}

3.2.6、重定向

dup和dup2函数
int dup(int oldfd);  
int dup2(int oldfd,int newfd);
成功返回新的文件描述符,失败返回-1,设置errno
例子:
	dup2(fd,STDOUT_FIFRNO);//将屏幕输出重定向给fd所指向文件
	//就可以将像printf这类原来向屏幕写的,写如到fd中。

3.3、进程

3.3.1、进程相关知识

程序:是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu,内存,打开的文件,锁。。。)

进程:是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。
在内存中执行。(程序运行起来,产生一个进程)

并发:在操作系统中,一个时间段内有多个进程都处于已启动运行到运行完毕之间的状态。但是任意时刻上只有一个进程在运行。

PCB进程控制块:进程id,文件描述符表、进程状态(初始态,就绪态,运行态,挂起态,终止态)、进程工作目录所在位置、umask掩码、信号相关信息资源、用户id,用户组id

父子进程之间在fork后,哪些是相同的,哪些是不同的?
刚刚fork完:
相同:全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
不相同:进程ID、fork()函数返回值、父进程ID、进程运行时间、闹钟(定时器)、未决信号集

似乎子进程复制了父进程0-3g用户空间内容,以及父进程的PCB,但PID不同。但是真的是这样吗?
当然不是!父子进程遵循读时共享、写时复制的原则。这样设计,无论是子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

父子进程共享全局变量吗?遵循读时共享,写时复制

父子进程共享的是:1.文件描述符(打开的文件的结构体),2.mmap建立的映射区

孤儿进程:父进程先于子进程结束,则子进程就会成为孤儿进程,子进程的父进程就会成为init进程,称为init进程领养孤儿进程
僵尸进程:子进程终止,父进程尚未回收,子进程残留资源存放在内核中,变成僵尸进程。
特别注意:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。怎么清除僵尸进程?kill父进程,父进程死后,子进程就变成孤儿进程了。

fork()函数:创建进程
pid_t fork(void);
	返回值:创建成功,在父进程中该函数返回子进程id(>0),子进程返回0.

getpid()函数:得到当前进程的id
int getpid(void);

getppid()函数:得到当前进程的父进程id
int getppid(void);
//使用fork()函数生成5个子进程,并依次打印是自己是第几个子进程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
int main(int argc, char*argv[]){
	int i;
	pid_t pid;
	for(i = 1;i<=5;i++){
		if(fork()==0){
			sleep(i-1);
			printf("I'm %dth child\n",i);
			break;
		}
	}
	if(i==6){
		sleep(5);
		printf("I'm parent\n");
	}
	return 0;
}

3.3.2、gdb调试

使用gdb调试时,只能跟踪一个进程。可以在fork函数调用前,通过指令设置gdb调试工具跟踪符父进程或者是子进程。默认父进程

set follow-fork-mode child
set follow-fork-mode parent
#一定要在fork()函数之前设置才有效

3.3.3、exec函数族

fork创建子进程后执行的是和父进程相同的程序(但也可能执行不同的代码分支),子进程往往需要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新的进程,所以调用exec前后的该进程id并不会改变。
将当前进程的.text、.data替换为所加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程id不变,换核不换壳

//
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[]);
	l(list) 命令行参数列表
	p(path) 搜索file时使用path变量
	v(vector) 使用命令行参数数组
	e(environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
	实际上,只有ececve函数是真正的系统调用,其他几个都是调用它。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
int main(int argc,char* argv[]){
	pid_t pid = fork();
	if(pid==-1){
		perror("fork error");
		exit(1);
	}else if(pid ==0){
		//execlp("ls","-l","-d","-h",NULL);//错误写法,第一个参数是可执行文件名
		//1
		execlp("ls","ls","-l","-d","-h",NULL);
		//2
		//execl("./loop_fork","loop_fork",NULL);
		
		perror("exec error");
		exit(1);
	}else{
		sleep(1);
		printf("I'm parent: %d\n",getpid());
	}

}

exec函数族的一般规律:
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回-1.所以通常我们直接在exec函数调用后直接调用perror()和exit()函数,无需if判断。

3.3.4、wait、waitpid函数

一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的pcb还保留着,内核在其中还保留了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的型号是哪个。这个进程的父进程可以调用wait函数或者waitpid函数获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在shell中用特殊变量$?查看,因为shell是它的父进程,当它终止时shell调用wait函数或者waitpid函数得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程的终止信息。有三个功能:
1.阻塞等待子进程退出。
2.回收子进程残留资源。
3.获取子进程结束状态。

//一个wait函数或者waitpid函数调用,只能回收一个子进程

pid_t wait(int *status);
//成功:返回子进程id,清理掉子进程id;失败:-1(没有子进程)
//可以使用wait函数的status参数来保存进程的退出状态。借助宏函数进一步判断进程终止原因。

pid_t waitpid(pid_t pid,int* status,int options);
//返回值:正常:返回>0,正常回收的子进程id;=0,参数3指定了WNOHANG,并且没有子进程结束。=-1,失败,errno
//特殊参数
//参数pid: -1:回收任意子进程(相当于wait函数); 0 :回收和当前调用waitpid一个组的所有子进程; <-1(-进程组id) : 回收指定进程组内任意子进程; >0 :回收指定id的子进程。
//options:WNOHANG 指定回收方式为非阻塞
对status的处理:
	WIFEXITED(status) //返回值非0--进程正常退出
	WEXITSTATUS(status) //上述为真,使用该函数--获取进程退出状态(exit的参数)
	
	WIFSIGNALED(status) //返回值非0--进程异常终止(信号终止)
	WTERMSIG(status)//上述为真,使用该函数--取得使该进程终止的那个信号的编号
	
	WIFSTOPPED(STATUS)//返回指非0---进程处于暂停状态
	WSTOPSIG(status)//上述为真,使用该函数--取得使进程赞同的那个信号编号
	WIFCONTINUED(status) //为真--进程暂停后已经继续执行


#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main(void){
	pid_t pid,wpid;
	int status;
	pid = fork();
	if(pid==0){
		printf("child id - %d,going to d=sleep 10s\n",gepid());
		sleep(10);
		printf("child die\n");
		return 55;
	}else if(pid > 0){
		//wpid = wait(NULL);//不关心子进程结束原因
		wpid = wait(&status);//如果子进程没有终止,父进程阻塞在这个函数上
		if(wpid == -1){
			perror("wait error");
			exit(1);
		}
		if(WIFEXITED(status)){
			printf("child exit with %d\n",WEXITSUATUS(status));
		}
		if(WIFSIGNALED(status)){
			printf("child exit with %d\n",WTERNSIG(status));
		}
		printf("parent wait finish:%d\n",wpid);
	}else{
		perror("fork");
		return 1;
	}
	return 0;
}

//阻塞方式回收所有子进程
while((wpid = waitpid(-1,NULL,0)));
//非阻塞方式回收所有子进程
while((wpid=waitpid(-1,NULL,WNOHANG)) != -1){
	if(wpid >0)	printf("wait child %d\n",wpid);
	else if(wpid==0){
		sleep(1);
		continue;
	}
}

3.4、进程通信

LPC(InterProcess Communication)
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓存区。在进程间完成数据传递需要借助操作系统提供的特殊方法:文件,信号,共享内存,消息队列,套接字,命名管道等。现在常用的方法有:管道(使用简单)、信号(开销小)、共享映射区(无血缘关系)、本地套接字(最稳定)
管道

3.4.1、匿名管道

管道是一种最基础的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:

  1. 本质是一个伪文件(实为内核缓存区)
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出
    管道的原理:管道实为内核使用环型队列机制,借助内核缓存区(4k)实现。

管道的局限性:

  1. 数据不能进程自己写自己读
  2. 管道中的数据不可反复读取。一旦读走,管道中不在存在。
  3. 采用双向半双工通信方式,数据只能单方向流动。 不是单工哦,同一时间只能一个方向通信。
//pipe函数:创建并打开管道。
int pipe(int pipefd[2]);//成功返回0.失败返回-1;
//参数:pipefd[0]:读端;pipefd[1]:写端

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
int main(int argc,char* argv[]){
	int ret;
	int fd[2];
	ret = pipe(fd);
	if(ret==-1){
		perror("pipe error")
		exit(1);
	}
	pid = fork();
	if(pid >0){
		close(fd[0]);
		char * str="hell0o pipe\n";
		write(fd[1],str,strlen(str));
		close(fd[1]);
		sleep(1);
	}else if(pid == 0){
		close(fd[1]);
		char buf[1024];
		int ret = read(fd[0],buf,sizeof(buf));
		write(STDOUT_FILENO,buf,ret);
		close(fd[0]);
	}
	return 0;
}

使用管道注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

  1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用为0),而任然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没有关闭(管道写端引用大于0),而持有管道写端的进程也没有向管道写数据,这是有进程从管道读端读数据,那么管道总剩余的数据都被读取后,再次read会阻塞,直到管道找那个有数据可读才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这是有进程向管道写入数据,那么该进程会收到信号SIGPIPE,通常会导进程异常终止。当然也可以对该信号进行捕捉,不终止该进程。
  4. 如果有指向管道读端的文件描述符没有关闭(管道读端引用大于0),而持有读端的进程也没有从管道中读数据,这时有进程向管道写数据,那么管道被写满时再次写入会阻塞,直到管道有空位置了才写入数据并返回。

总结

  1. 读管道:管道有数据,read返回实际读到的字节数;管道无数据且写端全部关闭,read返回0;管道无数据且写端没有全部关闭,read会阻塞等待。
  2. 写管道:管道读端全部被关闭,进程异常终止(也可以捕捉SIGPIPE信号,使进程终止);管道读端没有全部关闭且管道没有满,可以写入数据,并返回实际写入字节数;管道读端没有完全关闭且管道已满,写入会阻塞。
  3. 实际上管道允许一个写端,多个读端或一个读端,多个写端(但是可能会写乱);
//练习:使用管道实现父子进程间通信,完成:ls|wc-l。假定父进程实现ls,子进程实现wc
//ls命令正常会从结果集写出到stdout,但是现在会写入管道的写端,wc -l正常应该从stdin读取数据,但是此时从管道的读端读。
//pipe、exec、dup2
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/wait.h>
int main(int argc, char const *argv[])
{
    int fd[2];
    int rt = pipe(fd);
    if(rt == -1){
        perror("pipe error");;
        exit(1);
    }
    if(rt=(fork()) == 0){
        close(fd[1]);
        dup2(fd[0],STDIN_FILENO);
        execlp("wc","wc","-l",NULL);
        fprintf(stderr, "error execute wc\n");
        exit(1);
    }
    if(rt == -1){
        perror("fork error");
    }
    close(fd[0]);
    dup2(fd[1],STDOUT_FILENO);
    execlp("ls","ls","-l",NULL);
    fprintf(stderr, "error execute ls\n");
    wait(NULL);
    return 0;
}

3.4.2、命名管道

FIFO,不相关的进程之间也能交换数据。是Linux基础文件类型中的一种,但是FIFO文件在磁盘上没有数据块,仅仅用来标识内核中的一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程通信。

创建方式:

1.命令:mkfifo管道名;
2.库函数:int mkfifo(const char* pathname, mode_t mode);成功:0;失败:-1。
创建好就是一个文件,可以使用ls查看到。

创建好fifo文件后剩下的其实就是和前面操作文件一模一样。

一旦使用mkfifo创建一个FIFO,就可以使用open打开它,常见的文件I/O函数都可以用于fifo。如:close、read、write、unlink等。
打开时都不使用NONBLOCK的话,读端(写端)都会阻塞等待到对方存在时。
打开时使用NONOBLOCK的话,若只有一端,会返回-1,这种方式下,建议都以读写打开fifo文件。

//命令行:mkfifo my_test_fifo  //使用ls查看就行
//实现写文件
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(int argc,char* argv[]){
	int fd,i;
	char buf[4096];
	if(argc<2){
		printf("Enter like this: ./a.out fifoname\n");
		return -1;
	}
	fd = open(argv[1],O_WRONLY);
	if(fd<0){
		sys_err("open");
	}
	i=0;
	while(1){
		sprintf(buf,"hello itcast%d\n",i++);
		write(fd,buf,strlen(buf));
		sleep(1);
	}
	close(fd);
	return 0;
}

//实现读
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(int argc,char* argv[]){
	int fd,len;
	char buf[4096];
	if(argc<2){
		printf("Enter like this: ./a.out fifoname\n");
		return -1;
	}
	fd = open(argv[1],O_RDONLY);
	if(fd<0){
		sys_err("open");
	}
	while(1){
		len=read(fd,buf,sizeof(buf));
		write(STDOUT_FILENO,buf,len);
		sleep(1);
	}
	close(fd);
	return 0;


//也可以一个写,多个读,只不过也是一旦读走就没有了。

3.4.3、存储映射I/O

Memory-mapped I/O 使一个磁盘文件与存储空间中的一个缓存区相映射。于是当从缓存区中取数据,就相当于读文件中的相应字节。于是类似,将数据存入缓存区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用该方法,首先通知内核,将一个指定文件映射到内存区域中。这个映射工作可以通过mmap函数来实现。

3.4.3.1、mmap函数
//mmap函数----建立映射区
void* mmap(void* adrr,size_t length.int prot,int flags,int fd,off_t offset);
//返回:成功:返回创建的映射区首地址,失败:MAP_FAILED宏
//参数:
//addr --建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL,系统自动分配
//length --想要创建映射区的大小(小于等于文件实际大小),
/*prot--映射区权限PROT_READ,PROT_WRITE,PROT_READ|PROT_WRITE,这里说的是映射区的权限,
当flags为shared时,才需要该读写权限<=open时的权限。
		因为open时是对文件的读写权限,当为shared时,想映射区读写就也会向文件读写,所以就也要有文件的相应权限。*、
/*flags--标志共享内存的共享属性(常用于设置更新物理区域,设置共享,创建匿名映射区)
         MAP_SHARED--会将映射区所做的操作反映到物理设备(磁盘)上,
         MAP_PRIVATE--映射区所做的修改不会反应到硬盘上。*/
// fd:用于创建共享内存映射区的那个文件的文件描述符
//offset:偏移位置(需是4k的整数倍),默认0,表示映射文件全部

//munmap函数----释放映射区
int munmap(void* addr,size_t length)   
//addr:mmap的返回值
//length:大小

注意事项:

  1. 用于创建映射区的文件大小为0,mmap指定的大小不为0,则会出总线错误
  2. 用于创建映射区的文件大小为0,mmap指定的大小也为0,则会出无效参数
  3. 用于创建映射区的文件属性为只读,mmap创建的映射区为写,读,则会出无效参数
  4. 创建映射区需要read权限,当访问权限是PROT_SHARED时,mmap的读写权限,应该<=文件open时的权限。只写是不可以的。但是只读,则写入的时候会报错。
  5. 文件描述符fd,在mmap创建完映射区完成即可关闭。后续访问文件,使用的是地址.
  6. offset必须是4096的整数倍(因为MMU)
  7. 对申请的映射区内存,不能越界访问。
  8. munmap用于释放的地址必须是mmap申请的地址。
  9. 映射区访问权限是私有时,对内存的所有修改,只在内存有效,不会写到物理磁盘上。
  10. 映射区访问权限是私有时,只需要open文件时有读权限,用于创建映射区即可。

mmap函数的保险调用方式:

fd = open("文件名",O_RDWR)
mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

总结

  1. 创建映射区,隐含一次对映射文件的读操作;
  2. 映射区的释放与文件关闭无关。只要映射创建成功,文件就可以立即关闭。
  3. mmap创建映射区出错率非常高,一定要检查返回值。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>

int main(int argc,char* argv[]){
	char *p = NULL;
	int fd;
	fd = open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd == -1){
		perror("open error");
		exit(1);
	}
	//如果文件大小为0,则需要拓展文件大小,不然大小为0的文件创建的映射区大小也是0了。
	//1
	// lseek(fd,10,SEEK_END);
	// write(fd,"\0",1);
	//或者2
	//ftruncate(fd,20);//需要写权限才可改变文件大小
	//获取文件大小
	int len = lseek(fd,0,SEEK_END);
	p = static_cast<char*>(mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0));
	if(p == MAP_FAILED){
		perror("mmap error");
		exit(1);
	}
	//使用p对文件进行读写操作
	strcpy(p,"hello mmap");//需要写权限,这里的权限来自mmap创建指定
	printf("-----%s\n",p);//需要读权限,这里的权限来自mmap创建指定
	int ret = munmap(p,len);
	if(ret == -1){
		perror("mmap error");
		exit(1);
	}
	close(fd);
	return 0;
}
3.4.3.2、有血缘关系的mmap通信

父子进程等有血缘关系的进程之间也可以通过mmap建立映射区进行数据通信。但相应的要创建映射区的时候指定对应的标志位参数flags:
MAP_SHARED:(共享映射) 父子进程共享映射区。
MAP_PRIVATE:(私有映射) 父子进程各自独占映射区。

父进程先创建映射区。open(O_RDWR) MMAP(MAP_SHARED)
指定MAP_SHARED权限
fork()创建子进程。
一个进程读,一个进程写。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>
int var = 100;
int main(void){
	int *p;
	pid_t pid;
	int fd = open("temp",O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd<0){
		perror("open error");
		exit(1);
	}
	unlink("temp");//
	ftruncate(fd,4);
	p = static_cast<int*>(mmap(NULL,4,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0));
	if(p == MAP_FAILED){
		perror("mmap error");
		exit(1);
	}
	close(fd);
	pid = fork();
	if(pid == 0){
		*p = 2000;//写共享内存
		var = 1000;//修改全局变量
		printf("child,*p = %d,var = %d\n",*p,var);//读共享内存
	}
	else{
		sleep(1);
		printf("parent,*p = %d,var = %d\n",*p,var);//读共享内存
	}
	int ret = munmap(p,len);
	if(ret == -1){
		perror("mmap error");
		exit(1);
	}
	return 0;
}
//执行输出:
//child,*p = 2000,var = 1000
//parent,*p = 2000,var = 100
//若将mmap创建的权限改为MAP_PRIVATE:
//child,*p = 2000,var = 1000
//parent,*p = 0,var = 100
//各自写,各自读
3.4.3.3、无血缘关系的mmap通信
//写端
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>
struct student{
	int id;
	char name[256];
	int age;
};
int main(int argc,char* argv[]){
	struct student stu={10,"xiaoxiao",22};
	struct student*p;
	int fd;
	fd = open("test_map",O_RDWR|O_CREAT|O_TRUNC,0664);
	if(fd == -1){
		perror("open error");
		exit(1);
	}
	ftruncate(fd,sizeof(stu));
	p=(student*)mmap(NULL,sizeof(stu),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(p == MAP_FAILED){
		perror("mmap error");
		exit(1);
	}
	close(fd);
	while(1){
		memcpy(p,&stu,sizeof(stu));
		stu.id++;
		sleep(1);
	}
	munmap(p,sizeof(stu));
	return 0;
}
//读端
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>
struct student{
	int id;
	char name[256];
	int age;
};
int main(int argc,char* argv[]){
	struct student stu={10,"xiaoxiao",22};
	struct student*p;
	int fd;
	fd = open("test_map",O_RDWR);
	if(fd == -1){
		perror("open error");
		exit(1);
	}

	p=(student*)mmap(NULL,sizeof(stu),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(p == MAP_FAILED){
		perror("mmap error");
		exit(1);
	}
	close(fd);
	while(1){
		printf("id = %d,name = %s,age=%d\n",p->id,p->name,p->age);
        sleep(1);
	}
	munmap(p,sizeof(stu));
	return 0;
}
3.4.3.4、mmap匿名映射区
//mmap匿名映射区----只能父子进程之间通信
//头文件不变
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<errno.h>
#include<sys/wait.h>
#include<pthread.h>
int var = 100;
int main(void){
	int *p;
	pid_t pid;
    int len = 40;
	p = (int*)mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
	if(p == MAP_FAILED){
		perror("mmap error");
		exit(1);
	}
	pid = fork();
	if(pid == 0){
		*p = 2000;//写共享内存
		var = 1000;//修改全局变量
		printf("child,*p = %d,var = %d\n",*p,var);//读共享内存
	}
	else{
		sleep(1);
		printf("parent,*p = %d,var = %d\n",*p,var);//读共享内存
		wait(NULL);
	}
	int ret = munmap(p,len);
	if(ret == -1){
		perror("mmap error");
		exit(1);
	}
	return 0;
}

注意
无血缘关系进程通信实现区别:
mmap:数据可以反复读取。
fifo:数据只能一次读取。

3.4.4、信号

3.4.4.1、基本概念

信号机制:A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停去处理信号,处理完成后在继续执行。类似与硬件中断。但信号是软件层面上实现的,早期也叫软中断。
信号共性:简单、不携带大量信息、满足条件才发送。
所有信号的产生和处理全部都是由内核完成的。
与信号相关的事件和状态:
产生信号:(其实是驱使内核产生信号)

  1. 按键产生;Ctrl+c、Ctrl+z
  2. 系统调用产生;kill、raise
  3. 软件条件产生;定时器alarm
  4. 硬件异常产生;非法访问内存,除0
  5. 命令产生;kill命令

名词理解

递达:递送并且到达进程。
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。

信号的处理方式

  1. 执行默认操作;
  2. 忽略(丢弃);
  3. 捕捉(调用用户处理函数)

Linux内核的进程控制块PCB是一个结构体,task struct,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含信号相关的信息,主要指的是阻塞信号集和未决信号集。
阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽信号x后,再次受到该信号,该信号的处理将推后(解除屏蔽后在处理)。
未决信号集:1.信号产生,未决信号集描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转为0.这一时刻非常短暂。2.信号产生后由于某种原因(阻塞)不能抵达(到进程的意思),这类信号的集合称为未决信号集。在屏蔽解除前,信号一种处于未决状态。

信号类型

1-31是常规信号--都有默认事件和处理动作,
34-64是实时信号

1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

信号4要素

编号、名称、事件、默认处理动作
通过man 7 signal查看帮助文档。

默认处理动作
Term:终止进程;
Ign:忽略进程;
Core:终止进程,生成Core文件
Stop:暂停进程
Cont:继续运行进程

常见信号

SignalValueActionComment
SIGHUP1Term当用户退出shell时,由该shell启动的所有进程收到这个信号,默认动作为终止进程
SIGINT2Term当用户按下ctrl+c时,用户终端向正在运行的由该终端启动的程序发出此信号。默认动作为终止进程。
SIGQUIT3Core当用户按下ctrl+\时产生该信号,用户终端向正在运行的由该终端启动的程序发送此信号。默认动作为终止进程。
SIGILL4CoreCPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
SIGTRAP5Core该信号由中断点指令或其他trap指令产生。默认动作为终止进程并产生core文件。
SIGABRT6Core调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
SIGBUS7Core非法访问内存地址,包括内存对齐出错,默认动作为终止进程产生core文件
SIGFPE8Core在发生致命的运算错误是发出,包括浮点运算错误,溢出,除0等。默认动作为终止进程产生core文件。
SIGKILL9Term无条件杀死进程。本信号不能被忽略,处理,阻塞。默认动作为终止进程,向管理员提供可以杀死任何进程的方法。
SIGUSE110Core用户定义信号。默认动作终止进程
SIGSEGV11Core指示进程进行了无效内存访问。默认终止进程产生core文件。
SIGUSR212Term用户自定义信号,默认终止进程
SIGPIPE13TermBroken pipe向一个没有读端的管道写数据,默认动作终止进程
SIGALRM14Term定时器超时,超时时间由系统调用alarm设置。默认终止进程。
SIGTERM15Term程序结束信号,与SIGKILL不同的是,该信号可以被阻塞或终止,通常表示程序正常退出
SIGSTKFLT16TermLinux早期版本信号,保留向后兼容。默认动作为终止进程
SIGCHLD17Ign子进程状态发生变化时,父进程收到该信号,默认为忽略该信号
SIGCONT18Cont如果进程已经停止,则继续进行执行,默认动作为继续/忽略
SIGSTOP19Stop停止进程的执行,该信号不能忽略、处理阻塞。默认为暂停进程
SIGTSTP20Stop停止终端交互进程的运行。ctrl+z发出该信号。默认暂停进程
SIGTTIN21Stop后台进程读终端控制台。默认暂停进程
SIGTTOU22Stop该信号类似SIGTTIN,在后台进程要向终端输出数据时发生。默认暂停进程。
SIGURG23Ign套接字上有紧急数据,先当前正在运行的进程发出该信号,报告紧急数据到达。默认忽略该信号。
SIGXCPU24Term进程执行时间超过cpu分配时间,系统产生该信号发送给进程。默认终止进程。
SIGXFSZ25Term超过文件最大长度设置。默认终止进程。
SIGVTALRM26Term虚拟时钟超时产生该信号,只计算该进程占用cpu的使用时间。默认动作终止进程
SGIPROF27Term类似于26,不仅包括该进程占用cpu的时间还包括执行系统调用的时间。默认终止进程。
SIGWINCH28Ign窗口大小发生变化时发出。默认忽略该信息

注意:只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!

3.4.4.2、Kill函数与Kill命令
kill命令产生信号: Kill -SIGKILL pid
Kill函数
int kill(pid_t pid,int sig);
	返回值:成功返回0;失败返回-1(ID非法,信号非法,普通用户杀死init进程等),设置errno
	
	sig:推荐写信号宏名,不推荐数字,不同操作系统的信号编号可能是不同的,但是名称是一样的。
	
	pid: >0:发送信号给指定进程。 =0:发生信号给调用kill函数进程属于同一进程组的所有进程;
<-1:取Pid的绝对值发送给对应进程组(-号指代组,-组id);=-1:发送给进程有权限发送的所有进程。

进程组:每个进程都属于一个进程组,进程组是一个或多个进程的集合,相互关联,共同完成一个实体任务,
每个进程组都有一个进程组长,默认进程组id与进程组长id相同。

权限保护:超级用户可以发送信号给任意用户,普通用户不能向系统用户发生信号。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效id == 接收者实际或有效用户id

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<signal.h>
int main(int argc,char* argv[]){
    pid_t pid = fork();
    if(pid>0){
        printf("parent,pid = %d\n",getpid());
        while(1);
    }else if(pid == 0){
        printf("child pid = %d , ppid = %d\n",getpid(),getppid());
        sleep(2);
        kill(getppid(),SIGKILL);//杀死父进程
    }
    return 0;

}
3.4.4.3、定时器相关函数

每个进程都有且只有唯一一个定时器

alarm函数

设置定时器。在指定seconds到后,内核会给当前进程发送编号为14 SIGALRM信号。进程收到该信号,默认动作终止进程。采用自然计时法
unsigned int alarm(unsigned int seconds);
	返回值:返回0或者剩余秒数,无失败
注意:只有一个闹钟,故反复调用alarm操作的是同一个。
取消定时器alarm(0),返回旧时钟剩余秒数。

定时:与进程状态无关!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都在计时。

setitimer函数

#include <sys/time.h>
设置定时器。可以替代alarm函数,角度微秒,可以实现周期定时。
int setitimer(int which,const struct itimerval* new_value,struct itimerval* old_value);
成功:0;失败:-1,设置errno
参数:

	which:指定定时方式:
		1.自然定时:ITIMER_REAL ----> 144 SIGLARM  计算自然时间
		2.虚拟空间计时(用户空间):ITIMER_VIRTUAL ---->26 SIGVTALRM   只计算占用cpu的时间
		3.运行时计时(用户+内核):ITIMER_PROF ----> 27 SIGPROF   计算占用cpu和执行系统调用的时间
struct itimerval{
	struct timeval it_interval;//用来设置两次定时任务之间的间隔时间
	struct timeval it_value; //定时时长
}//两个参数都设置为0,即清0操作
/*settimer工作机制是,先对it_value倒计时,当it_value为零时触发信号。然后重置为it_interval。继续对it_value倒计时。一直这样循环下去。先延时it_value,触发信号,此后间隔it_interval触发信号*/
struct timeval{
	time_t tv_sec;//秒
	suseconds_t tv_usec;//微秒
}
new_value:定时秒数
old_value:传出参数,上次定时剩余时间。

3.4.4.4、信号集操作函数

内核通过读取未决信号集来判断信号是否被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。以达到屏蔽指定信号的目的。

int sigemptyset(sigset_t *set);----将某个信号集清0,成功0,失败-1

int sigfillset(sigset_t *set);-----将某个信号集置1,成功0,失败-1

int sigaddset(sigset_t *set,int signum);----将某个信号加入信号集,成功0,失败-1

int sigdelset(sigset_t *set,int signum);----将某个信号清出信号集,成功0,失败-1

int sigismember(const sigset_t *set,int signum);----判断某个信号是否在信号集中,在返回1,不在返回0,出错返回-1

sigprocmask()函数用于屏蔽信号、解除屏蔽信号也使用该函数。本质,读取或修改进程的信号屏蔽字(PCB中)


严格注意,屏蔽信号:只能将信号处理延后执行(延至解除屏蔽);而忽略表示将信号不处理。
int sigprocmask(int how,const sigset_t* set,sigset_t* oldset);
返回值:成功0,失败-1,设置errno
参数:
	set:传入参数,是一个位图
	oldset:传出参数,保存旧的信号屏蔽集合
	how:参数取值:假设当前的信号屏蔽字为mask。
		1.SIG_BLOCK:当how设置此值,相当于mask = mask|set
		2.SIG_UNBLOCK:相当于mask = mask &~set
		3.SIG_SETMASK:相当于mask = set 若调用该函数解除了当前若干个信号的阻塞,则在调用该函数前,至少将其中一个信号递达。
	对同一个set来说,1是设置阻塞,2用来取消阻塞。3一般不用

sigpending函数

读取当前进程的未决信号集
int sigpending(sigset_t *set);//参数是传出参数
返回值:
	成功0,失败-1,设置errno
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<signal.h>

void sys_err(const char *str){
    perror(str);
    exit(1);
}
void printf_set(sigset_t set){
    int i;
    for(i = 1;i<32;i++){
       if(sigismember(&set,i)) putchar('1');
       else putchar('0') ;
    }
    printf("\n");
    sleep(1);

}
int main(int argc,char* argv[]){
    sigset_t set,oldset,pedset;
    sigemptyset(&set);

    sigaddset(&set,SIGINT);
    sigaddset(&set,SIGQUIT);

    int ret = sigprocmask(SIG_BLOCK,&set,&oldset);
    if(ret == -1){
        sys_err("sigprocmask error");
    }
    while(1){
        ret = sigpending(&pedset);
        if(ret == -1){
            sys_err("sigpending error");
        }
        printf_set(pedset);
    }
    
    return 0;
}

//终端运行起来使用 kill -number 进程id 观察是否有效
3.4.4.5、信号捕捉

signal函数

注册一个信号捕捉函数:
typedef void(*sighandler_t)(int)
sighandler_t signal(int signum,sighandler_t handler);
参数:
	signum:待捕捉信号
	handler:处理函数,返回值只能为空,参数只能为int
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<signal.h>
void sys_err(const char *str){
    perror(str);
    exit(1);
}
void sig_catch(int signum){
    printf("catch you!! %d\n",signum);
}
int main(int argc,char* argv[]){
    signal(SIGINT,sig_catch);
    while(1);
    return 0;
}

sigaction函数

修改信号处理动作(通常在Linux用其来注册一个信号捕捉函数)
int sigaction(int signum,const struct sigaction*act,struct sigaction* oldact);
返回值:
	成功返回-;失败返回-1,设置errno;
参数:
	act:传入参数,新的处理方式;
	oldact:传出参数,旧处理方式;
struct sigaction {
    void (*sa_handler)(int);//捕捉函数
    sigset_t sa_mask;/*用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,
 它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。*/
    int sa_flags;/*设置默认属性
    SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
	SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
	SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。
但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号*/
};

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<signal.h>
void sys_err(const char *str){
    perror(str);
    exit(1);
}
void sig_catch(int signum){
    printf("catch you!! %d\n",signum);
   // sleep(5);
}
int main(int argc,char* argv[]){
    struct sigaction act,oldact;
    act.sa_handler = sig_catch;//设置捕捉函数
    sigemptyset(&act.sa_mask);/*设置屏蔽字(捕捉函数期间生效)即在捕捉函数执行时,
    其他信号是否响应。通过对该参数进行修改就可以控制。*/
    //sigaddset(&act.sa_mask,SIGQUIT) 设置了这句,捕捉函数执行期间(加sleep函数),按ctrl+\是不会响应的
    act.sa_flags = 0;

    int ret = sigaction(SIGINT,&act,&oldact);
    if(ret == -1){
        sys_err("sigaction error");
    }
   // ret = sigaction(SIGQUIT,&act,&oldact);//设置多个捕捉函数
    //if(ret == -1){
    //    sys_err("sigaction error");
    //}
    while(1);
    return 0;
}

信号捕捉特性:
1.进程正常运行时,默认PCB中有一个信号屏蔽字,假定为mask,他决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号后,要调用该函数。而该函数有可能执行时间很长,在这期间所屏蔽的信号不能由mask指定。而是用sa_mask来指定。调用完信号处理函数,在恢复为mask。即设置act.sa_amsk
2.xx信号不在函数执行期间,xx信号被自动屏蔽。 设置act.sa_flags=0;
3.阻塞的常规信号不支持排队,产生多次只记录一次(后32个实时信号支持排队)

对3的解释,在上述代码的捕捉函数内加上sleep(5);在执行期间,第一次进入捕捉函数后,在多次按ctrl+c是不响应的,5s到了后,只会响应一次。

在这里插入图片描述

3.4.4.6、SIGCHLD信号

产生条件

  1. 子进程终止时;
  2. 子进程接收到SIGSTOP信号暂停时;
  3. 子进程处于暂停态,接受到SIGCONT后唤醒时;
//借助SIGCHLD信号回收子进程
子进程结束运行,其父进程会受到SIGCHLD信号,该信号默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成对子进程状态的回收。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<signal.h>
#include<sys/wait.h>
void sys_err(const char *str){
    perror(str);
    exit(1);
}
void sig_catch(int signum){

    pid_t wpid; 
    //1,有问题,会出现某些子进程不能被正常回收,因为在捕捉函数执行期间,对同一信号屏蔽不处理
    // wpid =  wait(NULL);
    // printf("catch you!! pid = %d\n",wpid);
	//2
	while((wpid = wait(NULL)) != -1){//也可以使用waitpid函数,循环回收,防止僵尸进程出现,或者说一次出现多个子进程思死亡,但是只会有一个信号进捕捉函数,但是wait可以回收已经死亡的函数,故这里可以这样使用。--一次捕捉回收多个死亡子进程
		printf("catch you!! pid = %d\n",wpid);
	}
	
}
int main(int argc,char* argv[]){
    pid_t pid;
    int i ;
    //添加阻塞----因为在父进程注册信号捕捉前,就有子进程发送该信号,而不能被捕捉到
     sigset_t set,oldset,pedset;
	 sigemptyset(&set);
     sigaddset(&set,SIGCHLD);
     int ret = sigprocmask(SIG_BLOCK,&set,&oldset);
     if(ret == -1){
         sys_err("sigprocmask error");
     }

    for(i=1;i<5;i++){
        if((pid=fork())==0)   break;
    }
    if(5==i){
        sleep(5);//在父进程注册之前,子进程就死了,发送SIGCHLD信号被默认忽略。就不能正常回收了,所以需要在前面设置屏蔽。若没有,观察输出,就一个也来不及回收。
        printf("I'm parent, pid=%d\n",getpid());
        struct sigaction act,oldact;
        act.sa_handler = sig_catch;//设置捕捉函数
        sigemptyset(&act.sa_mask);//设置屏蔽字(捕捉函数期间生效)
        act.sa_flags = 0;//设置默认属性
        int ret = sigaction(SIGCHLD,&act,&oldact);
        if(ret == -1){
            sys_err("sigaction error");
        }
        //解除阻塞--如果不解除阻塞,捕捉函数就不可能在执行。
		 ret = sigprocmask(SIG_UNBLOCK,&set,&oldset);
	     if(ret == -1){
	         sys_err("sigprocmask error");
	     }

        while(1);//模拟父进程的后续逻辑
    }else{
        printf("child pid =%d\n",getpid());
    }
    return 0;
}

3.5、守护进程

3.5.1、进程组和会话

进程组也被称为作业。代表一个或多个进程的集合。每个进程都属于一个进程组。
当父进程创建子进程时候,默认父子进程属于同一个进程组,且进程组id等于第一个进程id。
可以使用kill -SIGKILL -进程组id(负的,前面是负号,杀死进程,就直接是id),可以将整个进程组内进程全部杀死。
组长进程(进程组id相等的那个进程)可以创建一个进程组,创建该进程组的进程终止,只有进程组还剩余其他进程,进程组就还存在,与组长进程是否终止无关。
进程组生存期:进程组创建的最后一个进程离开(终止或转到其他进程组)
一个进程可以为自己或子进程设置进程组id。
创建会话:
注意事项
1.调用进程不能是进程组组长,该进程变成新会话首进程
2.该进程成为一个新进程组的组长
3.需要root权限(ubuntu不需要)
4.新会话丢弃原有的控制终端,该会话没有控制终端
5.该调用进程是组长进程,则返回出错
6.建立新会话时,先调用fork,父进程终止,子进程调用setsid()

getsid()函数---获取进程所属会话id
pid_t getsid(pid_t pid);
成功返回id,失败返回-1设置errno
pid为0,表示查看当前进程的会话id
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程

setsid()函数---创建会话,并以自己的id设置进程组id,同时也是新会话的id
pid_t setsid(void);
成功返回调用进程id,失败返回-1设置errno
调用setsid函数的进程,即使新的会长,也是新组长。
---进程id,进程组id,会话id都是一样的了。
3.5.2、守护进程

daemon(精灵)进程。是Linux中的后台服务进程,通常独立于控制终端(一般不和用户直接交互)并且周期性执行某种任务或等待处理某些事件,一般采用以d结尾的名字。不受用户登录注销影响。
Linux后台的一些系统服务进程,没有控制终端,不能直接交互,都是守护进程。
创建守护进程最关键一步是调用setsid函数创建一个新会话,并成为会长。
创建守护进程步骤

1.创建子进程,父进程退出----所有工作在子进程中,形式上脱离了控制终端。
2.在子进程中创建新会话----使子进程完全独立出来,脱离控制。
3.通常根据需要,改变当前目录---chdir()函数----防止占用可卸载的文件系统
int chdir(const char *path);成功是0,失败-1,设置errno
4.通常根据需要,重设文件权限掩码---umask()函数----防止继承的文件创建屏蔽字拒绝某些权限
5.通常根据需要,关闭/重定向文件描述符----继承的打开文件不会用到,浪费系统资源,无法卸载
//6.开始执行守护进程核心工作守护进程退出处理程序模型
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<pthread.h>
#include<sys/stat.h>
void sys_err(const char* str){
        perror(str);
        exit(1);
}
int main(int argc,char *argv[]){
    pid_t pid;
    pid = fork();
    if(pid>0)
        exit(0);//关闭父进程
    pid = setsid();//创建新会话
    if(pid == -1){
        sys_err("setsid error");
    }
    int ret = chdir("/home/wjr");//改变工作目录
    if(ret == -1){
        sys_err("chdir error");
    }
    umask(0022);//改变文件访问权限掩码
    close(STDIN_FILENO);//关闭文件描述符0
    int fd = open("/dev/null",O_RDWR);//fd---->0
    if(fd == -1)
        sys_err("open error");
    dup2(fd,STDOUT_FILENO);//重定向stdout,stderr
    dup2(fd,STDERR_FILENO);
    while (1);//模拟守护进程业务
    
}

3.6、线程

3.6.1、线程基本知识

编译时 -lpthread
什么是线程:轻量级进程,本质是进程(在Linux环境下);
进程:有独立进程地址空间和独立的PCB
线程:有独立的PCB,没有独立的地址空间
Linux下:线程是最小的执行单位;进程是最小的分配资源单位,可以看成只有一个线程的进程

ps -Lf 进程id  可以得到线程号(不是线程id)

线程共享资源:文件描述符、信号处理方式(对信号处理而言,随便哪个线程收到处理都可以,一般别把线程和进程混用)、当前工作目录、用户和用户组id、内存地址空间。-----全局变量
线程间全局变量共享

线程不共享资源:线程id、处理器现场(寄存器)和栈指针、独立的栈空间、errno变量、信号屏蔽字、调度优先级

优点:提高并发性、开销小、数据通信、共享数据方便
缺点:库函数,不稳定、调试,编写困难,不支持gdb调试、对信号支持不好
总之,优点相对突出,缺点不是硬伤。Linux下由于实现方法导致进程和线程差别不是很大。

3.6.2、线程控制函数

pthread_self函数 --- 获取线程id
pthread_t pthread_self(void);
成功返回本线程id,失败无
线程id:本质:在LINUX下为无符号整数(%lu),其他系统可能结构体
线程id是进程内部识别标志。(两个进程的线程id可能相同)
注意:不应该使用全局变量pthread_t tid 在子线程中通过pthread_create传出参数来获取线程id,而应该使用pthread_self.
pthread_create函数----创建一个新线程
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void*),void* arg);
成功返回0.失败返回errno
thread:传出参数,表示新创建子线程id
attr:线程属性,传NULL,使用默认属性
start_routine:子线程回调函数。创建成功时,该pthread_create函数返回时,会自动调用回调函数。
arg:回调函数的参数,没有传NULL
线程属性:
1.线程属性初始化:
	初始化线程属性:int pthread_attr_init(pthread_attr_t* attr);//成功返回0,失败返回错误号
	销毁线程属性:int pthread_attr_destroy(pthread_attr_t* attr);//同上
2.线程的分离状态---决定一个线程以什么样的方式终止自己
	非分离态,只有当pthread_join函数返回才算终止。
	分离态:自己运行结束,线程就结束了。
	设置分离状态:int pthread_attr_setdetachstate(pthread_attr_t* attr,int detachstate);
	获取分离状态: int pthread_attr_getdetachstate(pthread_attr_t* attr,int* detachstate);
	detachstate:PTHREAD_CREATE_DETACHED --->分离线程、 PTHREAD_CREATE_JOINABLE --->非分离线程
//创建线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
void *tfn(void *arg){
    printf("thread:pid = %d, tid = %lu\n",getpid(),pthread_self());
    return NULL;
    
}
int main(int argc, char const *argv[])
{
    pthread_t tid;
    printf("main:pid = %d, tid = %lu\n",getpid(),pthread_self());
    //**************
    pthread_attr_t attr;
    int ret = pthread_attr_init(&attr);
    if(ret != 0){
        fprintf(stderr,"attr_init error:%s\n",strerror(ret));
    }
    ret = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//设置线程属性为分离
    if(ret != 0){
        fprintf(stderr,"attr_setdetachstate error:%s\n",strerror(ret));
    }
    //************
    //ret = pthread_create(&tid,NULL,tfn,NULL);//使用默认的线程属性,需要join回收。
    ret = pthread_create(&tid,&attr,tfn,NULL);
    if(ret != 0){
        fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
    }
    
    //************
    ret = pthread_attr_destroy(&attr);
    if(ret != 0){
        fprintf(stderr,"attr_destroy error:%s\n",strerror(ret));
    }
    //***************
	ret = pthread_join(tid,NULL);//阻塞等待回收,因为前面设置了分离态,其实它肯定会返回错误号
    if(ret != 0){
        fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
    }

    //sleep(5);//为了给子线程时间处理,因为如果没有这个时间,主线程就执行return,返回到调用者,结束了进程。
    //return 0;//可以替换为 pthread_exit((void*)0);//这样退出的就是主线程,而不是进程了,上面sleep也可以不用要了,进程退出,所有线程就都结束了。
    pthread_exit((void*)0);
}
//创建多个线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
void *tfn(void *arg){
    int i = (long)arg;
    sleep(i);
    printf(" I'm %dth thread:pid = %d, tid = %lu\n",i+1,getpid(),pthread_self());
    return NULL;
    
}
int main(int argc, char const *argv[])
{
    pthread_t tid;
    printf("main:pid = %d, tid = %lu\n",getpid(),pthread_self());
    int ret ;
    int i;
    for( i = 0;i<5;i++){
        ret = pthread_create(&tid,NULL,tfn,(void*)i);//采用值传递,借助强转
        if(ret != 0){
            fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
        }
        ret = pthread_detach(tid);//设置线程分离,线程终止会自动清理,无需回收
    }
    sleep(i);
    return 0;
    //pthread_exit((void*)0);
}
pthread_exit函数----将当前线程退出
void pthread_exit(void* retval);
retval表示线程退出状态,通常传NULL,
exit()是退出进程,即会将所有的线程退出,
return 表示返回到调用者
pthread_join函数----阻塞等待线程退出,获取线程退出状态--类似waitpid函数
int pthread_join(pthread_t thread,void** retval);
成功返回0,失败:错误号
参数:
	thread:线程ID;---不是指针哦
	retval:存储线程结束状态--传出参数
对比记忆:
	进程中:main返回值、exit参数->int;等待子进程结束wait函数参数-->int*
	线程中:线程主函数返回值、pthread_exit->void*;等待线程结束pthread_join函数参数-->void**
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>

struct thrd{
    int var;
    char str[256];
};
void* tfn(void *arg){
    struct thrd *tval;
    tval = (thrd*)malloc(sizeof(thrd));
    strcpy(tval->str,"hello thread");
    tval->var = 1000;
    printf("thread:pid = %d, tid = %lu\n",getpid(),pthread_self());
    return  (void*)tval;
}
int main(int argc, char const *argv[])
{
    pthread_t tid;
    struct thrd *retval;
    printf("main:pid = %d, tid = %lu\n",getpid(),pthread_self());
    int ret = pthread_create(&tid,NULL,tfn,NULL);
   	if(ret != 0){
        fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
    }
    ret = pthread_join(tid,(void**)&retval);
    if(ret != 0){
        fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
    }
    printf("str=%s,var=%d\n",retval->str,retval->var);
    pthread_exit(NULL);
}
pthread_detach函数----实现线程分离
int pthread_detach(pthread_t thread);
成功返回0,失败返回错误号。
thread:待分离线程id;
线程分离状态:指定该状态,线程主动与主控线程断开连续。线程结束后,其退出状态不由其他进程获取,而是直接自己自动释放。
网络、多线程服务器常用。
也可以使用pthread_create()函数的参数2来设置线程分离。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
void *tfn(void *arg){
    printf("thread:pid = %d, tid = %lu\n",getpid(),pthread_self());
    return NULL;
    
}
int main(int argc, char const *argv[])
{
    pthread_t tid;
    printf("main:pid = %d, tid = %lu\n",getpid(),pthread_self());
    int ret = pthread_create(&tid,NULL,tfn,NULL);
    if(ret != 0){
        fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
        exit(0);
    }
    ret = pthread_detach(tid);//设置线程分离,线程终止会自动清理,无需回收
    if(ret != 0){
        fprintf(stderr,"pthread_detach error:%s\n",strerror(ret));
        exit(0);
    }
    sleep(1);
    ret = pthread_join(tid,NULL);
    if(ret != 0){
        fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
        exit(0);
    }
    //pthread_exit((void*)0);
    return 0;
}

//输出
main:pid = 18201, tid = 139683286677312
thread:pid = 18201, tid = 139683278337792
pthread_join error:Invalid argument

pthread_cancel函数  -----杀死(取消)线程---对应kill函数
注意:线程的取消不是实时的,而是有一定延时。需要等待线程到达某个取消点(进入内核就有取消点)。
int pthread_cancel(pthread_t thread);
成功返回0,失败返回错误号
thread:待杀死线程id
我们可以在程序中,手动添加一个取消点。pthread_testcancel();
成功被pthread_cancel()杀死的线程,返回-1,使用pthread_join()回收
主线程中:在cancel()杀死后使用join()回收。
	void* tret=NULL;
	pthread_cancel(tid);
	pthread_join(tid,&tret);
待杀死子线程中:
	pthread_testcancel();
	

线程使用注意事项

  1. 主线程退出其他线程不退出,主线程调用pthread_exit函数
  2. 避免僵尸线程。pthread_join、pthread_detach、pthread_create设置分离属性。被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
  3. malloc和mmap申请的内存可以被其他线程释放。
  4. 应避免在多线程模型中调用fork除非马上调用exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
  5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。

3.7、线程同步

3.7.1、线程同步基本知识

所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。“同”对应协同,协助。主旨在于协同步调,按预定的先后次序运行。
线程同步:指的是一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。-----对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
多个控制流,共同操作一个共享资源的情况,都需要同步。
数据混乱原因:
1.资源共享
2.调度随机
3.线程间缺乏必要的同步机制
以上三点中,前两点不能改变,想要提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就容易出现混乱。所以只能从第三点着手解决。使得多个线程访问共享资源的时候,出现互斥。

锁的使用:建议锁!!对公共数据进行保护,所有线程应该在访问公共数据前先拿锁在访问。但是,锁本身不具备强制性。

3.7.2、互斥量

Linux中提供了一把互斥锁mutex。
每个线程对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的。但通过“锁”将资源的访问变成互斥操作,而与时间相关的错误就不会在产生了。

int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
以上5个函数的返回值都是:成功返回0,失败返回错误号
pthread_mutex_t 类型,本质是一个结构体。为简化理解,应用是可以忽略实现细节,当整数对待
pthread_mutex_t mutex,变量mutex只有两种取值10
restrict 关键字----用来限定指针变量。被该关键字限定的指针变量所指向的内存操作必须由本指针完成.
参数attr指定了新建互斥锁的属性。如果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。
POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁

使用mutex一般步骤
1.创建锁:pthread_mutex_t lock;
2.初始化:
3.加锁:阻塞线程
4.访问共享数据:
5.解锁:唤醒阻塞在锁上的线程
6.销毁锁:
注意事项:锁的粒度越小越好,访问共享变量前加锁,访问完立刻解锁。
lock()和unlock():lock加锁失败会阻塞,等待锁释放;trylock加锁失败直接返回错误号(EBUSY),不阻塞。

//mutex例子
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<time.h>
#include<errno.h>
#include<string.h>

pthread_mutex_t mutex;//定义一把互斥锁

void* tfn(void* arg){
    srand(time(NULL));
    while(1){
        pthread_mutex_lock(&mutex);
        printf("小王,你好");
        sleep(rand()%3);//模拟长时间操作共享资源,导致cpu易主,产生时间相关错误
        printf(",我是小李\n");
        pthread_mutex_unlock(&mutex);

        sleep(rand()%3);//不能放在unlock上面,其他线程就基本没有机会拿到锁了,因为解锁万,立马又加锁
    }
    return NULL;
}
int main(int argc, char const *argv[])
{
    
    pthread_mutex_init(&mutex,NULL);
    pthread_t tid;
    pthread_create(&tid,NULL,tfn,NULL);
    srand(time(NULL));
    while(1){
        pthread_mutex_lock(&mutex);
        printf("小李,你好");
        sleep(rand()%3);//模拟长时间操作共享资源,导致cpu易主,产生时间相关错误
        printf(",我是小王\n");
        pthread_mutex_unlock(&mutex);

        sleep(rand()%3);
    }
    pthread_join(tid,NULL);
 
    pthread_mutex_destroy(&mutex);

    return 0;
}

死锁:是使用锁不恰当的现象:
1.对一个锁反复加锁。一个进程第一次加锁成功,再次加锁会阻塞,就没机会调用解锁函数了
2.进程1拿到A锁,进程2拿到了B锁,进程1阻塞等待B锁,进程2阻塞等待A锁。

3.7.3、读写锁

读共享、写独占
写锁优先级高
锁只有一把。以读方式加锁–读锁,以写方式加锁–写锁

1.读写锁是“写模式加锁”时,解锁,所有对该锁加锁的线程都会被阻塞。
2.读写锁是“读模式加锁”时,解锁,如果线程以读模式对其加锁会成功,写模式加锁会阻塞。
3.读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图读模式加锁的线程,写优先。如果前面已经有尝试加写被锁阻塞住的话,后续加读锁也都会被阻塞住(尽管当前时刻是读锁占用的状态)。这样做的目的主要是为了避免“写饥饿”,在“多读少写”的情况下防止数据修改延迟过高。读锁、写锁并行阻塞,写锁优先级高
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当以写锁模式锁住时,它是独占的,写独占、读共享

相比较互斥量而言,对读线程多的时候,读写锁提高了效率

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
以上7个函数成功返回0,失败直接返回错误号。
pthread_rwlock_t类型用于定义一个读写锁变量

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<time.h>
#include<errno.h>
#include<string.h>
int counter;
pthread_rwlock_t rwlock;//全全局读写锁

//3个线程不定时写同一个资源,5个线程不定时读资源
void *th_write(void *arg){
    int t;
    int i = (long)arg;
    while(1){
        pthread_rwlock_wrlock(&rwlock);//写模式加速,写独占
        t = counter;
        usleep(1000);
        printf("**write %d,%lu: counter = %d, ++counter = %d\n",i,pthread_self(),t,++counter);
        pthread_rwlock_unlock(&rwlock);
        usleep(10000);
    }
    return NULL;
}
void* th_read(void* arg){
    int i = (long)arg;
    while(1){
        pthread_rwlock_rdlock(&rwlock);//读现场间,读锁共享
        printf("-------read %d,%lu: %d\n",i,pthread_self(),counter);
        pthread_rwlock_unlock(&rwlock);
        usleep(2000);
    }
    return NULL;
}
int main(int argc, char const *argv[])
{
    int i;
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock,NULL);
    for(i=0;i<3;i++){
        pthread_create(&tid[i],NULL,th_write,(void*) i);
    }
    for(i=0;i<5;i++){
        pthread_create(&tid[i+3],NULL,th_read,(void*)i);
    }
    for(i=0;i<8;i++){
        pthread_join(tid[i],NULL);
    }
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

3.7.4、条件变量

本身不是锁!但是通常结合锁来使用。mutex

int pthread_cond_init(pthread_cont_t* restrict cond,const pthread_condattr_t* restrict attr); ---->初始化条件变量
int pthread_cond_destroy(pthread_cont_t* cond)   ----->销毁一个条件变量

int pthread_cond_wait(pthread_cont_t* restrict cond,pthread_mutex_t* restrict mutex)  ----->阻塞等待一个条件变量
作用:
	1.阻塞等待条件变量cond满足
	2.释放已掌握的互斥锁(解锁)---说明在这个函数前已经得到某个锁了。
	以上两步为一个原子操作
	3.当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁(上锁)
	
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex,
           const struct timespec *restrict abstime) ---->等待一个条件变量,同时可以设置等待超时

int pthread_cond_signal(pthread_cont_t* restrict cond)----->唤醒阻塞在信号变量上的(至少)一个线程

int pthread_cond_broadcast(pthread_cont_t* restrict cond)----->唤醒阻塞在条件变量上的所有线程

以上6个函数的返回值都是,成功返回0,失败直接返回错误号
phread_cond_t 类型 用于定义条件变量。
初始化条件变量:
	1.pthread_cond_init(&cond,NULL)   ---->动态初始化
	2.pthread_cond_t cond = PTHREAD_COND_INITALIZER; ----->静态初始化
//借助条件变量模拟生产者-消费者问题
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<stdio.h>
//链表作为共享数据
struct msg{
    struct msg* next;
    int num;
};
struct msg* head;
pthread_cond_t has_producer = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* consumer(void *p){
    struct msg *mp;
    for(;;){
        pthread_mutex_lock(&lock);
        while(head == NULL){
            pthread_cond_wait(&has_producer,&lock);
        }
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&lock);
        printf("Consumer %lu----%d\n",pthread_self(),mp->num);
        free(mp);
        sleep(rand()%5);
    }
}
void* producer(void*  p){
    struct msg* mp;
    for(;;){
        mp = (msg*)malloc(sizeof(struct msg));
        mp->num = rand()%1000+1;
        printf("produce ---- %d\n",mp->num);
        pthread_mutex_lock(&lock);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&lock);
        pthread_cond_signal(&has_producer);//将等待在该条件变量上的一个线程唤醒
        sleep(rand()%5);
    }
}
int main(int argc, char const *argv[])
{
    pthread_t pid,cid;
    srand(time(NULL));
    pthread_create(&pid,NULL,producer,NULL);
    pthread_create(&cid,NULL,consumer,NULL);

    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    return 0;
}

3.7.5、信号量

可以应用于线程、进程间同步。
进化版互斥锁。由于互斥锁的粒度比较大,如果我们希望多个线程间对某一个对象的部分数据共享,使用互斥锁是没有相应实现的,只能将整个数据对象锁住。这样虽然达到了多线程共享数据时保证数据的正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成串行执行,与直接使用单进程无差别。
信号量:是一种相对折中的处理方式,既能保证同步,又能提高并发。

int sem_init(sem_t* sem,int pshared,unsigned int value) ---->初始化信号量
参数:
pshared:
	0:用于线程间同步
	1:用于进程间同步
value:N值,指定同时访问的线程数。
int sem_destroy(sem_t* sem)  ---->销毁信号量
int sem_wait(sem_t* sem)   -----阻塞
int sem_trywait(sem_t* sem)
int sem_timewait(sem_t* sem,const struct timespec* abs_timeout)
/*定时1s
time_t cur = time(NULL);
struct timespec t;
t.tv_sec = cur+1;
t.tv_nsec = t.tv_sec + 100;

*/

int sem_post(sem_t* sem)
以上6个函数返回值:正确返回0,失败返回-1,设置errno(没有pthread前缀)
sem_t 本质是结构体。但简单看做整数
规定信号量不能<0.头文件<semaphore.h>

信号量基本操作:
	sem_wait:sem>0,则信号量--;sem=0,则线程阻塞(类比加锁)
	sem_post: 将信号量++,同时唤醒阻塞在信号量上的线程(类比解锁)
	区别在于:mutex相当于只能取01;sem可以取>=0
/*借助信号模拟生产者-消费者问题,生产者生产产品,放入工厂,工厂总空间为NUM,一个产品占用一个空间,消费者消费工厂中的产品。
工厂剩余空间为0,生产者阻塞;工厂中产品为0,消费者阻塞
*/
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<stdio.h>
#include<semaphore.h>
//
#define NUM 5
int queue[NUM];//全局数组实现环形队列
sem_t blank_number,product_number;
void* consumer(void *arg){
    int i = 0;
    while(1){
        sem_wait(&product_number);//消费者将产品数--,为0阻塞
        printf("consume ---- %d\n",queue[i]);
        queue[i]=0;//消费
        sem_post(&blank_number);//消费后,空间大小++
        i = (i+1)%NUM;
        sleep(rand()%3);
    }
}
void* producer(void*  arg){
    int i = 0;
    while(1){
        sem_wait(&blank_number);//生产者将空间--,为0阻塞
        queue[i] = rand()%1000 +1;//生产
        printf("produce----%d\n",queue[i]);
        sem_post(&product_number);//借助下标实现产品数++
        i = (i+1)%NUM;
        sleep(rand()%1);
    }
    
}
int main(int argc, char const *argv[])
{
     srand(time(NULL));
    pthread_t pid,cid;
    sem_init(&blank_number,0,NUM);//初始化工厂剩余空间为NUM,线程间共享
    sem_init(&product_number,0,0);//初始化产品数为0
    pthread_create(&pid,NULL,producer,NULL);
    pthread_create(&cid,NULL,consumer,NULL);
    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    sem_destroy(&blank_number);
    sem_destroy(&product_number);
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值