shell是命令解释器,根据输入的命令执行相应命令。
查看当前系统下有哪些shell,cat /etc/shells
查看当前系统正在使用的shell,echo $SHELL
bash是大多数 linux系统以及mac os x10.4默认的shell,它能运行于大多数unix风格的操作系统之上,甚至被移植到了windows的cygwin系统中,以实现windows的posix虚拟接口。此外,它也被djgpp项目移植到了ms-dos上。
命令和路径的补齐
在bash下敲命令,tab键可以补全已经敲了一部分的文件名或目录名。
历史记录,是很方便的功能。按上下光标键(或者ctrl+p、crtl+n)可以一条一条浏览以前输过的命令。在shell输入history可以查看所有的历史命令。
快捷键
ctrl+p,上一条历史命令
ctrl+n,下一条历史命令
ctrl+b,光标左移
ctrl+f,光标右移
ctrl+d,删除光标后面的
ctrl+a,光标到行首
ctrl+e,光标到行尾
backspace,删除光标前面的
linux中没有盘符,只有一个/根目录,所有文件都在它下面。
bin,系统可执行程序,如命令。
boot,内核和启动程序,所有和启动相关的文件都保存在这里。
grub,引导器相关文件。
dev,设备文件。
etc,系统软件的启动和配置文件,系统在启动过程中需要读取的文件都在这个目录。如lilo参数、用户账户和密码。
home,用户的主目录。
lib,系统程序文件,存放着系统最基本的动态链接共享库,类似于windows下的system32目录。几乎所有的应用程序都需要用到这些共享库。
media,挂载媒体设备,如光驱、u盘等。
opt,可选的应用软件包。
proc,这个目录是系统内存的映射,可以直接访问这个目录来获取系统信息。这个目录的内容不再硬盘上而在内存里。
sbin,管理员系统程序。
selinux
srv
sys,udev用到的设备目录树。
tmp,临时文件夹。
usr,这是个最庞大的目录,用到的很多应用程序和文件几乎都存在这个目录下。
bin,应用程序。
game,游戏程序。
include
lib,应用程序的库文件。
lib 64
local,包含用户程序等。
sbin,管理员应用程序。
list的缩写,列出目录的内容。
-a 列出隐藏文件,文件中以.开头的文件。
-l 列出文件的详细信息。
-R 连同子目录中的内容一起列出。
文件类型
- 普通文件
d 目录
l 符号链接
b 块设备文件
c 字符设备文件
s socket文件,网络套接字
p 管道
change dir
cd ~
cd dir4/dir2
cd ..
查看命令所在的路径。
which ls
查看当前所在路径。
创建目录,可以一次创建多个。参数为-p,表示可以连同父目录一起创建。
mkdir -p aa/bb/cc
删除空目录,可以一次删除多个。参数为-p,可以连同空的父目录一起删除。
将每个文件的访问及修改时间都更新为目前的时间。如果文件不存在,则创建一个字节数为0的文件。
删除文件rm file
删除目录rm dir -rf
重命名mv file1 file2
移动文件mv file1 dir(文件路径)
拷贝文件
cp file1 file2
cp file1 dir/
cp file1 ../
拷贝目录
cp dir1 dir2 -r
cp dir1 ~/ -r
查看文件里内容,输出到终端,如果cat时没有跟文件名,则读标准输入,遇到\r输出到标准输出,终端下输入ctrl+d表示结束。
查看文本文件的内容,屏幕显示完一屏就等待用户按下任意键再滚动到下一屏,中途不想看下去了,按ctrl+c或者q终止显示。
查看文本文件的内容,屏幕显示完一屏就等待 用户按键,用户可以向上或向下查看,按ctrl+c或q终止显示。
显示指定文件的前面几行。如果没有指定文件,将从标准输入上读取。如果没有指定要显示的行数,则默认显示前10行。如果要显示前n行,head -n file1
显示文件的最后几行。若没有指定显示的行或字符数,则默认显示末尾10行。如果要显示末尾n行,tail -n file1
链接有两种,一种为硬链接(hard link),另一种为符号链接(symbol link)。建立硬链接时,链接文件和被链接文件必须位于同一个文件系统中,并且不能建立指向目录的硬链接。而对符号链接,则不存在这个问题。默认情况下,ln产生硬链接。如果给ln命令加上-s选项,则建立符号链接。
ln file1 file2
ln -s file1 file2
这个命令需要下载安装。按结构树的形状显示目录和文件。
利用wc指令可以计算文件的byte数、字数、或是列数,若不指定文件名称,就是所给予的文件为-,则wc指令会从标准输入设备读取数据。
-c或-bytes或-chars,只显示bytes数。
-l或-lines,只显示列数。
-w或-words,只显示字数。
od -tcx file1
-t 指定数据的显示格式,主要的参数有:
c ascii字符或反斜杠序列。
d 有符号十进制数,每个整数size字节。
f 浮点数,每个整数size字节。
o 八进制,系统默认值为02,每个整数size字节。
u 无符号十进制数,每个整数size字节。
x 十六进制数,每个整数size字节。
查看某个目录的大小。
以M为单位du -hm file
以B为单位,du -hb file
以K为单位,4k的整数倍,du -hk file
查看磁盘使用情况。
df --block-size=GB
df --block-size=MB
查看当前登录用户
文字设定法
chmod [who] [+|-|=] [mode] 文件名
操作对象who可以是下述字母中的任一个或它们的组合。
u 即文件或目录的所有者。
g 表示同组用户,即文件属主有相同id的所有用户。
o 其他用户。
a 表示所有用户,是系统默认值
+ 添加某个权限
- 取消某个权限
= 赋予给定权限并取消其他所有权限
chmod u+x file
数字设定法
一个3位数,每一位为权限代表数之和,r4w2x1,百位数为所有者权限,十位为所有组权限,个位为其他用户权限。比如chmod 755 file。r4w2x1的原理是3个二进制位表示的组合
r w x 十进制数
0 0 0 0
0 0 1 1代表x
0 1 0 2代表w
1 0 0 4代表r
这样421可以表示所有的权限的组合,称为8421。
更改某个文件或目录的属主和属组。例如root用户把自己的一个文件拷贝给用户A,为了让A能够存储这个文件,root用户应该把这个文件的属主设为A,否则,A用户无法存取这个文件。
-R 递归式改变指定目录及其下的所有子目录和文件的拥有者。
-v 显示chown命令所做的工作。
chown 用户名:组名 file
改变指定文件所属的用户组。其中group可以是用户组id,也可以是etc/group文件中用户组的组名。文件名是以空格分开的要改变属组的文件列表,支持运算符。如果用户不是该文件的属主或超级用户,则不能改变该文件的组。
-R 递归式地改变指定目录及其下的所有子目录和文件的属组。
chgrp 组名 file
根据文件名查找
find [option] path [expression]
在目录中搜索文件,path指定目录路径,系统从这里开始沿着目录树向下查找文件。path是一个路径列表,相互用空格分离,如果不写path,那么默认为当前目录。expression是find命令接受的表达式,find命令的所有操作都是针对表达式的。
find . –name ‘file*’
find / -name ‘vim’
find ~ -name ‘*.c’
根据内容检索。
grep [option] pattern [file …]
在指定文件中搜索特定的内容,并将含有这些内容的行输出到标准输出。若不指定文件名,则从标准输入读取。
-c 只输出匹配行的计数
-I 不区分大小写(只适用单字符)
-h 查询多文件时不显示文件名
-R 从指定目录开始递归检索
-l 查询多文件时只输出包含匹配字符的文件名
-n 显示匹配行及行号
-s 不显示不存在或无匹配文本的错误信息
-v 显示不包含匹配文本
grep “hello” ./* -R
vi /etc/apt/source.list 软件源列表
更新完源服务器列表后需要更新软件源
apt-get update 更新源
apt-get install package 安装包
apt-get remove package 删除包
apt-cache search package 搜索软件包
apt-cache show package 获取包的相关信息,如说明、大小、版本等
apt-get install package --reinstall 重新安装包
apt-get -f install 修复安装
apt-get remove package --purge 删除包,包括配置文件等
apt-get build-dep package 安装相关的编译环境
apt-get upgrade 更新已经安装的包
apt-get dist-upgrade 升级系统
apt-cache depends package 了解使用该包依赖那些包
apt-cache rdepends package 查看该包被那些包依赖
apt-get source package 下载该包的源代码
apt-get clean && apt-get autoclean 清理无用的包
apt-get check 检查是否有损坏的依赖
dpkg -I xxx.deb 安装deb软件包命令
dpkg -r xxx.deb 删除软件包命令
dpkg -r --purge xxx.deb 连同配置文件一起删除命令
dpkg -info xxx.deb 查看软件包信息命令
dpkg -L xxx.deb 查看文件拷贝详情命令
dpkg -l 查看系统中已经安装软件包信息命令
dpkg -reconfigure xxx 重新配置软件包命令
解压缩源码代码包
cd dir
./configure 检测文件是否缺失,创建makefie,检测编译环境。
make 编译源码,生成库和可执行程序
make install 把库和可执行程序,安装到系统路径下
mount [-t vfstype] -o options device dir
-t vfstype 指定文件系统的类型,通常不必指定。mount会自动选择正确的类型。常用类型有:
光盘或光盘镜像 iso9660
dos fat16文件系统 msdos
windows 9x fat32文件系统 vfat
windows nt nftfs文件系统 ntfs
mount windows文件网络共享 smbfs
unix(linux)文件网络共享 nfs
-o options 主要用来描述设备或档案的挂载方式,常用的参数有:
loop 用来把一个文件当成硬盘分区挂接上系统
ro 采用只读方式挂接设备
rw 采用读写方式挂载设备
iocharset 指定访问文件系统所用字符集
检测存储设备名称fdisk -l
挂载存储设备sdb1到挂载点/mnt目录mount /dev/sdb1 /mnt
访问/mnt
卸载/mnt unmount /mnt
拷贝
拷贝光碟dd if = /dev/crdom of = cdrom.iso
将文件sfile拷贝到文件dfile中dd if = sfile of = dfile
创建一个100M的空文件dd if = /dev/zero of = hello.txt bs=100M count=1
/dev/null,外号无底洞,可以向它输出任何数据,不会溢出。
/dev/zeor,一个输入设备,可以用它来初始化文件,从里面读出来的数据都是0。
tar [主选项+辅选项] 文件或目录
tar可以为文件和目录创建档案。利用tar命令用户可以为某一特定文件创建档案(备份文件),也可以在档案中改变文件,或者向档案中加入新的文件。使用该命令时,主选项是必须的。
主选项包括
c 创建新的档案文件。
r 把要存档的文件追加到档案文件的末尾。
t 列出档案文件的内容,查看已经备份了哪些文件。
u 更新文件。用新增的文件取代原备份文件,如果在备份文件中找不到要更新的文件,则把它追加到备份文件的最后。
x 从档案文件中释放文件。
辅助选项
f 使用档案文件或设备,通常必选
k 保存已经存在的文件。
m 在还原文件时,把所有文件的修改时间设定为现在。
M 创建多卷的档案文件,以便在几个磁盘中存放。
v 详细报告tar处理的文件信息。通常选用。
w 每一步都要求确认。
z 用gzip来压缩/解压缩文件,加上该选项后可以将档案文件进行压缩,但还原时也一定要使用该选项进行解压缩。通常选用。
j 用bzip2来压缩/解压缩文件,加上该选项后可以将档案文件进行压缩,但还原时也一定要使用该选项进行解压缩。通常选用。
打包
tar cvf dir.tar dir
tar xvf dir.tar dir
打gz压缩包
tar zcvf dir.tar.gz dir
tar zxvf dir.tar.gz
打bz2压缩包
tar jcvf dir.tar.bz2 dir
tar jxvf dir.tar.bz2
指定目录解压缩
tar zxvf dir.tar.gz -C /home/xuexie
打包,把dir压缩成newdir.rar
rar a -r dir.rar dir
解包,把newdir.rar解压缩到当前目录
unrar x dir.rar
打包
zip -r dir.zip dir
解包
unzip dir.zip
查看当前在线上的用户情况。
who -uH
用于监控后台进程的工作情况。
-e 显示所有进程
-f 全格式
-h 不显示标题
-l 长格式
-w 宽输出
a 显示终端上的所有进程,包括其他用户的进程
r 只显示正在进行的进程
x 显示没有控制终端的进程
ps aux
ps ajx
ps -Lf
用来显示当前shell下正在运行哪些作业。
比如
cat
进入然后ctrl+z,将作业放到后台挂起
jobs
查看作业
多开几个cat
fg 2可以查看第2个作业进入,ctrl+c可以关闭这个作业
jobs查看
把指定的后台作业或挂起作业移到前台运行。
把被挂起的进程提到后台执行。或者执行可执行文件时空格&,比如./redis &
向指定进程发送信号
kill [-signal] -s signal
查看信号编号
kill -l [signal]
给一个进程发信号,或终止一个进程的运行
kill -9 进程号
查看当前进程环境变量。
配置当前用户环境变量vim ~/.bashrc
配置系统环境变量,配置时需要有root权限vim /etc/profile
在配置文件内export PATH=$PATH:添加的路径
useradd -s /bin/bash -g newuser -d /home/newuser -m newuser
useradd -s /bin/sh -g group -G adm,root xwp
此命令新建了一个用户xwp,该用户的登陆shell是/bin/sh,属于group用户组,同时又属于adm和root用户组,其中group用户组是其主组。
-s 指定新用户登陆时shell类型
-g 指定所属组,该组必须已经存在
-G 指定附属组,该组必须已经存在
-d 用户家目录
-m 用户家目录不存在时,自动创建该目录
groupadd newgroup
passwd user
su user
变成root用户
su
设置root密码
passwd
userdel 选项 用户名
常用的选项是-r,把用户的主目录一起删除。userdel -r user此命令删除用户user在系统文件中的记录,同时删除用户的主目录。
查看网卡信息
关闭网卡ifconfig eth0 down
开启网卡ifconfig eth0 up
给eth0配置临时ip ifconfig eth0 IP
ping [选项] 主机名/ip地址
查看网络上的主机是否在工作。
参数
-c 数目,在发送指定数目的包后停止。
-d 设定SO_DEBUG的选项。
-f 大量且快速地送网络封包给一台机器,看它的回应。
-I 秒数,设定间隔几秒送一个网络封包给一台机器,预设值是一秒送一次。
-l 次数,在指定次数内,以最快的方式送封包数据到指定机器。
-q 不显示任何传送封包的信息,只显示最后的结果。
-r 不经由网关而直接送封包到一台机器,通常是查看本机的网络接口是否有问题。
-s 字节数,指定发送的数据字节数,预设值是56,加上8字节的ICMP头,一共是64ICMP数据字节。
netstat [选项]
显示网络连接、路由器和网络接口信息,可以让用户得知目前有哪些网络连接正在运作。
-a 显示所有socket,包括正在监听的。
-c 每个1秒就重新显示一遍,直到用户中断它。
-i 显示所有网络接口的信息,格式同ifconfig -e。
-n 以网络ip地址代替名称,显示出网络连接情形。
-r 显示核心路由表,格式同route -e。
-t 显示tcp协议的连接情况。
-u 显示udp协议的连接情况。
-v 显示正在进行的工作。
nslookup name
查询一台机器的ip地址和其对应的域名。需要一台域名服务器提供域名服务。
不带参数使用nslookup命令时,出现提示符>,在后面输入要查询的ip地址或域名,并回车即可。如果要退出该命令,输入exit并回车即可。
finger [-lmsp] user [user@host…]查询用户的信息,通常会显示系统中某个用户的用户名、主目录、停滞时间、登陆时间、登陆shell等信息。
-s 显示用户的注册名、实际姓名、中断名称、写状态、停滞时间、登陆时间等信息。
-l 除了用-s选项显示的信息外,还显示用户主目录、登陆shell、邮件状态等信息,以及用户主目录下的.plan、.project和.forward文件的内容。
-p 除了不显示.plan文件和.project文件外,与-l选项相同。
文件传输协议file trans protocol
安装ftp服务器
配置vsftpd.conf文件vi /etc/vsftpd/vsftpd.conf
添加如下设置
anonymous_enable=YES
anon_root=/home/haha/ftp
no_anon_password=YES
write_enable=YES
anon_upload_enable=YES
anon_mkdir_write_enable=YES
重启服务器,重新加载/etc/vsftpd.conf配置文件/etc/init.d/vsftpd restart
进入/home/haha/ftp目录下创建一个空目录,供用户上传
cd ~/ftp
mkdir anonymous
chmod 777 anonymous
测试上传功能,登陆ftp服务器,进入到anonymous目录
ftp ip
cd anonymous
上传put somefile
安装ftp客户端测试上传下载
lftp客户端,也是一种ftp客户程序。是以文本方式操作的,比起图形界面更为方便。lftp几乎具有bash的所有方便功能,tab补齐,bookmark,queue,后台下载等。用户与ftp类似。
put 上传文件
mput 上传多个文件
get 下载文件
mget 下载多个文件
mirror 下载整个目录及其子目录
mirror -R 上传整个目录及其子目录
!command 调整本地shell执行命令command
网络文件系统net file system
安装nfs服务器
设置/etc/exports配置文件,vi /etc/exports
添加这行配置
/home/用户名/nfs *(rw,sync,no_root_squash)
在用户目录下创建nfs目录
mkdir /home/用户名/nfs
重启服务器,重新加载配置文件
在/home/用户名/nfs目录下创建测试文件hello
测试服务器,把服务器共享目录nfs挂载到/mnt节点mount -t nfs -o nolock -o tcp IP:/home/用户名/nfs /mnt
进入/mnt目录下可以看到hello文件,表示构建成功
卸载网络共享目录umount /mnt
安装ssh
远程登录ssh 用户名@IP
shift-pageup
shift-pagedown
看手册。每一个命令和系统函数都有自己的man page。
man man
man read 查看read命令的man page
man 2 read 查看read系统函数的man page
man -k read 以read为关键字查找相关的man page
清屏,使光标和提示符回到屏幕第一行。快捷键ctrl+l。
alias [-p] name =value…
将value字符串起个别名叫name,以后在命令行输入name,shell自动将其解释为value,如果不带参数执行本命令,或以参数-p执行,则显示当前定义的别名列表。
alias ls=’ls --color=auto’
alias rm=’rm -i’
echo [-n] 字符串
在显示器上显示一段文字,一般起到一个提示的作用。其中选项n表示输出文字后不换行,字符串可以加引号,也可以不加引号。用echo命令输出加引号的字符串时,将字符串原样输出,用echo输出不加引号的字符串时,将字符串中的各个单词作为字符串输出,各字符串之间用一个空格分割。
echo $?查看上一个程序退出数值,正常情况下程序退出值是0。
查看当前时间
umask [-p] -S [mode]
umask指定用户创建文件时的掩码,其中的mode和chmod的命令中的格式一样。如果不用mode参数,则显示当前的umask设置。如果用-S参数,则以符号形式显示设置。
umask 0002
关机
shutdown -t 秒数 [-rkhncfF] 时间 [警告讯息]
-t 设定在切换至不同的runlevle之前,警告和删除二讯号之间的延迟时间。
-k 仅送出警告讯息文件,但不是真的shutdown
-r shutdown之后重启
-h shutdown之后关机
-n 不经过init,有shutdown指令本身来做关机动作,不建议使用。
-f 重新开机时,跳过fsck指令,不检查档案系统
-F 重新开机时,强迫做fsck检查
-c 将已经正在shutdown的动作取消
重启
lsb_release -a
free -m
visual interface,类似dos上的edit。可以执行输出、删除、查找替换、块操作。
vi有三种基本工作模式,命令模式;文本输入模式;末行模式。
命令模式
任何时候,按下esc,可以vi进入命令模式。
在命令模式下移动光标
h 左移
j 下移
k 上移
l 右移
M 光标移动到中间行。
L 光标移动到屏幕最后一行行首
G 移动到指定行,行号G,比如2G
w 向后一次移动一个字。
b 向前一次移动一个字。
{ 按段移动,上移。
} 按段移动,下移。
ctr+d 向下翻半屏
ctr+u 向上翻半屏
ctr+f 向下翻一屏
ctr+b 向上翻一屏
gg 光标移动到文件开头
G 光标移动到文件末尾
删除命令
x 删除光标后一个字符,相当于del
X 删除光标前一个字符,相当于backspace
dd 删除光标所在行,n dd删除指定的行数
D 删除光标所在字符
d0 删除光标前本行所有内容,不包含光标所在字符
dw 删除光标开始位置的子,包含光标所在字符
撤销
u 一步一步撤销
U 一次性撤销当前行所作的所有操作
ctr+r 反撤销
重复命令
. 重复上一次操作的命令
文本行移动
>> 文本行右移
<< 文本行左移
复制粘贴
yy 复制当前行,yy复制n行
p 在光标所在位置向下新开辟一行,粘贴
可视模式
v 按字符移动,选中文本
V 按行移动,选中文本可视模式可以配合d,y,>>,<<实现对文本的选中
替换操作
r 替换当前字符
R 替换当前行光标后的字符
分屏操作
进入末行模式:
sp 上下分屏,后可跟文件名
vsp 左右分屏,后可跟文件名
ctr+w+w 在多个窗口切换
启动分屏
使用大写O参数进行垂直分屏
vim -On file1 file2
使用小写o参数进行水平分屏
vim -on file1 file2
注意:n是数字,表示分屏的数量
关闭分屏
关闭当前窗口ctrl+w c
关闭当前窗口,如果只剩最后一个,则退出vi或vim。ctrl+w q
编辑中分屏
上下分割当前打开的文件ctrl+w s
上下分割,并打开一个新的文件:sp filename
左右分割当前打开的文件:vsp filename
左右分割,并打开一个新的文件:vsp filename
分屏编辑中光标的移动
vi中的光标键是hjkl,要在各个屏之间切换,只需要先按一下ctrl+w
把光标移动到上边的屏ctrl+w k
把光标移动到下边的屏 ctrl+w j
把光标移动到右边的屏 ctrl+w l
把光标移动到左边的屏 ctrl+w h
把光标移动到下一个屏 crtl+w w
移动分屏
向上移动ctrl+w K
向下移动 ctrl+w J
向右移动 ctrl+w L
向左移动 ctrl+w H
屏幕尺寸
增加高度ctrl+w +
减少高度 ctrl+w -
让所有的屏高度一致ctrl+w =
左加宽度ctrl+w >
右加宽度ctrl+w <
右增加n宽ctrl+w n <
查找命令/
查兰man page
光标移动到函数上,shift + k
光标移动到函数上,3shift + k,查看第三章的manpage
查看宏定义
[-d 可以查看宏定义,必须先包含此宏所在的头文件。
代码排版
gg=G 代码自动缩进排版
文本输入模式
在命令模式下,按下a(光标在后)、A(行尾)、i(光标在前)、I(行首)、o(向下新开一行,插入行首)、O(向上新开一行,插入行首),进入插入模式。
末行模式
在命令模式下,按下shitf+;(也就是:)。末行模式命令:
w 存盘。
wq 存盘,退出。
q! 不存盘,退出。
vimrc是vim的配置文件,可以修改两个位置
/etc/vim/vimrc
~/.vimrc
~/.vimrc优先级高
百度搜索一些vim配置文件。
-v /-v /-version查看gcc版本号
-I 指定头文件目录,注意-I和路径之间没有空格,比如gcc -I./include -o test.c dict.c app
-c 只编译,生成.o文件,不进行链接
-g 包含调试信息
-On n=0~3编译优化,n越大优化得越多
-Wall 提示更多警告信息
-D 编译时定义宏,注意-D和宏定义之间没有空格gcc -DDEBUGtest2.c -o test21
-E 生成预处理文件
-M 生成.c文件与头文件依赖关系以用于makefile,包括系统库的头文件
-MM 生成.c文件与头文件依赖关系以用于makefile,不包括系统库的头文件
binutils一组用于编译、链接、汇编和其他调试目的的程序,包括ar、as、ld、nm、objcopy、objdump等。
ar 打包生成静态库
as 汇编器
ld 链接器。gcc完成链接步骤,其实就是gcc调用ld,将用户编译生成的目标文件连同系统的libc启动代码链接在一起形成最终的可执行文件。
nm 查看目标文件中的符号(全局变量、全局函数等)。
objcopy 将原目标文件中的内容复制到新的目标文件中,可以通过不同的命令选项调整目标文件的格式,比如去除某些ELF文件头。
objdump 用于生成反汇编文件,主要依赖objcopy实现,a.out编译时需要-g,objdump-dSsx a.out>file。
ranlib 为静态库文件创建索引,相当于ar命令的s选项。
readelf 解读ELF头文件。
程序库,就是包含了数据和执行代码的文件。程序库文件不能单独执行,可以作为其他执行程序的一部分来完成某些功能。库,可以使得程序模块化,加快程序的再编译,实现代码重用,使得程序用于升级。程序库分为静态库(static library)和共享库(shared library)。
在可执行程序运行前就已经加入到执行码中,成为执行程序的一部分。静态库可以认为是一些目标代码的集合。一般以.a作为文件后缀名。使用ar(archiver)命令可以创建静态库。因为共享库有更大的优势,静态库已经不经常使用,但是静态库使用简单,仍有使用的余地,并会一直存在。
静态库在应用程序生成时,可以不必再编译,节省再编译时间。但在编译器越来越快的情况下,这一点似乎已不重要。
创建一个静态库,或要将目标代码加入到已经存在的静态库中,可以使用命令:ar rcs libmylib.a file1.o file2.o。表示把目标码file1.o和file2.o加入到静态库libmylib.o中。若libmylib.a不存在,会自动创建。然后更新.a文件的索引,使之包含新加入的.o文件的内容。
静态库创建成功后,需要链接到应用程序中使用。使用gcc的-l选项来指定静态库,使用-L参数来指定库文件的搜索路径。-l和-L之后都直接带参数而不跟空格。
使用gcc时,要注意其参数的顺序。-l是链接器选项,一定要放在被编译的文件名称之后,若放在名称之前则会链接失败,并会出现莫名其妙的错误。
在执行程序启动时加载到执行程序中,可以被多个执行程序共享使用。优势是独立,便于维护和更新。
共享库的创建,首相使用-fPIC或-fpic创建目标文件,PIC或pic表示位置无关代码,然后就可以使用以下格式创建共享库了
高级用法
gcc -share -Wl,-soname,your_soname -o library_name file_list library_list
一般用法
gcc -fPIC -c a.c
gcc -fPIC -c b.c
gcc -shared -Wl -o libmyay.so a.o b.o
高级用法例子
gcc -share -Wl,-soname,libmyab.so.1 -o libmyab.so.1.10 a.o b.o
按照共享库的命名惯例,每个共享库有三个文件名,real name、soname和linker name。真正的库文件的名字是real name,包含完整的共享库版本号。
soname是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic段只记录共享库的soname,只要soname一致,这个共享库就可以用。比如libmyab.so.1和libmyab.so.2是两个主版本号不同的libmyab,有些应用程序依赖于libmyab.so.1,有些应用程序依赖于libmyab.so.2,但对于依赖libmyab.so.1的应用程序来说,真正的库文件不管是libmyab.so.1.10还是libmyab.so.1.11都可以用,所以使用共享库可以很方便地升级文件而不需要重新编译应用程序,这是静态库所没有的优点。
linker name仅在编译链接时使用,gcc的-L选项应该指定linker name所在目录。有的linker name是库文件的一个符号链接,有的linker name是一段链接脚本。
在所有基于GNUglibc的系统中,在启动一个ELF二进制执行程序时,一个特殊的程序“程序装载器”会被自动装在并运行。在linux中,这个程序装载器就是/lib/ld-linux.so.X。它会查找并装载应用程序所依赖的所有共享库。被搜索的目录保存在/etc/ld.so.conf文件中。为了避免程序每次启动,都去搜索,linux系统,对共享库采用了缓存管理。ldconfig就是实现这一功能的工具,缺省读取/etc/ld.so.conf文件,对所有共享库按照一定规范建立符号连接,然后将信息写入/etc/ld.so.cache。/etc/ld.so.cache的存在大大加快了程序的启动速度。
修改/etc/ld.so.conf
vi /etc/ld.so.conf
添加共享库路径
更新查找共享库的路径
ldconfig -v
在/home/haha/test目录下创建lib目录,src目录和main.c文件和common.h文件
main.c文件
#include
#include"common.h"
int main(void)
{
printf("%d",add(2,5));
return 0;
}
common.h文件
#include
int add(int a,int b);
int dive(int a,intb);
int mul(int a,int b);
int sub(int a,int b);
在src目录下分别创建add.c、dive.c、mul.c和sub.c文件
比如add.c
#include
int add(int a, int b)
{
return a + b;
}
在src目录下
gcc -c -fPIC *.c
生成所有的编译文件。
在src目录下,创建静态库
ar rcs libmycalc.a *.o
nmlibmycalc.a可以查看静态库中的编译文件和函数
将生成的libmycalc.a文件copy到lib目录下cp libmycalc.a ../lib/
在test目录下
gcc mian.c lib/libmycalc.a -o app
编程生成可执行文件。
ldd app查看依赖的库的指引位置信息。
静态库的优点是直接将app发布出去可以使用。缺点是静态库文件数据编译到app中,app体积大。
利用上述静态库的案例
假设没在src文件下还没有.o文件
在src下生成库源码的编译文件
gcc -c -fPIC *.o
在src生成共享库的版本库文件
gcc -shared -Wl,-soname,libmycalc.so.1 -o libmycalc.so.1.10 *.o
将libmycalc.so.1.10copy到lib目录下
将共享库的路径添加到ld.so.conf文件中
vi /etc/ld.so.conf
添加
/home/haha/test/lib
更新库路径文件
ldconfig -v
在test文件下
gcc main.c lib/libmyclac.so.1.10 -o app
ldd app查看共享库路径是否可找到
在lib目录下
ln -n libmycalc.so.1.10 libmycalc.so生成静态库的real name。
gnu debug,gdb可以完全操控程序的运行,随时可以查看程序中所有的内部状态,比如各变量的值、传给函数的参数、当前执行的语句位置等。
如果要使用gdb,在编译程序的时候需要添加-g参数。比如,gcc -g main.c -o main。
常用命令
命令 简写 作用
help h 按模块列出命令类
help class 查看某一类型的具体命令
list l 查看代码,可跟行号和函数名
quit q 退出gdb
run r 全速运行程序
start 单步执行,运行程序,停在第一行执行语句
next n 逐过程执行
step s 逐语句执行,遇到函数,跳到函数内执行
backtrace bt 查看函数的调用的栈帧和层级关系
info i 查看函数内部局部变量的数值
frame f 切换函数的栈帧
finish 结束当前函数,返回到函数调用点
set 设置变量的值
run argv[1] argv[2] 调试时命令行传参
print p 打印变量的地址
break b 设置断点,可根据行号和函数名
delete d 删除断点,d breakpoints NUM
display 设置观察变量
undisplay 取消观察变量
continue c 继续全速运行剩下的代码
enable breakpoints 启动断点
disable breakpoints 禁用断点
x 查看内存x /20xw 显示20个单元,16进制,4字节每单元
watch 被设置观察点的变量发生修改时,打印显示
i watch 显示观察点
core文件 ulimit -c 1024 开启core文件,调试时gdb app core
gdb的调试模式
run 全速运行
start 单步调试
se follow-fork-mode child # Makefile 项目管理
用途是项目代码编译管理,节省编译项目时间,一次编写多次使用。
工作原理
分析各个目标和依赖之间的额关系;根据依赖关系自底向上执行命令;根据修改时间比目标新,确定更新;如果目标不依赖任何条件,则执行对应命令,以示更新。
例子
在calc目录下,创建一个做加减乘除的项目,四个方法分别为一个.c文件,有common.h头文件声明4个方法,在main.c程序中调用4个方法。
在calc目录下,编写Makefile文件
#目标:依赖(条件)
# 命令
#app:add.c sub.cdive.c mul.c main.c
# gcc add.c sub.c dive.c mul.c main.c -oapp
app:add.o sub.odive.o mul.o main.o
gcc add.o sub.o dive.o mul.o main.o -oapp
add.o:add.c
gcc -c add.c
sub.o:sub.c
gcc -c sub.c
dive.o:dive.c
gcc -c dive.c
mul.o:mul.c
gcc -c mul.c
main.o:main.c
gcc -c main.c
clean:
rm *.o
rm app
执行make,默认运行第一个关系树,即app。执行make clean,指定执行第二个关系树。make add.o可以执行某个关系树中的命令。
用途是清除编译生成的中间.o文件和最终目标文件。make clean,如果当前目录下有同名clean文件,则不执行clean对应的命令。见上面的例子。解决的方法是,使用伪目标声明:.PHONY:clean。
.PHONY:clean
clean:
rm -f *.o
rm -f app
clean命令中的特殊符号
- 此命令出错,make也会基础执行后续的命令,如-rm main.o
@ 不显示命令本身,只显示结果。如,@echo “clean done”
例子
.PHONY:clean
clean:
-rm add.o
-rm sub.o
rm mul.o
rm -f app
test:
@echo "hello"
使用$@表示目标,$^表示所有依赖,$<表示依赖中的第一个来增强编译代码的通用性
src = $(wildcard *.c)
obj = $(patsubst%.c,%.o,$(src))
target = app
$(target):$(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $< -o $@
.PHONY:clean
clean:
-rm add.o
-rm sub.o
rm mul.o
-rm -f *.o
rm -f app
test:
@echo "hello"
@echo $(src)
@echo $(obj)
增加通用性参数的例子
CPPFLAGS= -Iinclude
CFLAGS= -g -Wall
LDFLAGS=
CC=gcc
src = $(wildcard *.c)
obj = $(patsubst%.c,%.o,$(src))
target = app
$(target):$(obj)
$(CC) $^ -o $(LDFLAGS) $@
%.o:%.c
$(CC) -c $< $(CFLAGS) $(CPPFLAGS) -o$@
其他
distclean 目标 #彻底清除生成过程文件和生成配置文件
install 目标 #将生成的程序拷贝到/usr/bin目录下
例子
disclean:
rm /usr/bin/app
install:
cp app /usr/bin
make -C 指定目录 #进入指定目录,调用里面的Makefile
all:
make -C src
c标准函数I/O,每一个FILE文件流都有一个缓冲区buffer,默认大小为8192byte。系统函数直接读写的。
task_struct结构体
在操作系统内核中定义关于任务的结构体
位置/usr/src/linux-headers(也可能是其他路径比如kernels/2.x.x.xxx)/include/linux/sched.h
PCB会描述files_struct结构体
一个进程默认打开3个文件描述符。
新开文件返回文件描述符表中未使用的最小文件描述符。open函数可以打开或创建一个文件。
open(const char* pathname, int flags,…)
pathname参数是要打开或创建的文件名,和fopen一样,pathname既可以是相对路径也可以是绝对路径。flags参数有一些列常数值可选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都已O_开头,表示or。
必选项,三个,且仅允许指定一个。
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 可读可写打开
以下选项可以同时指定0个或多个,和必选项按位或起来作为flags参数。其中的一部分
O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该文件的访问权限。
O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断(Truncate)为0字节。
O_NONBLOCK 对于设备文件,以O_NONBLACK方式打开可以做非阻塞I/O(Nonblack I/O)。
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int fd;
char buf[1024] = "linux";
if (argc < 2)
{
printf("./appfilenname\n");
exit(1);
}
fd = open(argv[1], O_CREAT | O_RDWR |O_EXCL, 0777);
//fd = open(argv[1], O_RDWR);
//fd = open(argv[1], O_RDWR |O_APPEND);
printf("fd=%d\n", fd);
write(fd, buf, strlen(buf));
close(fd);
return 0;
}
open函数与c标准I/O的fopen的区别:
以可写的方式fopen一个文件时,如果文件不存在会自动创建;open一个文件时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节;open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。
第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r—r--,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示。注意的是,文件权限由open的mode参数和当前进程的umask掩码共同决定。
查看当前系统允许打开最大文件个数
cat /proc/sys/fs/file-max
ulimit -a查看系统中设置打开文件的个数等相关信息。ulimit -n 4096可以更改open files为4096。
read函数从打开的设备或文件中读取数据。
ssize_t read(int fd, void *buf, size_t count);
返回值,成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾。
参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时写位置后移。
write函数向打开的设备或文件中写数据。
wirte(int fd, const void *buf, size_t count);
返回值,成功返回写入的字节数,出错返回-1并设置errno。
写常规文件时,write的返回值通常等于请求写的字节数count,而向终端写则不一定。
#include
#include
#include
#include
#include
#include
#define SIZE 8192
int main(int argc,char *argv[])
{
char buf[SIZE];
int fd_src, fd_dest, len;
if(argc < 3)
{
printf("./mycp srcdest\n");
exit(1);
}
fd_src = open(argv[1], O_RDONLY);
fd_dest = open(argv[2], O_CREAT |O_WRONLY | O_TRUNC, 0644);
while ( (len = read(fd_src, buf,sizeof(buf))) > 0)
{
write(fd_dest, buf, len);
}
close(fd_src);
close(fd_dest);
return 0;
}
测试
gcc mycp.c -o cpapp
cpapp test.c testcp.c
当进程调用一个阻塞的系统函数时,该进程被置于睡眠状态,这时内核调度其他进程运行,直到该进程等待的事发生了,它才能继续运行。
与睡眠状态相对的是运行状态,在linux内核中,处于运行状态的进程分为两种情况
正在被调度执行。cpu处于该进程的上下文环境中,程序计数器里保存着该进程给的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
就绪状态,该进程不需要等待什么事件发生,随时都可以执行。但cpu暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时兼顾用户体验,不能让和用户交互的进程响应太慢。
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端设备输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,如果一直没有数据到达就会一直阻塞。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
阻塞读终端的例子
#include
#include
#include
#include
int main(void)
{
char buf[1024];
int len;
len = read(STDIN_FILENO, buf,sizeof(buf));
write(STDOUT_FILENO, buf, len);
return 0;
}
非阻塞,一般称为轮询,反复的询问。
非阻塞读终端的例子
需要用到错误处理
perror的例子
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("aaa", O_RDWR);
if (fd < 0)
{
printf("errno:%d\n",errno);
perror("int mainerr");
printf("strerror%d\n", strerror(errno));
}
printf("fd=%d\n", fd);
return 0;
}
非阻塞读终端
#include
#include
#include
#include
#include
#define MSG_TRY"try again\n"
#define MSG_TIMEOUT"timeout\n"
int main(void)
{
char buf[10];
int fd, n, i;
fd = open("/dev/tty",O_RDONLY | O_NONBLOCK);
if(fd < 0)
{
perror("open/dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10);
if(n < 0)
{
if(errno == EAGAIN)
{
sleep(1);
write(STDOUT_FILENO,MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read/dev/tty");
exit(1);
}
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
非阻塞读终端和等待延时的例子
#include
#include
#include
#include
#include
#define MSG_TRY"try again\n"
#define MSG_TIMEOUT"timeout\n"
int main(void)
{
char buf[10];
int fd, n, i;
fd = open("/dev/tty",O_RDONLY | O_NONBLOCK);
if(fd < 0)
{
perror("open/dev/tty");
exit(1);
}
for(i = 0; i < 5; i++)
{
n = read(fd, buf, 10);
if(n >= 0)
{
break;
}
if(errno != EAGAIN)
{
perror("read/dev/tty");
exit(1);
}
sleep(1);
write(STDOUT_FILENO, MSG_TRY,strlen(MSG_TRY));
}
if(i == 5)
{
write(STDOUT_FILENO,MSG_TIMEOUT, strlen(MSG_TIMEOUT));
}
else
{
write(STDOUT_FILENO, buf, n);
}
close(fd);
return 0;
}
每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置后移多少个字节。但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。
off_t lseek(int fd, off_t offset, int whence);
参数offset和whence的含义和fseek函数完全相同。和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,中间空洞的部分读出来都是0。
若lseek成功执行,则返回新的偏移量,因此可这样打开文件的当前偏移量。
使用lseek拓展一个文件,注意需要有至少一次写操作,获取一个文件的大小。
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd = open("abc", O_RDWR);
if(fd < 0)
{
perror("open abcerr");
exit(-1);
}
lseek(fd, 0x10000, SEEK_SET);
write(fd, "a", 1);
close(fd);
fd = open("block.c", O_RDWR);
if(fd < 0)
{
perror("open block.cerr");
exit(-1);
}
printf("block.c size = %d\n",lseek(fd, 0, SEEK_END));
close(fd);
return 0;
}
获取或设置文件的访问控制属性。使用fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志,而不必重新open文件。
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
例子
#include
#include
#include
#include
#include
#define MSG_TRY"try again\n"
int main(void)
{
char buf[10];
int n;
int flags;
flags = fcntl(STDIN_FILENO, F_GETFL);
flags |= O_NONBLOCK;
if(fcntl(STDIN_FILENO, F_SETFL, flags)== -1)
{
perror("fcntl");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if(n < 0)
{
if(errno == EAGAIN)
{
sleep(1);
write(STDOUT_FILENO,MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read stdin");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是不能用read/write读写的,称为out-of-band数据。read/write读写的数据是in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。
#include
int ioctl(int d, int request, …);
d是某个设备的文件描述符。request是ioctl的命令,可变参数取决于request,通常是一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于request。
获取终端窗口大小的例子
#include
#include
#include
#include
int main(void)
{
struct winsize size;
if(isatty(STDOUT_FILENO) == 0)
{
exit(1);
}
if(ioctl(STDOUT_FILENO, TIOCGWINSZ,&size) < 0)
{
perror("ioctl TIOCGWINSZerror");
exit(1);
}
printf("%d rows, %dcolumns\n", size.ws_row, size.ws_col);
return 0;:
}
获取屏幕大小的例子
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd;
struct fb_var_screeninfo fb_info;
fd = open("/dev/fb0", O_RDONLY);
if(fd < 0)
{
perror("open/dev/fb0");
exit(1);
}
ioctl(fd, FBIOGET_VSCREENINFO,&fb_info);
printf("r=%d c=%d\n",fb_info.xres, fb_info.yres);
close(fd);
return 0;
}
文件系统中存储的最大单位是块block,一个块究竟多大是在格式化时确定的,例如mke2fs的-b选项可以设定块大小为1024、2048或4096字节。启动块boot block的大小是确定的,一般是1KB,启动块是由PC标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是ext2文件系统系统的开始,ext2文件系统将整个分区划分为若干个同样大小的块组block group,每个块组都由以下部分组成。
超级块super block描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次mount的时间等等。超级块在每个块组的开头都有一份拷贝。
块组描述符表GDT,group descriptor table,由很多块组描述组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符group descriptor存储一个块组的描述信息,例如在这个块组中从哪里开始是inode块,从哪里开始是数据块,空闲的inode块和数据块还有多少等等。和超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是重要的,一旦超级块意外损坏就会失去整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数据,因此有多份拷贝。通常内核只用到第0个块组中的拷贝,当执行e2fsck检查文件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其他块组,这样当第0个块组的开头意外损坏时就可以用其他拷贝来恢复,从而减少损失。
块位图block bitmap,一个块组中的块是这样利用的,数据块存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块,超级块、块描述符表、块位图、inode位图、inode表这几个部分存储该块组的描述信息。块位图用来描述整个块组中哪些图、inode位图、inode表这几部分存储该块组的描述信息。块位图用来描述哪些块已经用哪些块空闲,它本身占一个块,其中的每个bit代表本块组中的一个块,这个bit为1表示该块已用,这个bit为0表示该块空闲可用。
在格式化一个分区时究竟会划出多少个块组,主要的限制在于块位图本身必须只占一个块。用mke2fs格式化时默认块大小是1024字节,可以用-b参数指定块大小,现在设块大小指定为b字节,那么一个块大小可以有8b个bit,这样大小的一个块位图就可以表示8b个块的占用情况,因此一个块组最多可以有8b个块,如果整个分区有s个块,那么就可以有s/(8b)个块组。格式化时可以用-g参数指定一个块组有多少个块,但是通常不需要手动指定,mke2fs工具会计算出最优的数值。
inode位图,inode bitmap,和块位图类似,本身占一个块,其中每个bit表示一个inode是否空闲可用。
inode表,inode table,一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等)、权限、文件大小、创建/修改/访问时间等,也就是ls -l命令看到的那些信息,这些信息存在inode中而不是数据中。每个文件都有一个inode,一个块组中的所有inode组成了inode表。
inode表占多少个块在格式化时就要决定并写入块组描述符中,mke2fs格式化工具的默认策略是一个块组有多少个8KB就分配多少个inode,也就是,如果平均每个文件的大小是8KB,当分区存满的时候inode表会得到比较充分的利用,数据块也不浪费,如果这个区存的都是很大的文件,则数据块用完的时候inode会有一些浪费,如果这个分区存的都是很小的文件,则有可能数据坏还没有用完inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用mke2fs的-i参数手动指定每多少个字节分配一个inode。
数据块data block,根据不同的文件类型有以下几种情况:
对于常规文件,文件的数据存储在数据块中。
对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,ls -l命令看到的其他信息都保存在该文件的inode中。注意这个概念,目录也是一种文件,是一种特殊类型的文件。
对于符号链接,如果目录路径名较短则直接保存在inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。
设备文件 FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号保存在inode中。
目录中记录项文件类型
编码 文件类型
0 unknown
1 regular file
2 directory
3 character device
4 block device
5 named pipe
6 socket
7 symbolic link
数据块寻址描述
比如索引项blocks[12]指向两级的间接寻址块,最多可表示(b/4)2+b/4+12个数据块,对于1K的块大小最大可表示64.26MB的文件。索引项blocks[13]指向三级的间接寻址,最多可表示(b/4)3+(b/4)2+b/4+12个数据块,对于1K的块大小最大可表示16.06GB的文件。
这种寻址方式对于访问不超过12个数据块的小文件是非常快的,访问文件中的任意数据只需要两次读盘操作,一次读inode,一次读数据块。访问大文件中的数据则需要最多五次读盘操作,inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际,磁盘中的inode和数据块往往已经被内核缓存了,读大文件的效率也不会太低。
以下是文件以及目录相关的函数
stat既有命令也有函数,用来获取文件inode里主要信息,stat跟踪符号链接,lstat不跟踪符号链接。
#include
#include
#include
int stat(const char *path, struct stat*buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat*buf);
例子
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
struct stat s_buf;
if(argc < 2)
{
printf("./app filename\n");
exit(1);
}
if(stat(argv[1], &s_buf) < 0)
{
perror("stat");
exit(1);
}
printf("%s\t %d\n", argv[1],s_buf.st_size);
return 0;
}
按实际用户ID和实际组ID测试,跟踪符号链接。
#include
int access(const char *pathname, int mode);
参数mode
R_OK 是否有读权限
W_OK 是否有些权限
X_OK 是否有执行权限
F_OK 测试一个文件是否存在
例子
#include
#include
int main(void)
{
if(access("abc", F_OK) <0)
{
perror("abc err:");
exit(1);
}
printf("abc is ok\n");
return 0;
}
#include
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
例子
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int mode;
if(argc < 3)
{
printf("./mychmod modefilename\n");
exit(1);
}
mode = atoi(argv[1]);
chmod(argv[2], mode);
return 0;
}
#include
int chown(const char *path, uid_t owner,gid_t group);
int fchown(int fd, uid_t owner, gid_tgroup);
int lchown(const char *path, uid_t owner,gid_t group);
使用chown必须具有root权限。
创建一个硬链接。当rm删除文件时,只是删除了目录下的记录项和把inode硬链接计数减1,当硬链接计数减为0时,才会真的删除文件。
#include
int link(const char *oldpath, const char*newpath);
注意:
硬链接通常要求位于同一文件系统中,POSIX允许跨文件系统;
符号链接没有文件系统限制;
通常不允许创建目录的硬链接,某些unix系统下超级用户可以创建目录的硬链接;
创建目录项以及增加硬链接计数应当是一个原子操作。
创建软链接
#include
int symlink(const char *oldpath, const char*newpath);
获取符号链接指向的文件名。
#include
ssize_t readlink(const char *path, char*buf, size_t bufsiz);
如果是符号链接,则删除符号链接。如果是硬链接,硬链接数减1,当减为0时,释放数据块和inode。如果文件硬链接数为0,但有进程已打开该文件,并持有文件描述符,则等进程关闭该文件时,kernel才真正去删除该文件。利用该特性创建临时文件,先open或creat创建一个文件,马上unlink此文件。
#include
int rename(const char *oldpath, const char*newpath);
#include
int chdir(const char *path);
int fchdir(int fd);
#include
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);
char *get_current_dir_name(void);
例子
#include
#include
#include
#define SIZE 4096
int main(void)
{
char buf[SIZE];
chdir("/home/haha/");
printf("pwd:%s\n",getcwd(buf, sizeof(buf)));
return 0;
}
#include
long fpathconf(int fd, int name);
long pathconf(char *path, int name);
例子
#include
#include
#include
int main(void)
{
printf("%ld\n",fpathconf(STDOUT_FILENO, _PC_NAME_MAX));
printf("%ld\n",pathconf("abc", _PC_NAME_MAX));
return 0;
}
#include
#include
int mkdir(const char *pathname, mode_tmode);
#include
int rmdir(const char *pathname);
#include
#include
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
#include
struct dirent *readdir(DIR *dirp);
把目录指针恢复到目录的起始位置。
#include
#include
void rewinddir(DIR *dirp);
#include
long telldir(DIR *dirp);
#include
void seekdir(DIR *dirp, long offset);
#include
#include
int closedir(DIR *dirp);
#include
#include
#include
#define MAX_PATH 1024
void dirwalk(char*dir, void (*fcn)(char *))
{
char name[MAX_PATH];
struct dirent *dp;
DIR *dfd;
if((dfd = opendir(dir)) == NULL)
{
fprintf(stderr,"dirwalk:can't open %s\n", dir);
return;
}
while((dp = readdir(dfd)) != NULL)
{
if(strcmp(dp->d_name,".") == 0 || strcmp(dp->d_name, "..") == 0)
{
continue;
}
if(strlen(dir) +strlen(dp->d_name) + 2 > sizeof(name))
{
fprintf(stderr,"dirwalk: name %s %s too long\n", dir, dp->d_name);
}
else
{
sprintf(name,"%s/%s", dir, dp->d_name);
(*fcn)(name);
}
}
closedir(dfd);
}
void fsize(char*name)
{
struct stat stbuf;
if(stat(name, &stbuf) == -1)
{
fprintf(stderr, "fsize:can't access %s\n", name);
return;
}
if((stbuf.st_mode & S_IFMT) ==S_IFDIR)
{
dirwalk(name, fsize);
}
printf("%8ld %s\n",stbuf.st_size, name);
}
int main(int argc,char **argv)
{
if(argc == 1)
{
fsize(".");
}
else
{
while(--argc > 0)
{
fsize(*++argv);
}
}
return 0;
}
linux支持多种文件系统格式,如ext2,ext3,reiserfs,fat,ntfs,iso9660等,不同的磁盘分区、光盘或其他存储设备都有不同的文件系统格式,然而这些文件系统都可以mount到某个目录下。linux内核在各种不同的文件系统格式上做了一个抽象层,使得文件、目录、读写访问等概念成为抽象层的概念,因此各种文件系统看起来都一样,这个抽象层称为虚拟文件系统virtual filesystem。
#include
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup和dup2都可用来复制一个现存的文件描述符,使两个文件描述符指向同一个file结构体。如果两个文件描述符指向同一个file结构体,file status flag和读写位置只保存一份在fiel结构体中,并且file结构体的引用计数是2。如果两次open同一文件得到两个文件描述符,则每个描述符对应一个不同的file结构体,可以有不同的file status flag和读写位置。
例子
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd, save_fd;
char msg[] = "This istest\n";
fd = open("abc", O_RDWR |O_CREAT, S_IRUSR | S_IWUSR);
if(fd < 0)
{
perror("open");
exit(1);
}
save_fd = dup(STDOUT_FILENO);
dup2(fd, STDOUT_FILENO);
close(fd);
write(STDOUT_FILENO, msg, strlen(msg));
dup2(save_fd, STDOUT_FILENO);
write(STDOUT_FILENO, msg, strlen(msg));
close(save_fd);
return 0;
}
每个进程都有一个进程控制块PCB来维护进程相关的信息,linux内核的进程控制块是task_struct结构体。进程相关的信息有:
进程id,系统中每个进程有唯一的id,在c语言中用pid_t类型表示,是一个非负整数。
进程的状态,有运行、挂起、停止、僵尸等状态。
进程切换时需要保存和恢复的一些cpu寄存器。
描述虚拟地址终端的信息。
当前工作目录(current working directory)
umask掩码。
文件描述符表,包含很多指向file结构体的指针。
和信号相关的信息。
用户id和组id。
控制终端、session和进程组。
进程可以使用的资源上限(resource limit)。
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
#include
int main(void)
{
extern char **environ;
int i;
for(i = 0; environ[i] != NULL; i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
由于父进程在调用fork创建子进程时会把自己的环境变量表也复制给子进程,所以a.out打印的环境变量和shell进程的环境变量是相同的。
按照惯例,环境变量字符串都是name=value的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
PATH 可执行文件的搜索路径。PATH环境变量的值可以包含多个目录,用:隔开。在shell中用echo命令可以查看这个环境变量的值,echo $PATH
SHELL 当前shell,值通常是/bin/bash。
TERM 当前终端类型,在图形界面下的值通常是xterm,终端类型决定一写程序的输出显示方式,比如图像界面终端可以显示汉字,而字符终端一般不行。
LANG 语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME 当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
用environ指针可以查看所有环境变量字符串,但是不够方便,若果给出name要在环境变量表中查找对应的value,可以使用getenv函数。
#include
char *getenv(const char *name);
getenv的返回值是指向value的指针,若未找到则为 NULL。
修改环境变量可以用以下函数
#include
int setenv(const char *name, const char*value, int overwrite);
int unsetenv(const char *name);
setenv和unsetenv函数成功则返回为0,若出错则返回非0。
setenv将环境变量name的值设为value。如果已经存在环境变量name,那么
若rewrite非0,则覆盖原来的定义;
若rewrite为0,则不覆盖原来的定义,也不返回错误。
unsetenv删除name的定义。即使name没有定义也不返回错误。
修改进程资源限制,软限制可以改,最大值不能超过硬限制,硬限制只有root用户可以修改。
#include
#include
int getrlimit(int resource, struct rlimit*rlim);
int setrlimit(int resource, const structrlimit *rlim);
查看进程资源限制
cat /proc/self/limits
ulimits -a
进程的就绪、运行、睡眠和停止状态之间的转换。
操作系统中操作(创建、销毁、回收)进程的最原始的语言。
子进程调用父进程的0到3g空间和父进程内核中的PCB,但id号不同。fork调用一次返回两次。
#include
pid_t fork(void);
父进程中返回子进程ID。子进程中返回0。读时共享,写时复制。
进程相关函数
获取进程id和父进程id
#include
#include
pid_t getpid(void);
pid_t getppid(void);
获取实际用户和有效用户id
#include
#include
uid_t getuid(void);
uid_t geteuid(void);
获取实际用户组id和有效用户组id
#include
#include
gid_t getgid(void);
gid_t getegid(void);
例子
#include
#include
#include
#include
int main()
{
pid_t pid;
int n = 10;
pid = fork();
if(pid > 0)
{
while(1)
{
printf("parent%d\n", n++);
printf("myid:%d\n", getpid());
printf("my parentpid:%d\n", getppid());
sleep(1);
}
}
else if(pid == 0)
{
while(1)
{
printf("child%d\n", n++);
printf("myid:%d\n", getpid());
printf("my parentpid:%d\n", getppid());
sleep(3);
}
}
else
{
perror("fork");
exit(1);
}
return 0;
}
早期,用于fork后马上调用exec函数。父子进程,共用同一地址空间,子进程如果没有马上exec而是修改了父进程量值,此修改会在父进程中生效。设计初衷,提高系统效率,减少不必要的开销。现在fork已经具备读时共享写时复制机制,vfork逐渐废弃。
用fork创建子进程后执行的是和父进程相同的程序。子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,用户控件代码和数据完全被新程序替代,从新程序的启动例程开始执行。调用exec开启新进程,所以调用exec前后该进程的id并未改变。
#include
extern char **environ;
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 *constargv[]);
int execvp(const char *file, char *constargv[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果不成功则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
对于带字母p的函数,如果参数中包含/,则将其视为路径名。
否则视为不带路径的程序名,在path环境变量的目录列表中搜索这个程序。
execlp(“ls”, “ls”, “-l”, NULL);
带有字母l表示list的exec函数要求将新程序的每个命令行参数都当作参数传给它,命令行参数的个数是可变的,因此函数原型中有…,…中的最后一个可变参数NULL,起sentinel的作用。
对于带有字母v表示vector的函数,则应该先构造各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个参数是NULL,就像main函数的argv参数或者环境变量表一样。
char *argvv[] = { “ls”, “-l”, NULL};
execvl(“/bin/ls”, argvv);
对于以e表示environment结尾的exec函数,可以把一份新的环境变量指向当前环境变量表,使用新的环境变量。其他exec函数仍然使用当前的环境变量表执行新程序。
例子
#include
#include
int main(void)
{
printf("hello\n");
execl("/bin/ls","ls", "-l", NULL);
printf("linux\n");
return 0;
}
事实上,只有execve是真正的系统调用,其他五个函数最终都调用execve,所以execve在man手册第2节,其他函数在man手册第3节。
fork结合execl使用的例子
#include
#include
#include
#include
int main(void)
{
pid_t pid;
pid = fork();
if(pid == 0)
{
execl("/usr/bin/firebox", "firebox","wwww.baidu.com", NULL);
}
else if(pid > 0)
{
while(1)
{
printf("parent\n");
sleep(1);
}
}
else
{
perror("fork");
exit(1);
}
return 0;
}
将一个文件中的小写以大写的形式输出显示的例子,注意不是将文件中的小写更新为大写,原文件不变的。
编写小写转大写的upper.c
#include
int main(void)
{
int ch;
while((ch = getchar()) != EOF)
{
putchar(toupper(ch));
}
return 0;
}
将原文件指向标准输入,并调用小写转大写输出的函数
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int fd;
if(argc != 2)
{
fputs("请执行:wrapper filename\n",stderr);
exit(1);
}
fd = open(argv[1], O_RDONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
dup2(fd, STDIN_FILENO);
close(fd);
execl("./upper","upper", NULL);
perror("exec ./upper");
exit(1);
}
#include
#include
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, intoptions);
int waitid(idtype_t idtype, id_t id,siginfo_t *infop, int options);
关于僵尸进程和孤儿进程
如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这样的进程称为僵尸zombie进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了,为了观察到僵尸进程,自定义一个不正常的程序,父进程fork出子进程,子进程终止,而父进程既不终止也不调用wait清理子进程。
孤儿进程,父进程先于子进程结束,则子进程成为孤儿进程。子进程的父进程成为1号进程init进程,称为init进程领养孤儿进程。
僵尸进程和调用wait清理僵尸进程的例子
#include
#include
#include
#include
#include
int main()
{
pid_t pid, pid_c;
int n = 10;
pid = fork();
if(pid > 0)
{
while(1)
{
printf("parent%d\n", n++);
printf("myid:%d\n", getpid());
printf("my parentpid:%d\n", getppid());
pid_c = wait(NULL);
printf("wait forchild %d\n", pid_c);
sleep(1);
}
}
else if(pid == 0)
{
printf("my id:%d\n",getpid());
sleep(10);
}
else
{
perror("fork");
exit(1);
}
return 0;
}
若调用成功则返回清理掉的子进程id,若调用出错则返回-1。
wait和waitpid的区别
如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时,如果在options参数中指定WHOHANG可以使父进程不阻塞而立即返回0。
wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
waitpid中参数pid的值
< -1 回收指定进程组内的任意子进程
-1 回收任意子进程
0 回收和当前调用waitpid一个组的所有子进程
>0 回收指定id的子进程
孤儿进程的例子
#include
#include
#include
int main(void)
{
pid_t pid;
pid = fork();
if(pid > 0)
{
printf("this isparent\n");
exit(0);
}
else if(pid == 0)
{
while(1)
{
printf("this is%d, its parent is %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
perror("fork");
exit(-1);
}
return 0;
}
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信IPC,inter process communication。
管道是一种最基本的IPC机制,由pipe函数创建。
#include
int pipe(int pipefd[2]);
#define _GNU_SOURCE
#include
int pipe2(int pipefd[2], int flags);
管道作用于有血缘关系的进程之间,通过fork来传递。
调用pipe函数时在内核中开辟一块缓冲区(管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端。管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
实现过程
父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
#include
#include
#include
#include
int main(void)
{
int fd[2];
char str[1024] = "nihaolinux";
char buf[1024];
pid_t pid;
if(pipe(fd) < 0)
{
perror("pipe");
exit(1);
}
pid = fork();
if(pid > 0)
{
close(fd[0]);
write(fd[1], str, strlen(str));
wait(NULL);
}
else if(pid == 0)
{
int len;
close(fd[1]);
len = read(fd[0], buf, sizeof(buf));
//sprintf(str, "child%s", buf);
write(STDOUT_FILENO, buf, len);
}
else
{
perror("fork");
exit(1);
}
return 0;
}
管道的四种特殊情况
如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
fpathconf(int fd, int name)测试管道缓冲区大小,_PC_PIPE_BUF。
#include
#include
int main(void)
{
int fd[2];
pipe(fd);
printf("pipe buf %ld\n",fpathconf(fd[0], _PC_PIPE_BUF));
printf("%ld\n",fpathconf(STDOUT_FILENO, _PC_NAME_MAX));
printf("%ld\n",pathconf("abc", _PC_NAME_MAX));
return 0;
}
创建一个有名管道,解决无血缘关系的进程通信,fifo。
创建管道的命令mkfifo myfifo
查看ls -l myfifo
例子
首先手动创建一个myfifo的管道
一般通过函数创建fifo管道
#include
#include
int mkfifo(const char *pathname, mode_tmode);
编写fifo_w.c向管道写的程序
#include
#include
#include
#include
#include
#include
#include
void sys_err(char*str, int exitno)
{
perror(str);
exit(exitno);
}
int main(int argc,char *argv[])
{
int fd;
char buf[1024] = "hellolinux\n";
if(argc < 2)
{
printf("./fiforappfifoname\n");
exit(1);
}
fd = open(argv[1], O_WRONLY);
if(fd < 0)
{
sys_err("open", 1);
}
write(fd, buf, strlen(buf));
close(fd);
return 0;
}
编写fifo_r.c读管道的程序
#include
#include
#include
#include
#include
#include
void sys_err(char*str, int exitno)
{
perror(str);
exit(exitno);
}
int main(int argc,char *argv[])
{
int fd;
int len;
char buf[1024];
if(argc < 2)
{
printf("./fiforappfifoname\n");
exit(1);
}
fd = open(argv[1], O_RDONLY);
if(fd < 0)
{
sys_err("open", 1);
}
len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd);
return 0;
}
mmap/munmap
mmap可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针来做而不需要read/write函数。
#include
void *mmap(void *addr, size_t length, intprot, int flags, int fd, off_t offset);
void *mmap64(void *addr, size_t length, intprot, int flags, int fd, off64_toffset);
int munmap(void *addr, size_t length);
如果addr参数为NULL,内核会自己在进程地址空间中选择合适的地址建立映射。如果addr不是NULL,则给内核一个提示,应该从什么地址开始映射,内核会选择addr之上的某个 合适的地址开始映射。建立映射后,真正的映射首地址通过返回值可以得到。len参数是需要映射的那一部分文件的长度。off参数是从文件的什么位置开始映射,必须是页大小的整数倍(在32位体系统结构上通常是4K)。filedes是代表该文件的描述符。
例子
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int fd;
int *p;
int len;
fd = open("abc", O_RDWR);
if(fd < 0)
{
perror("open");
exit(1);
}
len = lseek(fd, 0, SEEK_END);
p = mmap(NULL,len, PROT_READ |PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED)
{
perror("mmap");
exit(1);
}
close(fd);
p[0] = 0x30313233;
munmap(p, len);
return 0;
}
用于进程间通信时,一般设计成结构体,来传输通信的数据。
进程间通信的文件,应该设计成临时文件。
当报总线错误时,先查看共享文件是否有存储空间。
编写写进程
#include
#include
#include
#include
#include
#include
#include
#define MAPLEN 0x1000
struct PERSON
{
int id;
char name[20];
char sex;
};
void sys_err(char*str, int exitno)
{
perror(str);
exit(exitno);
}
int main(int argc,char *argv[])
{
struct PERSON *mm;
int fd, i = 0;
if(argc < 2)
{
printf("./process_mmapappfilename\n");
exit(1);
}
fd = open(argv[1], O_RDWR | O_CREAT,0777);
if(fd < 0)
{
sys_err("open", 1);
}
if(lseek(fd, MAPLEN - 1, SEEK_SET) <0)
{
sys_err("lseek", 3);
}
if(write(fd, "\0", 1) < 0)
{
sys_err("write", 4);
}
mm = mmap(NULL, MAPLEN, PROT_READ |PROT_WRITE, MAP_SHARED, fd, 0);
if(mm == MAP_FAILED)
{
sys_err("mmap", 2);
}
close(fd);
while(1)
{
mm->id = i;
sprintf(mm->name,"hahx-%d", i);
if(i % 2 == 0)
{
mm->sex = 'm';
}
else
{
mm->sex = 'w';
}
i++;
sleep(1);
}
munmap(mm, MAPLEN);
return 0;
}
编写读进程
#include
#include
#include
#include
#include
#include
#include
#define MAPLEN 0x1000
struct PERSON
{
int id;
char name[20];
char sex;
};
void sys_err(char*str, int exitno)
{
perror(str);
exit(exitno);
}
int main(int argc,char *argv[])
{
struct PERSON *mm;
int fd, i = 0;
if(argc < 2)
{
printf("./process_mmapappfilename\n");
exit(1);
}
fd = open(argv[1], O_RDWR);
if(fd < 0)
{
sys_err("open", 1);
}
mm = mmap(NULL, MAPLEN, PROT_READ |PROT_WRITE, MAP_SHARED, fd, 0);
if(mm == MAP_FAILED)
{
sys_err("mmap", 2);
}
close(fd);
unlink(argv[1]);
while(1)
{
printf("id %d,name %s,sex%c\n", mm->id, mm->name, mm->sex);
sleep(1);
}
munmap(mm, MAPLEN);
return 0;
}
ls -l /var /run/
srw-rw-rw-. 1 root root 0 Jun 22 18:46 acpid.socket
这种s类的文件socket类型的类似fifo。
kill -l查看linux系统所有的信号。
man 7 signal可以查看信号机制。
信号产生种类:
终端特殊按键
ctrl+c SIGINT
ctrl+z SIGTSTP
ctrl+\ SIGQUIT
硬件异常,比如除0操作,访问非法内存等。
#include
int main(void)
{
int a = 0;
char *str = "hello";
*str = 'H';
printf("%d\n", 3/a);
while(1);
return 0;
}
kill函数或kill命令
#include
#include
int kill(pid_t pid, int sig);
例子
#include
#include
#include
#include
int main(int argc,char *argv[])
{
if(argc < 3)
{
printf("./mykill signalpid\n");
}
if(kill((pid_t)atoi(argv[2]),atoi(argv[1])) < 0)
{
perror("kill");
exit(1);
}
return 0;
}
类似的函数
int raise(int sig);
void abort(void);
某种软件条件发生
定时器alarm到时,每个进程只有一个定时器
#include
unsigned int alarm(unsigned int seconds);
例子
#include
#include
int main(void)
{
int counter;
alarm(1);
for(counter = 0;1;counter++)
{
printf("counter=%d ",counter);
}
return 0;
}
信号产生的原因
(1) SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
(2)SIGINT:当用户按下了组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止里程。
(3)SIGQUIT:当用户按下组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
(4)SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
(5)SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程并产生core文件。
(6 ) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
(7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
(8)SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
(9)SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
(10)SIGUSE1:用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
(11)SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
(12)SIGUSR2:这是另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
(13)SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
(14) SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
(15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
(16)SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
(17)SIGCONT:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为终止进程
(18)SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
(19)SIGTSTP:停止进程的运行。按下组合键时发出这个信号。默认动作为暂停进程。
(21)SIGTTOU:该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
(22)SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
(23)SIGXFSZ:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
(24)SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
(25)SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
(26)SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
(27)SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
(28)SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
(29)SIGPWR:关机。默认动作为终止进程。
(30)SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
(31)SIGRTMIN~(64)SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程
manpage里信号3种处理方式
SIG_IGN
SIG_DEL
a signal handing function
进程处理信号的行为
第一,默认处理动作,有五种情况
term 终止当前进程
core 终止当前进程并且core dump,用于死后验尸
gcc -g test.c
ulimit -c 1024
gdb test.o core #进程死之前的内存情况
ign 忽略掉该信号
stop 停止当前进程
cont 继续执行先前停止的进程
第二,忽略
第三,捕捉(用户自定义信号处理函数)
sigset_t为信号集,可sizeof(sigset_t)查看
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, intsignum);
如果在进程解除对某信号的阻塞之前这种信号产生过多次。 POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t 来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(SignalMask),这里的“屏蔽”应该理解 为阻塞而不是忽略。
调用函数sigprocmask可以读取或更改进程的信号屏蔽字。
#include
int sigprocmask(int how, const sigset_t*set, sigset_t *oldset);
成功返回0,出错返回-1。
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,how参数的可选值为:
SIG_BLOCK set包含了希望添加到当前信号屏蔽字的信号,相当于mask=mask | set。
SIG_UNBLOCK set包含了希望从当前屏蔽字中解除阻塞的信号,相当于mask=mask & ~set。
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set。
如果调用siprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending
#include
int sigpending(sigset_t *set);
sigpending读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
例子
#include
#include
voidprintsigset(const sigset_t *set)
{
int i;
for(i = 1; i < 32; i++)
{
if(sigismember(set, i) == 1)
{
putchar('1');
}
else
{
putchar('0');
}
}
puts(" ");
}
int main(void)
{
sigset_t s,p;
sigemptyset(&s);
sigaddset(&s, SIGINT);
sigprocmask(SIG_BLOCK, &s, NULL);
while(1)
{
sigpending(&p);
printsigset(&p);
sleep(1);
}
return 0;
}
sigaction
#include
int sigaction(int signum, const structsigaction *act, struct sigaction*oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler,早起的捕捉函数。
sa_sigaction,新添加的捕捉函数,可以传参,和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数。
sa_mask,在执行捕捉函数时,设置阻塞其他信号,sa_mask|进程阻塞信号集,退出捕捉函数后,还原回原有的阻塞信号集。
sa_flags,SA_SIGINFO或者0。
sa_restorer,保留,已过时。
#include
#include
void do_sig(int num)
{
int n = 5;
printf("do_sig\n");
while(n--)
{
printf("num = %d\n", num);
sleep(1);
}
}
int main(void)
{
struct sigaction act;
act.sa_handler = do_sig;
//act.sa_handler = SIG_DFL;
//act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
while(1)
{
printf("*********\n");
sleep(1);
}
return 0;
}
利用SIGUSR1好SIGUSR2实现父子进程同步输出
typedef void(*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)
int system(const char *command) #集合fork,exec,wait一体。
例子
#include
#include
#include
void do_sig(int n)
{
printf("hello\n");
}
int main(void)
{
signal(SIGINT, do_sig);
while(1)
{
printf("**************\n");
sleep(1);
}
return 0;
}
可重新进入。
不含全局变量和静态变量是可重入函数的一个要素。
可重入函数介绍见man 7 signal
在信号捕捉函数里应使用可重入函数。
在信号捕捉函数里禁止调用不可重入函数。
例子
strtok就是一个不可重入函数,因为strtok内部维护了一个内部静态指针,保存上一次切割到的位置,如果信号的捕捉函数中也去调用strtok函数,则会造成切割字符串混乱,应用strtok_r版本,r表示可重入。
#include
#include
int main(void)
{
char buf[] = "hello linux hellocpp hello haha";
char *save = buf, *p;
while((p = strtok_r(save, "", &save)) != NULL)
{
printf("%s\n", p);
}
return 0;
}
时序竞态,两个函数的竞争状态。
相关函数
int pause(void)
使调用进程挂起,直到有信号递达,如果递达信号是忽略,则继续挂起。
int sigsuspend(const sigset_t *mask)
以通过指定mask来临时解除对某个信号的屏蔽,然后挂起等待,当被信号唤醒sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。
#include
#include
#include
void do_sig(int n)
{
}
int mysleep2(int n)
{
signal(SIGALRM, do_sig);
alarm(n);
pause();
}
unsigned intmysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
/* set our handler,save previous information */
newact.sa_handler = do_sig;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact,&oldact);
/* block SIGALRM andsave current signal mask */
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask,&oldmask);
alarm(nsecs);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM); /*make sure SIGALRM isn't blocked */
sigsuspend(&suspmask); /* wait forany signal to be caught */
/* some signal hasbeen caught, SIGALRM is now blocked */
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL);/* reset previous action */
/* reset signal mask,which unblocks SIGALRM */
sigprocmask(SIG_SETMASK, &oldmask,NULL);
return(unslept);
}
int main2(void)
{
struct sigaction act;
act.sa_handler = do_sig;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);
pause();
printf("hellolinux");
return 0;
}
int main(void)
{
while(1)
{
mysleep(2);
printf("two secondspassed\n");
}
return 0;
}
异步I/O全局变量
为避免异步I/O使用的类型,sig_atomic_t,平台下的原子类型。
volatile,防止编译器开启优化选项时,优化对内容的读写。
当子进程终止时,子进程接收到SIGSTOP信号停止时,子进程处在停止态,接收到SIGCHLD后唤醒时。
status处理方式
#include
#include
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, intoptions);
int waitid(idtype_t idtype, id_t id,siginfo_t *infop, int options);
例子
#include
#include
#include
#include
#include
#include
#include
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(intsigno)
{
/*
while(!(waitpid(0, NULL, WNOHANG) ==-1))
;
*/
int status;
pid_t pid;
while((pid = waitpid(0, &status,WNOHANG)) > 0)
{
if(WIFEXITED(status))
{
printf("child %dexit %d\n", pid, WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
{
printf("child %d cancel signal%d\n", pid, WTERMSIG(status));
}
}
}
int main(void)
{
pid_t pid;
int i;
for(i = 0; i < 10; i++)
{
if((pid = fork()) == 0)
{
break;
}
else if(pid < 0)
{
sys_err("fork");
}
}
if(pid == 0)
{
int n = 8;
while(n--)
{
printf("child id%d\n", getpid());
sleep(1);
}
return i;
}
else if(pid > 0)
{
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act,NULL);
while(1)
{
printf("parent id%d\n", getpid());
sleep(1);
}
}
return 0;
}
相关函数
#include
int sigqueue(pid_t pid, int sig, constunion sigval value);
void (*sa_sigaction)(int, siginfo_t *, void *)
siginfo_t
{
int si_int;
/* POSIX.1b signal */
void *si_ptr;
/* POSIX.1b signal */
sigval_t si_value;
/* Signal value */
...
}
read阻塞时,信号中断系统调用。
返回部分读到的数据;read调用失败,errno设成EINTER。
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。
每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。
硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下Ctrl-Z,对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。
网络终端
虚拟终端或串口终端的数目是有限的,虚拟终端(字符控制终端)一般就是/dev/tty1∼/dev/tty6六个,串口终端的数目也不超过串口的数目。然而网络终端或图形终端窗口的数目却是不受限制的,这是通过伪终端(Pseudo TTY)实现的。一套伪终端由一个主设备(PTY Master)和一个从设备(PTYSlave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程。从设备和上面介绍的/dev/tty1这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件而是访问主设备。网络终端或图形终端窗口的Shell进程以及它启动的其它进程都会认为自己的控制终端是伪终端从设备,例如/dev/pts/0、/dev/pts/1等。
一个或多个进程的集合,称为进程组。进程组id是一个正整数。获得当前进程组id的函数。
#include
int setpgid(pid_t pid, pid_t pgid);
pid_t getpgid(pid_t pid);
pid_t getpgrp(void); /* POSIX.1 version */
pid_t getpgrp(psid_t pid); /* BSD version */
int setpgrp(void); /* System V version */
int setpgrp(pid_t pid, pid_t pgid); /* BSDversion */
例子
#include
#include
#include
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0)
{
perror("fork");
exit(1);
}
else if (pid == 0)
{
printf("child process PIDis %d\n",getpid());
printf("Group ID is%d\n",getpgrp());
printf("Group ID is%d\n",getpgid(0));
printf("Group ID is%d\n",getpgid(getpid()));
exit(0);
}
sleep(3);
printf("parent process PID is%d\n",getpid());
printf("Group ID is%d\n",getpgrp());
return 0;
}
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止,只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。 一个进程可以为自己或子进程设置进程组ID
默认下,进程是属于shell会话。ps ajx SID查看会话id。
设置会话函数。
#include
pid_t setsid(void);
1.调用进程不能是进程组组长,该进程变成新会话首进程(sessionheader) 。
2.该进程成为一个新进程组的组长进程。
3.需有root权限(ubuntu不需要)
4.新会话丢弃原有的控制终端,该会话没有控制终端。
5.该调用进程是组长进程,则出错返回。
6.建立新会话时,先调用 fork,父进程终止,子进程调用。
pid_t getsid(pid_t pid);
pid为0表示察看当前进程session ID ps ajx命令查看系统中的进程。
参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示 列出与作业控制相关的信息。
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
#include
#include
#include
int main(void)
{
pid_t pid;
if((pid = fork()) < 0)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
printf("child process pidis %d\n", getpid());
printf("group id of childis %d\n", getpgid(0));
printf("session id ofchild is %d\n", getsid(0));
sleep(10);
setsid();
printf("changed:\n");
printf("child process pidis %d\n", getpid());
printf("group id of childis %d\n", getpgid(0));
printf("session id ofchild is %d\n", getsid(0));
sleep(20);
exit(0);
}
return 0;
}
daemon(精灵)进程,是linux中的后台服务进程,生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护进程编程步骤
1. 创建子进程,父进程退出;所有工作在子进程中进行;形式上脱离了控制终端。
2. 在子进程中创建新会话;setsid()函数;使子进程完全独立出来,脱离控制。
3. 改变当前目录为根目录;chdir()函数;防止占用可卸载的文件系统;也可以换成其它路径。
4. 重设文件权限掩码;umask()函数;防止继承的文件创建屏蔽字拒绝某些权限;增加守护进程灵活性。
5. 关闭文件描述符;继承的打开文件不会用到,浪费系统资源,无法卸载。
6. 开始执行守护进程核心工作。
7. 守护进程退出处理。
例子
#include
#include
#include
void daemonize(void)
{
pid_t pid;
if((pid = fork()) < 0)
{
perror("fork");
exit(1);
}
else if(pid != 0)
{
exit(0);
}
setsid();
if(chdir("/") < 0)
{
perror("chdir");
exit(1);
}
umask(0);
close(0);
open("/dev/null", O_RDWR);
dup2(0, 1);
dup2(0, 1);
}
int main(void)
{
daemonize();
while(1);
}
运行这个程序,它变成一个守护进程,不再和当前终端关联。用ps命令看不到,必须运行带x参数的ps命令才能看到。另外还可以看到,用户关闭终端窗口或注销也不会影响守护 进程的运行。
案例
周期性将当前时间记录到/tmp/daemon.log文件。
线程是程序执行的最小单元。与进程的关系,一种轻量级进程,也有PCB,创建线程使用的底层函数和进程一样,都是clone;从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的二级页表是相同的;进程可以蜕变成线程;线程可以认为是寄存器和栈;在linux下,线程是最小的执行单位,进程是最小的分配资源单位。
查看LWP号
ps -Lf pid
ps -eLf
ps -Lw pid 查看pid进程的线程
线程间共享资源的形式:文件描述符表;每种信号的处理方式;当前工作目录;用户id和pid;内存地址空间。
线程间非共享资源,线程id;处理器现场和栈指针;独立的栈空间;errno变量;信号屏蔽字;调度优先级。
线程的优点,提高程序的并发性;开销小,不用重新分配内存;通信和共享数据方便。
线程的缺点,线程不稳定(库函数实现);线程调试比较困难(gdb支持不好);线程无法使用unix经典事件,例如信号。
pthread manpage
查看manpage关于pthread函数man -k pthread
安装pthread相关的manpage
sudo apt-get install manpages-posix manpages-posix-dev
pthread_create
创建线程
#include
int pthread_create(pthread_t *thread, constpthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
pthread_t *thread:传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)。
const pthread_attr_t *attr:线程属性设置,如使用默认属性,则传NULL 。
void *(*start_routine) (void *):函数指针,指向新线程应该加载执行的函数模块。
void *arg:指定线程将要加载调用的那个函数的参数返回值:成功返回0,失败返回错误号。
有的系统函数都是成功返回0,失败返回-1,而错误号保存在全局变量errno中,而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。
pthread_self
获取调用线程tid。
#include
pthread_t pthread_self(void);
例子
#include
#include
void *th_fun(void*arg)
{
int *p = (int *)arg;
printf("thread pid = %x\n",getpid());
printf("thread id = %x\n",(unsigned int)pthread_self());
printf("*arg = %d\n", *p);
while(1);
}
int main(void)
{
pthread_t tid;
int n = 10;
pthread_create(&tid, NULL, th_fun,(void *)&n);
printf("main thread id =%x\n", (unsigned int)pthread_self());
printf("main child thread id =%x\n", (unsigned int)tid);
printf("main pid = %x\n",getpid());
sleep(2);
while(1);
return 0;
}
如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,在main函 数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行。
pthread_exit
调用线程退出函数,注意和exit函数的区别,任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是 用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
#include
void pthread_exit(void *retval);
pthread_join
等价于进程中的wait,阻塞回收线程。
#include
int pthread_join(pthread_t thread, void**retval);
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返 回值。
如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给 pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
pthread_cancel
在进程内某个线程可以取消另一个线程。
#include
int pthread_cancel(pthread_t thread);
被取消的线程,退出值,定义在Linux的pthread库中常数PTHREAD_CANCELED的值是-1。
可以在头文件pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void *) -1)
例子
#include
#include
#include
#include
void *thr_fn1(void*arg)
{
printf("thread 1returning\n");
return (void *)1;
}
void *thr_fn2(void*arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
void *thr_fn3(void*arg)
{
while(1)
{
printf("thread 3writing\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
void *tret;
pthread_create(&tid, NULL, thr_fn1,NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code%d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn2,NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code%d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn3,NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code%d\n", (int)tret);
return 0;
}
pthread_detach
设置线程为分离态。
#include
int pthread_detach(pthread_t thread);
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用 pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
例子
#include
#include
#include
#include
#include
void *thr_fn(void*arg)
{
int n = 10;
while(n--)
{
printf("thread count%d\n", n);
sleep(1);
}
}
int main(void)
{
pthread_t tid;
void *tret;
int err;
pthread_create(&tid, NULL, thr_fn,NULL);
pthread_detach(tid);
while(1)
{
err = pthread_join(tid,&tret);
if(err != 0)
{
fprintf(stderr,"thread %s\n", strerror(err));
}
else
{
fprintf(stderr,"thread exit code %d\n", (int)tret);
}
sleep(1);
}
return 0;
}
pthread_equal
比较两个线程是否相等。
#include
int pthread_equal(pthread_t t1, pthread_tt2);
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1.从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用 exit。
2.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3.线程可以调用pthread_exit终止自己。
同一进程的线程间,pthread_cancel向另一线程发终止信号。系统并不会马上关闭被取消线程,只有在被取消线程下次系统调用时,才会真正结束线程。或调用pthread_testcancel,让内核去检测是否需要取消当前线程。
#include
#include
void *th_fun(void*arg)
{
int *p = (int *)arg;
int i = 0;
printf("thread pid = %x\n",getpid());
printf("thread id = %x\n",(unsigned int)pthread_self());
printf("*arg = %d\n", *p);
while(1)
{
i++;
pthread_testcancel();
}
}
int main(void)
{
pthread_t tid;
int n = 10;
pthread_create(&tid, NULL, th_fun,(void *)&n);
printf("main thread id =%x\n", (unsigned int)pthread_self());
printf("main child thread id =%x\n", (unsigned int)tid);
printf("main pid = %x\n",getpid());
sleep(2);
pthread_cancel(tid);
while(1);
return 0;
}
linux下线程的属性是可以根据实际项目需要,进行设置,默认属性已经可以解决绝大多数开发时遇到的问题。对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
typedef struct
{
int etachstate;//线程的分离状态
int schedpolicy;//线程调度策略
structsched_paramschedparam; //线程的调度参数
intinheritsched; //线程的继承性
int scope; //线程的作用域
size_tguardsize; //线程栈末尾的警戒缓冲区大小
intstackaddr_set; //线程的栈设置
void* stackaddr;//线程栈的位置
size_tstacksize; //线程栈的大小
}pthread_attr_t;
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和 参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省M的堆 栈、与父进程同样级别的优先级。
关于查看支持的最大线程的例子
#include
#include
#include
#include
void *th_fun(void*arg)
{
while(1)
{
sleep(1);
}
}
int main(void)
{
pthread_t tid;
int i = 1, err;
while(1)
{
err = pthread_create(&tid,NULL, th_fun, NULL);
if(err != 0)
{
printf("%s\n", strerror(err));
exit(1);
}
printf("%d\n", i++);
}
return 0;
}
线程属性初始化
#include
int pthread_attr_init(pthread_attr_t*attr);
int pthread_attr_destroy(pthread_attr_t*attr);
线程的分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统 资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
线程分离状态的函数
#include
intpthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
intpthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数 pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
#include
#include
#include
#include
void *th_fun(void*arg)
{
int n = 10;
while(n--)
{
printf("%x %d\n", (int)pthread_self(), n);
sleep(1);
}
return (void *)1;
}
int main(void)
{
pthread_t tid;
pthread_attr_t attr;
int err;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr,th_fun, NULL);
err = pthread_join(tid, NULL);
while(1)
{
if(err != 0)
{
printf("%s\n", strerror(err));
pthread_exit((void*)1);
}
}
return 0;
}
线程的栈地址
POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE 检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE 来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr和pthread_attr_getstackaddr两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)。
#include
intpthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(pthread_attr_t*attr, void **stackaddr);
线程栈的大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。
#include
intpthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
intpthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
上面的栈地址和栈大小函数已经过时,目前使用下面的函数
#include
int pthread_attr_setstack(pthread_attr_t*attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t*attr, void **stackaddr, size_t *stacksize);
例子
#include
#include
#include
#include
#define SIZE 0X100000
int print_ntimes(char*str)
{
sleep(1);
printf("%s", str);
return 0;
}
void *th_fun(void*arg)
{
int n = 3;
while(n--)
{
print_ntimes("hellolinux\n");
}
}
int main(void)
{
pthread_t tid;
int err, detachstate, i = 1;
size_t stacksize;
void *stackaddr;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_getstack(&attr,&stackaddr, &stacksize);
printf("stackaddr = %p\n",stackaddr);
printf("stacksize = %x\n",(int)stacksize);
pthread_attr_getdetachstate(&attr,&detachstate);
if(detachstate ==PTHREAD_CREATE_DETACHED)
{
printf("threaddetached\n");
}
else if(detachstate ==PTHREAD_CREATE_JOINABLE)
{
printf("threadjoined\n");
}
else
{
printf("threadunknown\n");
}
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
while(1)
{
stackaddr = malloc(SIZE);
if(stackaddr == NULL)
{
perror("malloc");
exit(1);
}
stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize);
err = pthread_create(&tid,&attr, th_fun, NULL);
if(err != 0)
{
printf("%s\n", strerror(err));
exit(1);
}
printf("thread %d\n",i);
i++;
}
pthread_attr_destroy(&attr);
return 0;
}
printf("threadjoined\n");
}
else
{
printf("threadunknown\n");
}
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
while(1)
{
stackaddr = malloc(SIZE);
if(stackaddr == NULL)
{
perror("malloc");
exit(1);
}
stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize);
err = pthread_create(&tid,&attr, th_fun, NULL);
if(err != 0)
{
printf("%s\n", strerror(err));
exit(1);
}
printf("thread %d\n",i);
i++;
}
pthread_attr_destroy(&attr);
return 0;
}
查看当前pthread库版本。
getconf GNU_LIBPTHREAD_VERSION
NPTL实现机制POSIX,native posix thread library。
使用线程库时gcc指定 -lpthread。
主线程退出其他线程不退出,主线程应调用pthread_exit;
避免僵尸线程;
join 或者pthread_deatch 或者pthread_create指定分离属性。被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值。
malloc和mmap申请的内存可以被其他线程释放;
如果线程终止时没有释放加锁的互斥量,则该互斥量不能再被使用;
应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit;
信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。
多个线程同时访问共享数据时可能会冲突,这跟信号的可重入性是同样的问题。比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三个指令完成,从内存读变量值到寄存器;寄存器的值加1;将寄存器的值写会内存。假设两个线程在多处理器平台上同时执行这三条指令,则可能出现变量少加了一次的类似的错误。
多线程并发冲突的例子
#include
#include
#include
#define NLOOP 5000
int counter; /*incremented by threads */
void *doit(void *);
int main(int argc,char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL,&doit, NULL);
pthread_create(&tidB, NULL,&doit, NULL);
/* wait for boththreads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void*vptr)
{
int i, val;
for (i = 0; i < NLOOP; i++) {
val = counter;
printf("%x: %d\n",(unsigned int)pthread_self(), val + 1);
counter = val + 1;
}
return NULL;
}
创建两个线程,各自把counter增加5000次,正常情况下最后counter应该等于 10000,但事实上每次运行该程序的结果都不一样,有时候数到5000多,有时候数到6000 多。
线程同步的原因:
共享资源,多个线程都可对共享资源操作;线程操作共享资源的先后顺序不确定;处理器对存储器的操作一般不是原子操作。
mutex操作原语
#include
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_destroy(pthread_mutex_t*mutex);
int pthread_mutex_init(pthread_mutex_t*restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t*mutex);
int pthread_mutex_trylock(pthread_mutex_t*mutex);
int pthread_mutex_unlock(pthread_mutex_t*mutex);
临界区
保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区的选定,临界区的选定应尽可能小,如果选定太大会影响程序的并行处理性能。
互斥量的实例
#include
#include
#include
#define NLOOP 5000
int counter; /*incremented by threads */
pthread_mutex_tcounter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc,char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, doit,NULL);
pthread_create(&tidB, NULL, doit,NULL);
/* wait for both threads to terminate*/
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void*vptr)
{
int i, val;
for (i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex);
val = counter;
printf("%x: %d\n",(unsigned int)pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
比如,1.同一个线程在拥有A锁的情况下再次请求获得A锁 2.线程一拥有A锁,请求获得B锁;线程二拥有B锁,请求获得A锁。死锁导致的结果是无限等待,程序难以继续执行。
特点,读共享,写独占。
#include
pthread_rwlock_t
int pthread_rwlock_destroy(pthread_rwlock_t*rwlock);
int pthread_rwlock_init(pthread_rwlock_t*restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_rdlock(pthread_rwlock_t*rwlock);
intpthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
intpthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t*rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t*rwlock);
例子
#include
#include
int counter;
pthread_rwlock_trwlock;
//3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
void *th_write(void*arg)
{
int t;
while (1) {
pthread_rwlock_wrlock(&rwlock);
t = counter;
usleep(100);
printf("write %x :counter=%d ++counter=%d\n", (int)pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}
}
void *th_read(void*arg)
{
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("read %x :%d\n", (int)pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}
}
int main(void)
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 3; i++)
pthread_create(&tid[i],NULL, th_write, NULL);
for (i = 0; i < 5; i++)
pthread_create(&tid[i+3],NULL, th_read, NULL);
pthread_rwlock_destroy(&rwlock);
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
return 0;
}
条件变量给多个线程提供了一个汇合的场所。条件变量控制原语。
#include
pthread_cond_t cond =PTHREAD_COND_INITIALIZER;
int pthread_cond_destroy(pthread_cond_t*cond);
int pthread_cond_init(pthread_cond_t*restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t*restrict cond,
pthread_mutex_t *restrict mutex, conststruct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t*restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t*cond);
int pthread_cond_signal(pthread_cond_t*cond);
生产者消费者模型
#include
#include
#include
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_thas_product = 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_product, &lock);
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n",mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void*p)
{
struct msg *mp;
for (;;) {
mp = malloc(sizeof(structmsg));
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_product);
sleep(rand() % 5);
}
}
int main(int argc,char *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;
}
是互斥锁的升级版。信号量控制原语。
#include
sem_t;
int sem_init(sem_t *sem, int pshared,unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const structtimespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_destroy(sem_t *sem);
生产者消费者实例
#include
#include
#include
#include
#define NUM 5
int queue[NUM];
sem_t blank_number,product_number;
void *producer(void*arg)
{
int p = 0;
while (1) {
sem_wait(&blank_number);
queue[p] = rand() % 1000 + 1;
printf("Produce%d\n", queue[p]);
sem_post(&product_number);
p = (p+1)%NUM;
sleep(rand()%5);
}
}
void *consumer(void*arg)
{
int c = 0;
while (1) {
sem_wait(&product_number);
printf("Consume%d\n", queue[c]);
queue[c] = 0;
sem_post(&blank_number);
c = (c+1)%NUM;
sleep(rand()%5);
}
}
int main(int argc,char *argv[])
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM);
sem_init(&product_number, 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;
}
操作原语。
#include
intpthread_mutexattr_destroy(pthread_mutexattr_t *attr);
intpthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_getpshared(constpthread_mutexattr_t * restrict attr, int *restrict pshared);
intpthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
例子
#include
#include
#include
#include
#include
#include
#include
#include
struct mt {
int num;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};
int main(void)
{
int fd, i;
struct mt *mm;
pid_t pid;
fd = open("mt_test", O_CREAT| O_RDWR, 0777);
/* 不需要write,文件里初始值为0 */
ftruncate(fd, sizeof(*mm));
mm = mmap(NULL, sizeof(*mm),PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
memset(mm, 0, sizeof(*mm));
/* 初始化互斥对象属性 */
pthread_mutexattr_init(&mm->mutexattr);
/* 设置互斥对象为PTHREAD_PROCESS_SHARED共享,即可以在多个进程的线程访问,PTHREAD_PROCESS_PRIVATE
* 为同一进程的线程共享 */
pthread_mutexattr_setpshared(&mm->mutexattr,PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&mm->mutex,&mm->mutexattr);
pid = fork();
if (pid == 0){
/* 加10次。相当于加10 */
for (i=0;i<10;i++){
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("num++:%d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
}
else if (pid > 0) {
/* 父进程完成x+2,加10次,相当于加20 */
for (i=0; i<10; i++){
pthread_mutex_lock(&mm->mutex);
mm->num += 2;
printf("num+=2:%d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
wait(NULL);
}
pthread_mutex_destroy(&mm->mutex);
pthread_mutexattr_destroy(&mm->mutexattr);
/* 父子均需要释放 */
munmap(mm,sizeof(*mm));
unlink("mt_test");
return 0;
}
文件锁
使用文件锁实现进程锁。
使用fcntl提供文件锁。
#include
#include
int fcntl(int fd, int cmd, ... /* arg */ );
struct flock {
...
short l_type; /* Type of lock: F_RDLCK,
F_WRLCK,F_UNLCK */
short l_whence; /* How to interpret l_start:
SEEK_SET,SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock
(F_GETLKonly) */
...
};
例子
#include
#include
#include
#include
#include
#include
void sys_err(char*str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int fd;
struct flock f_lock;
if (argc < 2) {
printf("./a.outfilename\n");
exit(1);
}
if ((fd = open(argv[1], O_RDWR)) <0)
sys_err("open");
//f_lock.l_type = F_WRLCK;
f_lock.l_type = F_RDLCK;
f_lock.l_whence = SEEK_SET;
f_lock.l_start = 0;
f_lock.l_len = 0; //0表示整个文件加锁
fcntl(fd, F_SETLKW, &f_lock);
printf("get flock\n");
sleep(10);
f_lock.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &f_lock);
printf("un flock\n");
//close(fd);
return 0;
}
从应用的角度看,协议可理解为规则,是数据传输和数据的解释的规则。仅仅在两个端点间被遵守的协议称之为原始协议。当此协议被更多的人采用,不断的增加、改进、维护、完善,最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中,该协议就成为一个标准协议。最早的ftp协议由此衍生而来。tcp协议注重数据的传输。http协议着重于数据的解释。
常见的典型协议
传输层 常见协议有TCP/UDP协议。
应用层 常见的协议有HTTP协议,FTP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层 常见协议有ARP协议、RARP协议。
TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
FTP文件传输协议(File Transfer Protocol)
IP协议是因特网互联协议(InternetProtocol)
ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议是 Internet 组管理协议(InternetGroup Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP是反向地址转换协议,通过MAC地址确定IP地址。
c/s模式,传统的网络应用设计模式,客户机client/服务器server模式。需要在通讯两端各自部署客户机和服务器完成数据通信。
b/s模式,浏览器brower/服务器server模式。只需在一端服务器,而另外一端使用每台pc都默认配置的浏览器即可完成数据的传输。
优缺点
对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。而且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯公司所采用的通信协议,即为ftp协议的修改剪裁版。
因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。
C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。
B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。
B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。
因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。
OSI七层模型和TCP/IP四层模型。
OSI七层模型
物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
应用层:是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
TCP/IP四层模型
TCP/IP网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层。
数据包封装格式。
以太网帧格式。
arp数据报格式。
ip段格式。
udp数据报格式。
tcp数据包格式。
TCP连接建立断开。包含三次握手和四次挥手。
滑动窗口(tcp流量控制)
tcp状态转换。
半关闭。
2msl。
端口复用。在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:
intopt = 1;
setsockopt(listenfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
tcp异常断开。心跳检测机制。
Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
#include
int inet_pton(int af, const char *src, void*dst);
const char *inet_ntop(int af, const void*src, char *dst, socklen_t size);
支持IPv4和IPv6
可重入函数
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr。
因此函数接口是void *addrptr。
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
struct sockaddr {
sa_family_tsa_family; /* address family,AF_xxx */
charsa_data[14]; /* 14bytes of protocol address */
};
使用 sudo grep -r"struct sockaddr_in {" /usr 命令可查看到struct sockaddr_in结构体的定义。一般其默认的存储位置:/usr/include/linux/in.h 文件中。
struct sockaddr_in {
__kernel_sa_family_tsin_family; /*Address family */ 地址结构类型
__be16sin_port; /* Port number */ 端口号
structin_addr sin_addr; /*Internet address */ IP地址
/*Pad to size of `struct sockaddr'. */
unsignedchar __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsignedshort int) - sizeof(struct in_addr)];
};
struct in_addr { /* Internet address.*/
__be32s_addr;
};
struct sockaddr_in6 {
unsignedshort int sin6_family; /*AF_INET6 */
__be16sin6_port; /*Transport layer port # */
__be32sin6_flowinfo; /* IPv6 flowinformation */
structin6_addr sin6_addr; /*IPv6 address */
__u32sin6_scope_id; /*scope id (new in RFC2553) */
};
struct in6_addr {
union{
__u8u6_addr8[16];
__be16u6_addr16[8];
__be32u6_addr32[4];
}in6_u;
#defines6_addr in6_u.u6_addr8
#defines6_addr16 in6_u.u6_addr16
#defines6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
structsockaddr_un {
__kernel_sa_family_tsun_family; /* AF_UNIX */
charsun_path[UNIX_PATH_MAX]; /* pathname*/
};
Pv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sock-addr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void*类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)); /*initialize servaddr */
socket函数
#include /* See NOTES*/
#include
int socket(int domain, int type, intprotocol);
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
bind函数
#include /* See NOTES*/
#include
int bind(int sockfd, const struct sockaddr*addr, socklen_t addrlen);
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
listen函数
#include /* See NOTES*/
#include
int listen(int sockfd, int backlog);
查看系统默认backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
accept函数
#include /* See NOTES */
#include
int accept(int sockfd, struct sockaddr*addr, socklen_t *addrlen);
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-resultargument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
connect函数
#include /* SeeNOTES */
#include
int connect(int sockfd, const struct sockaddr*addr, socklen_t addrlen);
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
例子
服务端
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT8888
#define SERV_IP"127.0.0.1"
int main()
{
int lfd, cfd;
struct sockaddr_in serv_addr,clie_addr;
socklen_t clie_addr_len;
char buf[BUFSIZ];
int n, i;
lfd = socket(AF_INET, SOCK_STREAM, 0);
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr =htonl(INADDR_ANY);
bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(lfd, 128);
clie_addr_len = sizeof(clie_addr);
cfd = accept(lfd, (struct sockaddr*)&clie_addr, &clie_addr_len);
n= read(cfd, buf, sizeof(buf));
for(i = 0; i < n; i++)
{
buf[i] = toupper(buf[i]);
}
write(cfd, buf, n);
close(lfd);
close(cfd);
return 0;
}
客户端
#include
#include
#include
#include
#include
#include
#define SERV_IP"127.0.0.1"
#define SERV_PORT8888
int main(void)
{
int cfd, n;
struct sockaddr_in serv_addr;
socklen_t serv_addr_len;
char buf[BUFSIZ];
cfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP,&serv_addr.sin_addr.s_addr);
connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
while(1)
{
fgets(buf, sizeof(buf), stdin);
write(cfd, buf, strlen(buf));
n = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
}
close(cfd);
return 0;
}
上面的例子不仅功能简单,几乎没有什么错误处理,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
为使错误处理的代码不影响主程序的可读性,把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块。
例子
封装容错函数wrap.h和wrap.c
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(constchar *s);
int Accept(int fd,struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd,const struct sockaddr *sa, socklen_t salen);
int Connect(int fd,const struct sockaddr *sa, socklen_t salen);
int Listen(int fd,int backlog);
int Socket(intfamily, int type, int protocol);
ssize_t Read(int fd,void *ptr, size_t nbytes);
ssize_t Write(int fd,const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd,void *vptr, size_t n);
ssize_t Writen(intfd, const void *vptr, size_t n);
ssize_t my_read(intfd, char *ptr);
ssize_t Readline(intfd, void *vptr, size_t maxlen);
#endif
wrap.c
#include
#include
#include
#include
#include
void perr_exit(constchar *s)
{
perror(s);
exit(-1);
}
int Accept(int fd,struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if((n = accept(fd, as, saleptr)) <0)
{
if((errno = ECONNABORTED) ||(errno == EINTR))
{
goto again;
}
else
{
perr_exit("accepterror");
}
}
return n;
}
int Bind(int fd,const struct sockaddr *sa, socklen_t salen)
{
int n;
if((n = bind(fd, sa, salen)) < 0)
{
perr_exit("binderror");
}
return n;
}
int Connect(int fd,const struct sockaddr *sa, socklen_t salen)
{
int n;
printf("//////////in wrap beforeconnect()\n");
n = connect(fd, sa, salen);
printf("//////////in wrapconnect() return %d\n", n);
if(n < 0)
{
perr_exit("connecterror");
}
return n;
}
int Listen(int fd,int backlog)
{
int n;
if((n = listen(fd, backlog)) < 0)
{
perr_exit("listenerror");
}
return n;
}
int Socket(intfamily, int type, int protocol)
{
int n;
if((n = socket(family, type, protocol))< 0)
{
perr_exit("socketerror");
}
return n;
}
ssize_t Read(int fd,void *ptr, size_t nbytes)
{
ssize_t n;
again:
if((n = read(fd, ptr, nbytes)) == -1)
{
if(errno == EINTR)
{
goto again;
}
else
{
return -1;
}
}
return n;
}
ssize_t Write(int fd,const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if((n = write(fd, ptr, nbytes)) == -1)
{
if(errno == EINTR)
{
goto again;
}
else
{
return -1;
}
}
return n;
}
int Close(int fd)
{
int n;
if((n = close(fd)) == -1)
{
perr_exit("closeerror");
}
return n;
}
ssize_t Readn(int fd,void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while(nleft > 0)
{
if((nread = read(fd, ptr,nleft)) < 0)
{
if(errno == EINTR)
{
nread = 0;
}
else
{
return -1;
}
}
else if(nread == 0)
{
break;
}
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(intfd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while(nleft > 0)
{
if((nwritten = write(fd, ptr,nleft)) < 0)
{
if(nwritten < 0 &&errno == EINTR)
{
nwritten = 0;
}
else
{
return -1;
}
}
nleft -= nwritten;
ptr += nwritten;
}
return n - nleft;
}
static ssize_tmy_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if(read_cnt <= 0)
{
again:
if((read_cnt = read(fd, read_buf,sizeof(read_buf))) < 0)
{
if(errno == EINTR)
{
goto again;
}
else if(read_cnt == 0)
{
return 0;
}
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(intfd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for(n = 1; n < maxlen; n++)
{
if((rc = my_read(fd, &c))== 1)
{
*ptr++ = c;
if(c == '\n')
{
break;
}
}
else if(rc == 0)
{
*ptr = 0;
return n-1;
}
else
{
return -1;
}
}
*ptr = 0;
return n;
}
在server.c和client.c中包含wrap.h,然后使用wrap.c中封装的函数。
使用多进程并发服务器时要考虑以下几点:
父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
系统内创建进程个数(与内存大小相关)
进程创建过多是否降低整体服务性能(进程调度)
例子
使用上述例子中的错误处理函数wrap.h和wrap.c
编写server.c
#include
#include
#include
#include
#include
#include
#include
#include"wrap.h"
#define SERV_PORT8888
void wait_child(intargv)
{
while(waitpid(0, NULL, WNOHANG) >0);
return ;
}
int main(void)
{
pid_t pid;
int lfd, cfd, n, i;
struct sockaddr_in serv_addr,clie_addr;
socklen_t clie_addr_len;
char buf[BUFSIZ], clie_IP[BUFSIZ];
lfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr =htonl(INADDR_ANY);
Bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
Listen(lfd, 128);
while(1)
{
clie_addr_len =sizeof(clie_addr);
cfd = Accept(lfd, (structsockaddr *)&clie_addr, &clie_addr_len);
printf("client IP:%s,port:%d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP,sizeof(clie_IP)), ntohs(clie_addr.sin_port));
pid = fork();
if(pid < 0)
{
perror("forkerror");
exit(1);
}
else if(pid == 0)
{
close(lfd);
break;
}
else
{
close(cfd);
signal(SIGCHLD,wait_child);
}
}
if(pid == 0)
{
while(1)
{
n = read(cfd, buf,sizeof(buf));
if(n == 0)
{
close(cfd);
return 0;
}
else if(n == -1)
{
perror("read error");
exit(1);
}
else
{
for(i = 0; i< n; i++)
{
buf[i]= toupper(buf[i]);
}
write(cfd, buf,n);
write(STDOUT_FILENO, buf, n);
}
}
}
return 0;
}
使用上述例子的错误处理函数
编写server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"wrap.h"
#define SERV_PORT8888
#define MAXLINE 8192
struct s_info
{
struct sockaddr_in cliaddr;
int connfd;
};
void *do_work(void*arg)
{
int n,i;
struct s_info *ts = (structs_info*)arg;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
while(1)
{
n = Read(ts->connfd, buf,MAXLINE);
if(n == 0)
{
printf("the client%d closed...\n", ts->connfd);
break;
}
printf("received from %sat PORT %d\n",
inet_ntop(AF_INET,&(*ts).cliaddr.sin_addr, str, sizeof(str)),
ntohs((*ts).cliaddr.sin_port));
for(i = 0; i < n; i++)
{
buf[i] =toupper(buf[i]);
}
Write(STDOUT_FILENO, buf, n);
Write(ts->connfd, buf, n);
}
Close(ts->connfd);
return (void *)0;
}
void wait_child(intargv)
{
while(waitpid(0, NULL, WNOHANG) >0);
return ;
}
int main(void)
{
pthread_t tid;
int lfd, cfd, i = 0;
struct sockaddr_in serv_addr,clie_addr;
socklen_t clie_addr_len;
struct s_info ts[256];
lfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
Listen(lfd, 128);
while(1)
{
clie_addr_len =sizeof(clie_addr);
cfd = Accept(lfd, (structsockaddr *)&clie_addr, &clie_addr_len);
ts[i].cliaddr = clie_addr;
ts[i].connfd = cfd;
pthread_create(&tid, NULL,do_work, (void *)&ts[i]);
pthread_detach(tid);
i++;
}
return 0;
}
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种
select
select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。
#include
/* According toearlier standards */
#include
#include
#include
int select(int nfds,fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds*/
};
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
select实现高并发服务器
server.c
#include
#include
#include
#include
#include
#include"wrap.h"
#define MAXLINE 80
#define SERV_PORT6666
int main(int argc,char *argv[])
{
int i, maxi, maxfd, listenfd, connfd,sockfd;
int nready, client[FD_SETSIZE]; /* FD_SETSIZE 默认为 1024 */
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; /* #defineINET_ADDRSTRLEN 16 */
socklen_t cliaddr_len;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family =AF_INET;
servaddr.sin_addr.s_addr= htonl(INADDR_ANY);
servaddr.sin_port =htons(SERV_PORT);
Bind(listenfd,(struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20); /* 默认最大128 */
maxfd = listenfd; /* 初始化 */
maxi = -1; /*client[]的下标 */
for (i = 0; i maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 更新client[]最大下标值 */
if (--nready == 0)
continue; /* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,
负责处理未处理完的就绪文件描述符 */
}
for (i = 0; i <= maxi;i++) { /* 检测哪个clients 有数据就绪 */
if ( (sockfd =client[i]) < 0)
continue;
if (FD_ISSET(sockfd,&rset)) {
if ( (n =Read(sockfd, buf, MAXLINE)) == 0) {
Close(sockfd); /* 当client关闭链接时,服务器端也关闭对应链接 */
FD_CLR(sockfd,&allset); /* 解除select监控此文件描述符 */
client[i]= -1;
} else {
intj;
for(j = 0; j < n; j++)
buf[j]= toupper(buf[j]);
Write(sockfd,buf, n);
}
if(--nready == 0)
break;
}
}
}
close(listenfd);
return 0;
}
poll
是select的改进版
#include
int poll(structpollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监控的事件 */
short revents; /* 监控事件中满足条件返回的事件 */
};
POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
POLLRDNORM 数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级可读数据
POLLOUT 普通或带外数据可写
POLLWRNORM 数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
nfds 监控数组中有多少文件描述符需要被监控
timeout 毫秒级等待
-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏
0:立即返回,不阻塞进程
>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0。
使用poll实现多路I/O转接
server.c
#include
#include
#include
#include
#include
#include
#include
#include"wrap.h"
#define MAXLINE 80
#define SERV_PORT6666
#define OPEN_MAX 1024
int main(int argc,char *argv[])
{
int i, j, maxi, listenfd, connfd,sockfd;
int nready;
ssize_t n;
char buf[MAXLINE],str[INET_ADDRSTRLEN];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM,0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
client[0].fd = listenfd;
client[0].events = POLLRDNORM; /*listenfd监听普通读事件 */
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
maxi = 0; /*client[]数组有效元素中最大元素下标 */
for ( ; ; ) {
nready = poll(client, maxi+1,-1); /* 阻塞 */
if (client[0].revents &POLLRDNORM) { /* 有客户端链接请求 */
clilen =sizeof(cliaddr);
connfd =Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("receivedfrom %s at PORT %d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 1; i maxi)
maxi = i; /*更新client[]中最大元素下标 */
if (--nready <=0)
continue; /*没有更多就绪事件时,继续回到poll阻塞 */
}
for (i = 1; i <= maxi;i++) { /* 检测client[] */
if ((sockfd =client[i].fd) < 0)
continue;
if(client[i].revents & (POLLRDNORM | POLLERR)) {
if ((n =Read(sockfd, buf, MAXLINE)) < 0) {
if(errno == ECONNRESET) { /* 当收到 RST标志时 */
/*connection reset by client */
printf("client[%d]aborted connection\n", i);
Close(sockfd);
client[i].fd= -1;
}else {
perr_exit("readerror");
}
} else if(n == 0) {
/*connection closed by client */
printf("client[%d]closed connection\n", i);
Close(sockfd);
client[i].fd= -1;
} else {
for(j = 0; j < n; j++)
buf[j]= toupper(buf[j]);
Writen(sockfd,buf, n);
}
if(--nready <= 0)
break; /* nomore readable descriptors */
}
}
}
return 0;
}
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(EdgeTriggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
可以使用cat命令查看一个进程可以打开的socket描述符上限。
cat/proc/sys/fs/file-max
如有需要,可以通过修改配置文件的方式修改该上限值。
sudovi /etc/security/limits.conf
在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。
*soft nofile 65536
*hard nofile 100000
基础API
创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。
#include
intepoll_create(int size) size:监听数目
控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
#include
intepoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd: 为epoll_creat的句柄
op: 表示动作,用3个宏来表示:
EPOLL_CTL_ADD(注册新的fd到epfd),
EPOLL_CTL_MOD(修改已经注册的fd的监听事件),
EPOLL_CTL_DEL(从epfd删除一个fd);
event: 告诉内核需要监听的事件
structepoll_event {
__uint32_tevents; /* Epoll events */
epoll_data_tdata; /* User data variable */
};
typedefunion epoll_data {
void*ptr;
intfd;
uint32_tu32;
uint64_tu64;
}epoll_data_t;
EPOLLIN: 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
等待所监控文件描述符上有事件的产生,类似于select()调用。
#include
intepoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events: 用来存内核得到事件的集合,
maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout: 是超时时间
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
epoll实现高并发服务器
server.c
#include
#include
#include
#include
#include
#include
#include
#include"wrap.h"
#define MAXLINE 80
#define SERV_PORT6666
#define OPEN_MAX 1024
int main(int argc,char *argv[])
{
int i, j, maxi, listenfd, connfd,sockfd;
int nready, efd, res;
ssize_t n;
char buf[MAXLINE],str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX];
listenfd = Socket(AF_INET, SOCK_STREAM,0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
for (i = 0; i < OPEN_MAX; i++)
client[i] = -1;
maxi = -1;
efd = epoll_create(OPEN_MAX);
if (efd == -1)
perr_exit("epoll_create");
tep.events = EPOLLIN; tep.data.fd =listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD,listenfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");
while (1) {
nready = epoll_wait(efd, ep,OPEN_MAX, -1); /* 阻塞监听 */
if (nready == -1)
perr_exit("epoll_wait");
for (i = 0; i < nready;i++) {
if (!(ep[i].events& EPOLLIN))
continue;
if (ep[i].data.fd ==listenfd) {
clilen =sizeof(cliaddr);
connfd =Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("receivedfrom %s at PORT %d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (j = 0;j < OPEN_MAX; j++) {
if(client[j] < 0) {
client[j]= connfd; /* save descriptor */
break;
}
}
if (j ==OPEN_MAX)
perr_exit("toomany clients");
if (j >maxi)
maxi= j; /* max index in client[]array */
tep.events= EPOLLIN;
tep.data.fd= connfd;
res =epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res ==-1)
perr_exit("epoll_ctl");
} else {
sockfd =ep[i].data.fd;
n =Read(sockfd, buf, MAXLINE);
if (n == 0){
for(j = 0; j <= maxi; j++) {
if (client[j] == sockfd) {
client[j]= -1;
break;
}
}
res= epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if(res == -1)
perr_exit("epoll_ctl");
Close(sockfd);
printf("client[%d]closed connection\n", j);
} else {
for(j = 0; j < n; j++)
buf[j]= toupper(buf[j]);
Writen(sockfd,buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}
epoll的事件模式
事件模型
EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发只要有数据都会触发。
思考如下步骤:
假定我们已经把一个用来从管道中读取数据的文件描述符(RFD)添加到epoll描述符。
管道的另一端写入了2KB的数据
调用epoll_wait,并且它会返回RFD,说明它已经准备好读取操作
读取1KB的数据
调用epoll_wait……
在这个过程中,有两种工作模式:
ET模式
ET模式即Edge Triggered工作模式。
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
基于非阻塞文件句柄
只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
LT模式
LT模式即Level Triggered工作模式。
与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。
LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge-triggered):ET是高速工作方式,只支持no-blocksocket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).
基于管道的epoll边缘触发模式
#include
#include
#include
#include
#include
#define MAXLINE 10
int main(int argc,char *argv[])
{
int efd, i;
int pfd[2];
pid_t pid;
char buf[MAXLINE], ch = 'a';
pipe(pfd);
pid = fork();
if (pid == 0) {
close(pfd[0]);
while (1) {
for (i = 0; i 0) {
struct epoll_event event;
struct epoll_eventresevent[10];
int res, len;
close(pfd[1]);
efd = epoll_create(10);
/* event.events = EPOLLIN; */
event.events = EPOLLIN |EPOLLET; /* ET 边沿触发,默认是水平触发 */
event.data.fd = pfd[0];
epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0],&event);
while (1) {
res =epoll_wait(efd, resevent, 10, -1);
printf("res%d\n", res);
if (resevent[0].data.fd== pfd[0]) {
len =read(pfd[0], buf, MAXLINE/2);
write(STDOUT_FILENO,buf, len);
}
}
close(pfd[0]);
close(efd);
} else {
perror("fork");
exit(-1);
}
return 0;
epoll边缘模式实现I/O
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 10
#define SERV_PORT8080
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, efd, flag;
listenfd = socket(AF_INET, SOCK_STREAM,0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
/* event.events = EPOLLIN; */
event.events = EPOLLIN | EPOLLET; /* ET 边沿触发,默认是水平触发 */
printf("Accepting connections...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (structsockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
flag = fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd,&event);
while (1) {
printf("epoll_waitbegin\n");
res = epoll_wait(efd,resevent, 10, -1);
printf("epoll_wait endres %d\n", res);
if (resevent[0].data.fd ==connfd) {
while ((len =read(connfd, buf, MAXLINE/2)) > 0)
write(STDOUT_FILENO,buf, len);
}
}
return 0;
}
epoll边缘模式实现非阻塞I/O
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 10
#define SERV_PORT8080
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, efd, flag;
listenfd = socket(AF_INET, SOCK_STREAM,0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
/* event.events = EPOLLIN; */
event.events = EPOLLIN | EPOLLET; /* ET 边沿触发,默认是水平触发 */
printf("Accepting connections...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (structsockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
flag = fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd,&event);
while (1) {
printf("epoll_waitbegin\n");
res = epoll_wait(efd,resevent, 10, -1);
printf("epoll_wait endres %d\n", res);
if (resevent[0].data.fd ==connfd) {
while ((len =read(connfd, buf, MAXLINE/2)) > 0)
write(STDOUT_FILENO,buf, len);
}
}
return 0;
}
epoll反应堆模型
使用libevent库。好处,跨平台;代码精炼,使用epoll的非阻塞模式;使用回调函数,反应速度快。
例子
threadpool.h
#ifndef__THREADPOOL_H_
#define__THREADPOOL_H_
typedef structthreadpool_t threadpool_t;
threadpool_t*threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);
intthreadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg);
intthreadpool_destroy(threadpool_t *pool);
int threadpool_all_threadnum(threadpool_t*pool);
intthreadpool_busy_threadnum(threadpool_t *pool);
#endif
threadpool.c
#include
#include
#include
#include
#include
#include
#include
#include
#include"threadpool.h"
#define DEFAULT_TIME10 /*10s检测一次*/
#defineMIN_WAIT_TASK_NUM 10 /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/
#defineDEFAULT_THREAD_VARY 10 /*每次创建和销毁线程的个数*/
#define true 1
#define false 0
typedef struct {
void *(*function)(void *); /* 函数指针,回调函数 */
void *arg; /* 上面函数的参数 */
}threadpool_task_t; /* 各子线程任务结构体 */
/* 描述线程池相关信息 */
struct threadpool_t {
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- busy_thr_num */
pthread_cond_t queue_not_full; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queue_not_empty; /* 任务队列里不为空时,通知等待任务的线程 */
pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid; /* 存管理线程tid */
threadpool_task_t *task_queue; /* 任务队列 */
int min_thr_num; /* 线程池最小线程数 */
int max_thr_num; /* 线程池最大线程数 */
int live_thr_num; /* 当前存活线程个数 */
int busy_thr_num; /* 忙状态线程个数 */
int wait_exit_thr_num; /* 要销毁的线程个数 */
int queue_front; /* task_queue队头下标 */
int queue_rear; /* task_queue队尾下标 */
int queue_size; /* task_queue队中实际任务数 */
int queue_max_size; /* task_queue队列可容纳任务数上限 */
int shutdown; /* 标志位,线程池使用状态,true或false */
};
/**
* @function void *threadpool_thread(void*threadpool)
* @desc the worker thread
* @param threadpool the pool which own thethread
*/
void*threadpool_thread(void *threadpool);
/**
* @function void *adjust_thread(void*threadpool);
* @desc manager thread
* @param threadpool the threadpool
*/
void*adjust_thread(void *threadpool);
/**
* check a thread is alive
*/
intis_thread_alive(pthread_t tid);
intthreadpool_free(threadpool_t *pool);
threadpool_t*threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
int i;
threadpool_t *pool = NULL;
do {
if((pool = (threadpool_t*)malloc(sizeof(threadpool_t))) == NULL) {
printf("malloc threadpoolfail");
break;/*跳出do while*/
}
pool->min_thr_num = min_thr_num;
pool->max_thr_num = max_thr_num;
pool->busy_thr_num = 0;
pool->live_thr_num =min_thr_num; /* 活着的线程数初值=最小线程数 */
pool->queue_size = 0; /* 有0个产品 */
pool->queue_max_size =queue_max_size;
pool->queue_front = 0;
pool->queue_rear = 0;
pool->shutdown = false; /* 不关闭线程池 */
/* 根据最大线程上限数,给工作线程数组开辟空间, 并清零 */
pool->threads = (pthread_t *)malloc(sizeof(pthread_t)*max_thr_num);
if (pool->threads == NULL) {
printf("malloc threadsfail");
break;
}
memset(pool->threads, 0,sizeof(pthread_t)*max_thr_num);
/* 队列开辟空间 */
pool->task_queue =(threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
if (pool->task_queue == NULL) {
printf("malloc task_queuefail");
break;
}
/* 初始化互斥琐、条件变量 */
if (pthread_mutex_init(&(pool->lock),NULL) != 0
||pthread_mutex_init(&(pool->thread_counter), NULL) != 0
||pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
||pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
{
printf("init the lock or condfail");
break;
}
/* 启动 min_thr_num 个 work thread */
for (i = 0; i < min_thr_num; i++) {
pthread_create(&(pool->threads[i]), NULL, threadpool_thread,(void *)pool);/*pool指向当前线程池*/
printf("start thread0x%x...\n", (unsigned int)pool->threads[i]);
}
pthread_create(&(pool->adjust_tid), NULL, adjust_thread, (void*)pool);/* 启动管理者线程 */
return pool;
} while (0);
threadpool_free(pool); /* 前面代码调用失败时,释放poll存储空间 */
return NULL;
}
/* 向线程池中添加一个任务 */
intthreadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
{
pthread_mutex_lock(&(pool->lock));
/* ==为真,队列已经满, 调wait阻塞 */
while ((pool->queue_size ==pool->queue_max_size) && (!pool->shutdown)) {
pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
}
if (pool->shutdown) {
pthread_mutex_unlock(&(pool->lock));
}
/* 清空 工作线程调用的回调函数 的参数arg */
if (pool->task_queue[pool->queue_rear].arg!= NULL) {
free(pool->task_queue[pool->queue_rear].arg);
pool->task_queue[pool->queue_rear].arg = NULL;
}
/*添加任务到任务队列里*/
pool->task_queue[pool->queue_rear].function = function;
pool->task_queue[pool->queue_rear].arg= arg;
pool->queue_rear = (pool->queue_rear+ 1) % pool->queue_max_size; /* 队尾指针移动, 模拟环形 */
pool->queue_size++;
/*添加完任务后,队列不为空,唤醒线程池中等待处理任务的线程*/
pthread_cond_signal(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
/* 线程池中各个工作线程 */
void*threadpool_thread(void *threadpool)
{
threadpool_t *pool = (threadpool_t*)threadpool;
threadpool_task_t task;
while (true) {
/* Lock must be taken to wait onconditional variable */
/*刚创建出线程,等待任务队列里有任务,否则阻塞等待任务队列里有任务后再唤醒接收任务*/
pthread_mutex_lock(&(pool->lock));
/*queue_size == 0 说明没有任务,调 wait 阻塞在条件变量上, 若有任务,跳过该while*/
while ((pool->queue_size == 0)&& (!pool->shutdown)) {
printf("thread 0x%x iswaiting\n", (unsigned int)pthread_self());
pthread_cond_wait(&(pool->queue_not_empty),&(pool->lock));
/*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/
if (pool->wait_exit_thr_num >0) {
pool->wait_exit_thr_num--;
/*如果线程池里线程个数大于最小值时可以结束当前线程*/
if (pool->live_thr_num >pool->min_thr_num) {
printf("thread 0x%x isexiting\n", (unsigned int)pthread_self());
pool->live_thr_num--;
pthread_mutex_unlock(&(pool->lock));
pthread_exit(NULL);
}
}
}
/*如果指定了true,要关闭线程池里的每个线程,自行退出处理*/
if (pool->shutdown) {
pthread_mutex_unlock(&(pool->lock));
printf("thread 0x%x isexiting\n", (unsigned int)pthread_self());
pthread_exit(NULL); /* 线程自行结束 */
}
/*从任务队列里获取任务, 是一个出队操作*/
task.function =pool->task_queue[pool->queue_front].function;
task.arg = pool->task_queue[pool->queue_front].arg;
pool->queue_front =(pool->queue_front + 1) % pool->queue_max_size; /* 出队,模拟环形队列 */
pool->queue_size--;
/*通知可以有新的任务添加进来*/
pthread_cond_broadcast(&(pool->queue_not_full));
/*任务取出后,立即将 线程池琐释放*/
pthread_mutex_unlock(&(pool->lock));
/*执行任务*/
printf("thread 0x%x startworking\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter)); /*忙状态线程数变量琐*/
pool->busy_thr_num++; /*忙状态线程数+1*/
pthread_mutex_unlock(&(pool->thread_counter));
(*(task.function))(task.arg); /*执行回调函数任务*/
//task.function(task.arg); /*执行回调函数任务*/
/*任务结束处理*/
printf("thread 0x%x endworking\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter));
pool->busy_thr_num--; /*处理掉一个任务,忙状态数线程数-1*/
pthread_mutex_unlock(&(pool->thread_counter));
}
pthread_exit(NULL);
}
/* 管理线程 */
void*adjust_thread(void *threadpool)
{
int i;
threadpool_t *pool = (threadpool_t *)threadpool;
while (!pool->shutdown) {
sleep(DEFAULT_TIME); /*定时对线程池管理*/
pthread_mutex_lock(&(pool->lock));
int queue_size =pool->queue_size; /* 关注 任务数 */
int live_thr_num =pool->live_thr_num; /* 存活 线程数 */
pthread_mutex_unlock(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
int busy_thr_num =pool->busy_thr_num; /* 忙着的线程数 */
pthread_mutex_unlock(&(pool->thread_counter));
/* 创建新线程 算法:任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时如:30>=10 && 40<100*/
if (queue_size >= MIN_WAIT_TASK_NUM&& live_thr_num < pool->max_thr_num) {
pthread_mutex_lock(&(pool->lock));
int add = 0;
/*一次增加 DEFAULT_THREAD 个线程*/
for (i = 0; i max_thr_num && add < DEFAULT_THREAD_VARY
&&pool->live_thr_num < pool->max_thr_num; i++) {
if (pool->threads[i] == 0 ||!is_thread_alive(pool->threads[i])) {
pthread_create(&(pool->threads[i]), NULL, threadpool_thread,(void *)pool);
add++;
pool->live_thr_num++;
}
}
pthread_mutex_unlock(&(pool->lock));
}
/* 销毁多余的空闲线程算法:忙线程X2 小于 存活的线程数且 存活的线程数 大于 最小线程数时*/
if ((busy_thr_num * 2) pool->min_thr_num) {
/* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
pthread_mutex_lock(&(pool->lock));
pool->wait_exit_thr_num =DEFAULT_THREAD_VARY; /* 要销毁的线程数设置为10 */
pthread_mutex_unlock(&(pool->lock));
for (i = 0; i queue_not_empty));
}
}
}
return NULL;
}
intthreadpool_destroy(threadpool_t *pool)
{
int i;
if (pool == NULL) {
return -1;
}
pool->shutdown = true;
/*先销毁管理线程*/
pthread_join(pool->adjust_tid, NULL);
for (i = 0; i < pool->live_thr_num;i++) {
/*通知所有的空闲线程*/
pthread_cond_broadcast(&(pool->queue_not_empty));
}
for (i = 0; i < pool->live_thr_num;i++) {
pthread_join(pool->threads[i],NULL);
}
threadpool_free(pool);
return 0;
}
intthreadpool_free(threadpool_t *pool)
{
if (pool == NULL) {
return -1;
}
if (pool->task_queue) {
free(pool->task_queue);
}
if (pool->threads) {
free(pool->threads);
pthread_mutex_lock(&(pool->lock));
pthread_mutex_destroy(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
pthread_mutex_destroy(&(pool->thread_counter));
pthread_cond_destroy(&(pool->queue_not_empty));
pthread_cond_destroy(&(pool->queue_not_full));
}
free(pool);
pool = NULL;
return 0;
}
intthreadpool_all_threadnum(threadpool_t *pool)
{
int all_threadnum = -1;
pthread_mutex_lock(&(pool->lock));
all_threadnum = pool->live_thr_num;
pthread_mutex_unlock(&(pool->lock));
return all_threadnum;
}
intthreadpool_busy_threadnum(threadpool_t *pool)
{
int busy_threadnum = -1;
pthread_mutex_lock(&(pool->thread_counter));
busy_threadnum = pool->busy_thr_num;
pthread_mutex_unlock(&(pool->thread_counter));
return busy_threadnum;
}
intis_thread_alive(pthread_t tid)
{
int kill_rc = pthread_kill(tid, 0); //发0号信号,测试线程是否存活
if (kill_rc == ESRCH) {
return false;
}
return true;
}
/*测试*/
#if 1
/* 线程池中的线程,模拟处理业务 */
void *process(void*arg)
{
printf("thread 0x%x working on task%d\n ",(unsigned int)pthread_self(),*(int *)arg);
sleep(1);
printf("task %d is end\n",*(int*)arg);
return NULL;
}
int main(void)
{
/*threadpool_t *threadpool_create(intmin_thr_num, int max_thr_num, int queue_max_size);*/
threadpool_t *thp =threadpool_create(3,100,100);/*创建线程池,池里最小3个线程,最大100,队列最大100*/
printf("pool inited");
//int *num = (int *)malloc(sizeof(int)*20);
int num[20], i;
for (i = 0; i < 20; i++) {
num[i]=i;
printf("add task %d\n",i);
threadpool_add(thp, process,(void*)&num[i]); /* 向线程池中添加任务 */
}
sleep(10); /* 等子线程完成任务 */
threadpool_destroy(thp);
return 0;
}
#endif
传输层主要应用的协议模型有两种,一种是TCP协议,另外一种则是UDP协议。TCP协议在网络通信中占主导地位,绝大多数的网络通信借助TCP协议完成数据传输。但UDP也是网络通信中不可或缺的重要通信手段。
相较于TCP而言,UDP通信的形式更像是发短信。不需要在数据传输之前建立、维护连接。只专心获取数据就好。省去了三次握手的过程,通信速度可以大大提高,但与之伴随的通信的稳定性和正确率便得不到保证。因此,我们称UDP为“无连接的不可靠报文传递”。
那么与我们熟知的TCP相比,UDP有哪些优点和不足呢?由于无需创建连接,所以UDP开销较小,数据传输速度快,实时性较强。多用于对实时性要求较高的通信场合,如视频会议、电话会议等。但随之也伴随着数据传输不可靠,传输数据的正确率、传输顺序和流量都得不到控制和保证。所以,通常情况下,使用UDP协议进行数据传输,为保证数据的正确性,我们需要在应用层添加辅助校验协议来弥补UDP的不足,以达到数据可靠传输的目的。
与TCP类似的,UDP也有可能出现缓冲区被填满后,再接收数据时丢包的现象。由于它没有TCP滑动窗口的机制,通常采用如下两种方法解决:
服务器应用层设计流量控制,控制发送数据速度。
借助setsockopt函数改变接收缓冲区大小。如:
#include
int setsockopt(int sockfd, int level, intoptname, const void *optval, socklen_t optlen);
intn = 220x1024
setsockopt(sockfd,SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));
c/s模型udp版
server.c
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 80
#define SERV_PORT 6666
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int sockfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
sockfd = socket(AF_INET, SOCK_DGRAM,0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
printf("Accepting connections...\n");
while (1) {
cliaddr_len =sizeof(cliaddr);
n = recvfrom(sockfd, buf,MAXLINE,0, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (n == -1)
perror("recvfromerror");
printf("received from %sat PORT %d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] =toupper(buf[i]);
n = sendto(sockfd, buf, n, 0,(struct sockaddr *)&cliaddr, sizeof(cliaddr));
if (n == -1)
perror("sendtoerror");
}
close(sockfd);
return 0;
}
例子
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT8000 /* 无关紧要 */
#define MAXLINE 1500
#define BROADCAST_IP"192.168.42.255"
#define CLIENT_PORT9000 /* 重要 */
int main(void)
{
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
char buf[MAXLINE];
/* 构造用于UDP通信的套接字 */
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET; /* IPv4 */
serveraddr.sin_addr.s_addr =htonl(INADDR_ANY); /* 本地任意IP INADDR_ANY = 0 */
serveraddr.sin_port = htons(SERVER_PORT);
bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
int flag = 1;
setsockopt(sockfd, SOL_SOCKET,SO_BROADCAST, &flag, sizeof(flag));
/*构造 client 地址 IP+端口 192.168.7.255+9000 */
bzero(&clientaddr, sizeof(clientaddr));
clientaddr.sin_family = AF_INET;
inet_pton(AF_INET, BROADCAST_IP,&clientaddr.sin_addr.s_addr);
clientaddr.sin_port = htons(CLIENT_PORT);
int i = 0;
while (1) {
sprintf(buf, "Drink %d glasses ofwater\n", i++);
//fgets(buf, sizeof(buf), stdin);
sendto(sockfd, buf, strlen(buf), 0,(struct sockaddr *)&clientaddr, sizeof(clientaddr));
sleep(1);
}
close(sockfd);
return 0;
}
组播组可以是永久的也可以是临时的。组播组地址中,有一部分由官方分配的,称为永久组播组。永久组播组保持不变的是它的ip地址,组中的成员构成可以发生变化。永久组播组中成员的数量都可以是任意的,甚至可以为零。那些没有保留下来供永久组播组使用的ip组播地址,可以被临时组播组利用。
224.0.0.0~224.0.0.255 为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
224.0.1.0~224.0.1.255 是公用组播地址,可以用于Internet;欲使用需申请。
224.0.2.0~238.255.255.255 为用户可用的组播地址(临时组地址),全网范围内有效;
239.0.0.0~239.255.255.255 为本地管理组播地址,仅在特定的本地范围内有效。
可使用ip ad命令查看网卡编号。
例子
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT6666
#define CLIENT_PORT9000
#define MAXLINE 1500
#define GROUP"239.0.0.2"
int main(void)
{
int sockfd, i ;
struct sockaddr_in serveraddr,clientaddr;
char buf[MAXLINE] = "haha\n";
char ipstr[INET_ADDRSTRLEN]; /* 16Bytes */
socklen_t clientlen;
ssize_t len;
struct ip_mreqn group;
/* 构造用于UDP通信的套接字 */
sockfd = socket(AF_INET, SOCK_DGRAM,0);
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET; /*IPv4 */
serveraddr.sin_addr.s_addr =htonl(INADDR_ANY); /* 本地任意IPINADDR_ANY = 0 */
serveraddr.sin_port = htons(SERVER_PORT);
bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
/*设置组地址*/
inet_pton(AF_INET, GROUP,&group.imr_multiaddr);
/*本地任意IP*/
inet_pton(AF_INET, "0.0.0.0",&group.imr_address);
/* eth0 --> 编号命令:ip ad */
group.imr_ifindex =if_nametoindex("eth0");
setsockopt(sockfd, IPPROTO_IP,IP_MULTICAST_IF, &group, sizeof(group));
/*构造 client 地址 IP+端口 */
bzero(&clientaddr,sizeof(clientaddr));
clientaddr.sin_family = AF_INET; /*IPv4 */
inet_pton(AF_INET, GROUP,&clientaddr.sin_addr.s_addr);
clientaddr.sin_port =htons(CLIENT_PORT);
while (1) {
//fgets(buf, sizeof(buf),stdin);
sendto(sockfd, buf,strlen(buf), 0, (struct sockaddr *)&clientaddr, sizeof(clientaddr));
sleep(1);
}
close(sockfd);
return 0;
}
条件,需要屏幕截图模块;至少24帧。
socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
UNIX Domain Socket是全双工的,API接口语义丰富,相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,比如X Window服务器和GUI程序之间就是通过UNIXDomainSocket通讯的。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
对比网络套接字地址结构和本地套接字地址结构:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */ 地址结构类型
__be16 sin_port; /* Port number */ 端口号
struct in_addr sin_addr; /*Internet address */ IP地址
};
struct sockaddr_un {
__kernel_sa_family_t sun_family; /*AF_UNIX */ 地址结构类型
char sun_path[UNIX_PATH_MAX]; /*pathname */ socket文件名(含路径)
};
例子
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define QLEN 10
/*
* Create a serverendpoint of a connection.
* Returns fd if allOK, <0 on error.
*/
int serv_listen(constchar *name)
{
int fd, len, err, rval;
struct sockaddr_un un;
/* create a UNIX domain stream socket*/
if ((fd = socket(AF_UNIX, SOCK_STREAM,0)) < 0)
return(-1);
/* in case it already exists */
unlink(name);
/* fill in socket address structure */
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un,sun_path) + strlen(name);
/* bind the name to the descriptor */
if (bind(fd, (struct sockaddr*)&un, len) < 0) {
rval = -2;
goto errout;
}
if (listen(fd, QLEN) < 0) { /* tellkernel we're a server */
rval = -3;
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
errno = err;
return(rval);
}
int serv_accept(intlistenfd, uid_t *uidptr)
{
int clifd, len, err, rval;
time_t staletime;
struct sockaddr_un un;
struct stat statbuf;
len = sizeof(un);
if ((clifd = accept(listenfd, (structsockaddr *)&un, &len)) < 0)
return(-1); /* oftenerrno=EINTR, if signal caught */
/* obtain the client's uid from itscalling address */
len -= offsetof(struct sockaddr_un,sun_path); /* len of pathname */
un.sun_path[len] = 0; /* null terminate*/
if (stat(un.sun_path, &statbuf)< 0) {
rval = -2;
goto errout;
}
if (S_ISSOCK(statbuf.st_mode) == 0) {
rval = -3; /* not a socket */
goto errout;
}
if (uidptr != NULL)
*uidptr = statbuf.st_uid; /*return uid of caller */
/* we're done with pathname now */
unlink(un.sun_path);
return(clifd);
errout:
err = errno;
close(clifd);
errno = err;
return(rval);
}
int main(void)
{
int lfd, cfd, n, i;
uid_t cuid;
char buf[1024];
lfd =serv_listen("foo.socket");
if (lfd < 0) {
switch (lfd) {
case-3:perror("listen"); break;
case-2:perror("bind"); break;
case-1:perror("socket"); break;
}
exit(-1);
}
cfd = serv_accept(lfd, &cuid);
if (cfd < 0) {
switch (cfd) {
case-3:perror("not a socket"); break;
case-2:perror("a bad filename"); break;
case-1:perror("accept"); break;
}
exit(-1);
}
while (1) {
r_again:
n = read(cfd, buf, 1024);
if (n == -1) {
if (errno == EINTR)
goto r_again;
}
else if (n == 0) {
printf("the other sidehas been closed.\n");
break;
}
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, n);
}
close(cfd);
close(lfd);
return 0;
}
shell是一种字符终端界面。使用c函数实现一个简单的交互式shell。
功能,给出提示符,让用农户输入一行命令,识别程序和参数并调用适当的exec函数执行程序,待执行完成后再次给出提示符;识别和处理一下符号,简单的标准输入输出重定向<>,管道(|),shell进程先调用pipe创建一对管道描述符,然后fork出两个子进程,一个子进程关闭读端,调用dup2把写端赋给标准输出,另一个子进程关闭写端,调用dup2把读端赋给标准输入,两个子进程分别调用exec执行程序,而shell进程把管道的两端都关闭,调用wait等待两个子进程终止。
比如shell可以处理以下命令:
ls -l -R file1
cat < file1 | wc -c > file1
项目实现步骤
实现加载普通命令
实现重定向功能
实现管道
实现多重管