Linux
基本操作
登录
login # 登录
who # 显示所有用户
whoami # 显示当前登录的用户
man <命令> # 显示命令帮助
exit # 退出终端
常用操作
ed
$ ed <- 激活 ed 命令
a <- 告诉 ed 我要编辑新文件
My name is Titan. <- 输入第一行内容
And I love Perl very much. <- 输入第二行内容
. <- 返回 ed 的命令行状态
i <- 告诉 ed 我要在最后一行之前插入内容
I am 24. <- 将“I am 24.”插入“My name is Titan.”和“And I love Perl very much.”之间
. <- 返回 ed 的命令行状态
c <- 告诉 ed 我要替换最后一行输入内容
I am 24 years old. <- 将“I am 24.”替换成“I am 24 years old.”(注意:这里替换的是最后输的内容)
. <- 返回 ed 的命令行状态
w readme.text <- 将文件命名为“readme.text”并保存(注意:如果是编辑已经存在的文件,只需要敲入 w 即可)
q <- 完全退出 ed 编辑器
ls
ls
ls -l # 详细信息
ls -a # 包括隐藏文件
ls –t # 按修改时间排序
cat
cat # 显示文件
mv
mv <源文件> <目的文件> # 移动文件
# 如果源文件==目的文件,相当于重命名
cp
cp <源文件> <目的文件> # 复制文件
rm
rm <文件...> # 删除文件
wc
wc <文件名>
# 显示文件的:行数、词数、字节数
cmp
cmp <文件1> <文件2>
# 如果文件相同,则不显示信息。如果文件不同,则显示第一个不同的位置
diff
diff <文件1> <文件2>
# 显示两个文件不同的地方
文件目录
pwd
pwd # 显示当前路径
cd
cd <目录名> # 打开目录
cd .. # 打开上一级目录
mkdir
mkdir <目录名> # 创建子目录
rmdir
rmdir <目录名> # 删除子目录
通配符
* 0个或多个字符
? 任何一个字符
[a-z] a-z所有字符
[^a-z] a-z之外的所有字符
[xyz] xyz中任一字符
[^xyz] 除xyz任一字符
\ 转义字符
ls *.c
ls a*[1-9].c
ls ?.c
echo *
echo \*
重定向
> # 标准输出重定向
2> # 错误输出重定向
>> # 标准追加重定向
2>> # 错误追加重定向
< # 输入重定向
<< 分界符 # 从标准输入设备(键盘)中读入,直到遇到分界符才停止(读入的数据不包括分界符),这里的分界符其实就是自定义的字符串
管道
<命令1> | <命令2> # 前面命令的输出作为后面命令的输入
进程
who; whoami # 多个命令同时运行
<命令>& # 后台运行
ps # 显示当前运行的进程
kill <进程编号> # 终止某个进程
环境
PS1='`whoami`:`pwd` >' # m
$HOME
$PATH
执行多个命令
分号:顺序地独立执行各条命令, 彼此之间不关心是否失败, 所有命令都会执行。
&&:顺序执行各条命令, 只有当前一个执行成功时候, 才执行后面的。
||:顺序执行各条命令, 只有当前面一个执行失败的时候, 才执行后面的。
文件系统
文件权限
9个权限位,每3个为一组,分别代表文件所有者、同组用户和其他用户的权限。(见上面图片)
rwxrwxr-x
chmod <mode> <文件名...>
<mode> 权限设定字串,格式如下:
[ugoa][+-=][rwxX]
u 表示该文件的拥有者,g 表示同组者,o 表示其他以外的人,a 表示这三者皆是,不写默认为a。
+ 表示增加权限、- 表示取消权限、= 表示唯一设定权限。
r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有当文件为目录文件,或者其他类型的用户有可执行权限时,才将文件权限设置可执行。
举例:
chmod o+w hello.c
chmod a+x hello.c
chmod a-x hello.c
chmod a=r hello.c
chmod ug+w hello.c
八进制语法:将rwx分别对应三位二进制,如rw-对应110,即为6。
举例:
chmod 777 hello.c
chmod 664 hello.c # rw-rw-r--
目录文件权限
- x: 表示具有进入目录的权限
- r: 表示具有浏览目录下有哪些文件的权限,必须同时有x权限才可以
- w:表示具有新增、删除、移动目录内容文件的权限,同时需要有x权限
文件编辑和流处理
ed
a 从当前行开始添加
. 结束添加状态 //必须在行首输入
d 删除当前行
w 存盘
q 退出ed编辑器
临时进入shell !
显示命令 p
p 显示当前行
5 将当前行改为第5行并显示当前行
m,np 显示m到n行
.代表当前行,$代表最后一行
可使用相对行,如 .,.+3p $-5,$p .加偏移量时可以省略。
查找
/模式/ 查找下一个匹配模式的行,遇到尾行则从首行开始
?模式? 查找上一个匹配模式的行,遇到首行则从尾行开始
查找会改变当前行
查找返回的是行号,可使用。
1,/main/p 显示1到第一次出现main的行
/main/-1,$p 显示第一次出现main的行的上一行到最后一行
5,?int?d 删除5到最后一次出现int的行
插入、删除、撤销
na 从n行后添加
ni 从n行前插入。i、a都以行首.来结束
m,nd 删除m至n行
u 撤销所做的编辑
替换
s/old/new/ 把当前行中第一个old替换为new
s/old/new/g 把当前行中每一个old都替换为new
1,$s/old/new/g 把文件中所有的old都替换为new
速记符&
1,$s/big/very &/g 把文件中的big都替换为very big
s/and/\&/ 把and改为&
ed的正则表达式
c 普通字符,与自己匹配,如a、b等
\c 取消字符c的特殊意义,如\&
^ 在模式起始位置时,代表行首。如^a表示行首为a
$ 在模式结束位置时,代表行尾。
. 匹配任意单个字符。
[…] 匹配[]中任意单个字符,如[1ac],[a-z], [1-9]
[^…] 匹配不在…中的任意单个字符
r*,零个或多个重复的r,r为单个字符。如a*, [ab]*, .*
& 仅用于替换命令s的右边,代表第一个模式
模式举例
/^$/ 空行
/./ 非空行
/^/ 任意行
/thing/ 包含字符串thing的行
/^thing/ 以thing开始的行
/thing$/ 以thing结尾的行
/^thing$/ 内容为thing的行
/thing.$/ 以thing加任意符号结尾的行
/thing\.$/ 以thing.结尾的行
/\/thing\// 包含/thing/的行
/[tT]hing/ 包含thing或Thing的行
/thing[0-9]/
/thing[^0-9]/
/thing[0-9][^0-9]/
/thing1.*thing2/
/^thing1.*thing2$/
全局命令
命令格式:m,ng/re/cmd,含义是从m行到n行中对于匹配re模式的行执行命令cmd. 如果作用范围是全文件(1,$),m、n可以省略。
g/…/p 显示所有包含…的行
g/…/d 删除所有包含…的行
g/…/s//rep1/ 对于包含…的行,将其中第一个…替换成rep1
g/…/s//rep1/g 把所有的…替换成rep1
g/…/s/pat/rep1/ 对于包含…的行,将其中第一个pat替换成rep1
v/^$/p 打印所有非空行
移动、复制
命令格式:m,nmd m到n行移到d行之后。
m,ntd m到n行拷贝到d行之后
g/^/m0 逆序
sed
语法:sed [-opt] 'ed command1;ed command2' file
sed从输入文件中依次读取每一行,按指定的ed命令处理,将结果送至标准输出,但不改变file本身。
who | sed 's/ .*//' # 显示当前登录用户,只显示用户名。
sed '10q' file # 显示file文件的前10行
sed '/pattern/d' file # 不显示包含pattern的行。
sed -n '/pattern/p' file # 仅显示包含pattern的行。sed默认显示每一行,-n选项关闭自动显示功能。
ls -l | sed -n '/^d/p' # 列出当前目录下的子目录
grep
语法:grep 'ed pattern' file
grep '^void' sig.c # 显示sig.c文件中以void开头的行
ls -l | grep '^d' # 列出当前目录下的子目录
ls -l | grep '^d' | sed 's/.* //' # 列出当前子目录名
awk
基本语法
awk 'pattern{action} pattern{action}' <file>
awk '/pattern/' file # 显示匹配模式的每一行,功能同grep
awk '{print}' # 显示每一行,功能同cat
awk '//{print}/main/{print"----------------------------"}' sig.c
常量
NR # 当前行数(记录数)
NF # 一行的字段数(单词数)
$1、$2... # 第n个字段
$0 # 整行
awk '{print NR,NF,$0}' file
ls -l | awk '/^d/{print}' # 列出当前目录下的子目录
ls -l | awk '{print NR,":"$1,$8}' # (,相当于输出一个空格)
awk '{printf("%d: %s\n",NR,$0)}' file
表达式
< <= > >= != == # 关系运算符
~ !~ # 匹配正则表达式和不匹配正则表达式
+ - * / % # 加,减,乘,除与求余
|| && # 逻辑或,逻辑与
ls -l | awk '$5~/.....*/' # 列出长度超过999字节的文件
ls -l | awk '$5=="4096"' # 列出长度为4096字节的文件
ls -l | awk 'length($9)>=5' # 列出文件名长度大于等于5的文件
特殊pattern
awk 'BEGIN{初始化动作}'
awk 'END{结束动作}'
变量和运算
awk '{
nw += NF;
nc +=length($0)+1;
}
END{
print NR, nw,nc;
}' file # 与wc命令的功能完全一样
awk 'NF>0{
if ($1==lastword)
printf("double %s, line %d:\n%s\n", $1,NR,$0);
for (i=2; i<=NF; i++)
if ($i==$(i-1))
printf("double %s, line %d:\n%s\n", $i,NR,$0);
lastword=$NF;
} ' file # 寻找文件中相同的相邻单词
流程控制
if,for,while都与C语言相同
break:跳出循环
continue:下一个循环
next:下一条记录,回到awk程序开始
exit:跳转至END模式或结束
内部变量
FILENAME # 当前输入文件名
RS # 输入记录的分割符(默认为换行符)
FS # 输入字段的分隔符(默认为空格、制表符)
NF # 当前记录的字段数
NR # 当前记录数
OFS # 输出字段的分隔符(默认为空格)
ORS # 输出记录的分隔符(默认为换行)
内部函数
cos(expr) # 求余弦
exp(expr) # 求自然指数
index(s1,s2) # 是否字符串s2位于s1中
int(expr) # 取整
length(s) # 求字符串长度
log(expr) # 求自然对数
sin(expr) # 求正玄
split(s, a, c) # 按分隔符c将s放入a[1],a[2],…中
substr(s,m,n) # 求s的子串,从第m个字符开始,共n个字符。
数组
awk '{line[NR] = $0}
END{
for (i=1; i<=NR; i++) print line[i]
}' file
shell编程
参数重置与移动
# set命令可以重置除$0外所有参数。
set b1 b2
echo $0 $1 $2 $3 $4 # $1 $2被重置为b1 b2
# shift命令可以左移除$0以外的位置参数。
shift
echo $0 $1 $2 $3 $4 # $1变成原来的$2,以此类推
shift 2
echo $0 $1 $2 $3 $4 # 承接上一个shift,再左移两位,$1变成原来的$4,以此类推
特殊符号含义
* # 匹配文件名中任意字符串
? # 匹配文件名中任意单个字符
[ccc] # 匹配文件名中单个ccc中的字符。ccc可以指定范围,如0-9,a-z等
; # 命令结束符。p1;p2,先执行p1,再执行p2
& # 后台命令结束符。不等命令结束,立即接受新的命令
`…` # 执行命令…,用执行后的标准输出代替… 例如$ echo `date`
(…) # 在子shell里运行括号里的命令
$1, $2, … # 位置参数
$var # 引用变量var的值
${var} # 同上,在可能引起歧义时,使用{}将变量名括起来。
'…' # 单引号,对…中的特殊字符不作解释
"…" # 双引号,对…中的特殊字符仅解释$\``
var=value # 对变量var赋值
P1 && p2 # 运行p1,若成功,运行p2
P1 || p2 # 运行p1,若不成功,运行p2
$# # 参数个数
$* # 命令行参数集合
$@ # 命令行参数集合 //与$*有细微差别
$? # 最后一条命令的返回值
$$ # 当前shell的进程号
$! # 最后一个后台命令的进程号
$HOME $PATH $PS1 $PS2
test
常用于if、while、until命令中的条件判断
数值测试:
-eq # 等于则为真
-ne # 不等于则为真
-gt # 大于则为真
-ge # 大于等于则为真
-lt # 小于则为真
-le # 小于等于则为真
字符串测试:
= # 等于则为真
!= # 不相等则为真
-z # 字符串 字符串的长度为零则为真
-n # 字符串 字符串的长度不为零则为真
文件测试:
-e 文件名 # 如果文件存在则为真
-r 文件名 # 如果文件存在且可读则为真
-w 文件名 # 如果文件存在且可写则为真
-x 文件名 # 如果文件存在且可执行则为真
-s 文件名 # 如果文件存在且至少有一个字符则为真
-d 文件名 # 如果文件存在且为目录则为真
-f 文件名 # 如果文件存在且为普通文件则为真
-c 文件名 # 如果文件存在且为字符型特殊文件则为真
-b 文件名 # 如果文件存在且为块特殊文件则为真
其他:
–a # 与
-o # 或
! # 非
test f1 –nt f2 #文件f1比文件f2新
test f1 –ot f2 #文件f1比文件f2旧
if
if list # list:命令序列
then # 必须换行
list
[elif list
then
list]
[else list]
fi
# 例1
if gcc ./hello.c
then
echo good program!
else
echo bad program!
fi
# 例2:判断参数个数
if test $# -eq 5
then
echo there are 5 argus
elif test $# -gt 5
then
echo more than 5 argus
else
echo less than 5 argus
fi
case
case 值 in
模式1)
command1
command2
...
commandN
;;
模式2)
command1
command2
...
commandN
;;
esac
例:
case $# in
5)
echo there are 5 argus;;
[0-4])
echo less than 5 argus;;
*)
echo more than 5 argus
esac
for
for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done
或
for var in item1 item2 ... itemN; do command1; command2… done;
或
for ((e1;e2;e3)) # 仅bash
do
list
done
例1:
for i in $*
do
echo $i
done
例2:
j=0
for i in $*
do
a[$j]=$i
let j=$j+1
done
for ((i=$#-1; i>=0; i--))
do
echo ${a[i]}
done
while
while condition
do
command
done
例:
int=1
while(( $int<=5 ))
do
echo $int
let "int++"
done
until
# until 循环执行一系列命令直至条件为 true 时停止。until 循环与 while 循环在处理方式上刚好相反。
until condition
do
command
done
系统调用
文件IO
文件描述符
一个非负整数,一个结构数组的下标,进程打开的文件表项的下标。open、creat函数会返回一个文件描述符。
文件描述符0、1、2默认打开,分别对应于标准输入(键盘)、标准输出(显示器)、标准错误输出(显示器)文件。在<unistd.h>中定义为STDIN_FILENO
、STDOUT_FILENO
、STDERR_FILENO
。
open
#include <fcntl.h>
int open(const char *pathname, int oflag, [mode_t mode])
// 返回值:若成功,返回非负整数,即文件描述符。一定是当前“进程文件描述符表”中最小未使用的描述符。出错返回-1。
// pathname,常量,文件名,绝对路径或相对路径均可。
// oflag,打开方式选项。O_RDONLY、O_WRONLY、O_RDWR三者必须选其一。O_CREAT、O_APPEND、O_TRUNC等任意选择。多个选项进行“或”运算构成oflag选项。
/*
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容(即将其长度截短为0)
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O
*/
// mode,仅当oflag具有O_CREAT选项时,需要此参数,用于指定新建文件的访问权限。
creat
#include <fcntl.h>
int creat(const char *pathname, mode_t mode)
// 返回值:若成功,返回非负整数,即文件描述符。出错返回-1
// pathname,常量,文件名,绝对路径或相对路径均可。
// mode,指定新建文件的访问权限。
// 若原有文件存在,则原有文件的属性和内容将会被覆盖。等价于:
open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode)
// 语义上creat简单,涉及原子操作,应当使用open。
close
#include <unistd.h>
int close(int filedes)
// 返回值:若成功,返回0,出错返回-1
// filedes,文件描述符
// 进程终止时,内核自动关闭其打开的所有文件
lseek
// 移动文件“读写指针”(或称“文件偏移量”)。读写操作会自动移动文件读写指针。
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence)
// off_t 与系统有关,通常是长整型
// filedes 文件描述符
// whence 移动偏移量的方式。SEEK_SET/0:绝对方式;SEEK_CUR/1:相对方式;SEEK_END/2:相对文件尾部
// offset 移动的距离,非绝对方式移动时,可以为负。
// 返回值:新的文件偏移量。
read
// 从已打开的文件中读取数据,自动移动文件读写指针。
#include <unistd.h>
ssize_t read(int filedes, void *buff, size_t nbytes)
// ssize_t 通常是整型,size_t通常是无符号整型。
// filedes 文件描述符
// buff 存放数据的缓冲区
// nbytes 需要读取的字节数
// 返回值:实际读取的字节数。正常读取时,返回值等于nbytes;遇到文件结束时,小于nbytes;出错返回-1
write
// 将数据写入已打开的文件,自动移动文件读写指针。
#include <unistd.h>
ssize_t write(int filedes, void *buff, size_t nbytes)
// ssize_t 通常是整型,size_t通常是无符号整型。
// filedes 文件描述符
// buff 存放数据的缓冲区
// nbytes 需要写入的字节数
// 返回值:实际写入的字节数。正常写入时,返回值等于nbytes;磁盘空间满时,小于nbytes(这种情况也可以认为是出错);出错返回-1
dup2
#include <unistd.h>
int dup2(int filedes1, int filedes2)
// 将文件描述符filedes1的表项复制给filedes2的表项。如果filedes2已经打开,则先将其关闭。
// 返回值:正常返回filedes2,出错返回-1
原子操作
// 例1 多个进程添写同一个日志文件
// 有问题的代码:
lseek(fd, 0L, 2)
write(fd, buf, nbytes) // 如果从这两条代码中间插入其他代码,就会出现覆盖的情况
// 正确的做法:
open时,使用O_APPEND选项
write时,不需要调用lseek,直接写。
//例2 多个进程创建可能重名的临时文件
// 有问题的代码:
if (open(tmpfile, O_WRONLY)<0)
creat(tmpfile, mode)
// 正确的做法:
open(tmpfile, O_CREAT|O_WRONLY|O_TRUNC, mode)
最好的做法是保证没有重名的临时文件。
文件属性和目录
文件结构
struct stat
{
mode _t st_mode; //文件类型和权限
ino_t st_ino; //i节点号
dev_t st_dev; //文件系统设备号(磁盘和分区)
dev_t st_rdev; //设备文件的设备号
nlink_t st_nlink; //链接数
uid_t st_uid; //文件所有者的用户ID
gid_t st_gid; //文件所有者的组ID
off_t st_size; //文件长度(字节数),普通文件
time_t st_atime; //最后一次访问的时间
time_t st_mtime; //最后一次修改文件内容的时间
time_t st_ctime; //最后一次修改文件属性的时间
}
// 相关函数
#include <sys/stat.h>
int stat(const char *pathname, struct stat *buf)
int fstat(int filedes, struct stat *buf)
文件类型
类型:普通文件、目录文件、字符设备文件、块设备文件、 FIFO、符号链接、套接字
// 判断文件类型
S_ISREG(mode_t mode) //是否普通文件
S_ISDIR(mode_t mode) //是否目录文件
S_ISCHR(mode_t mode) //是否字符设备文件
S_ISBLK(mode_t mode) //是否块设备文件
S_ISFIFO(mode_t mode) //是否管道文件
S_ISLNK(mode_t mode) //是否符号链接
S_SOCK(mode_t mode) //是否套接字
文件的权限
// 9个普通权限位
S_IRUSR , S_IWUSR , S_IXUSR //用户权限位
S_IRGRP, S_IWGRP, S_IXGRP //用户组权限位
S_IROTH, S_IWOTH, S_IXOTH //其他用户权限位
// 3个特殊权限位(针对可执行文件)
S_ISUID 执行时设置有效用户ID,如passwd命令文件
S_ISGID 执行时设置有效用户组ID
S_SVTX 第一次执行时,保存正文,即常驻内存。
// 相关函数
int chmod(const char *pathname, mode_t mode)
int fchmod(int filedes, mode_t mode)
常用函数
// 普通文件操作函数
# include <unistd.h>
int link(const char *existingpath, const char *newpath) //创建一个新的目录项newpath,指向一个现有的文件existingpath。
int unlink(const char *pathname) //删除一个目录项,对应文件的链接数减1。
#include <stdio.h>
int remove(const char *pathname) //删除一个文件或目录的链接。
int rename(const char *oldname, const char *newname) //文件或目录更名
// 目录文件操作函数
# include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode) //创建一个目录
# include <unistd.h>
int rmdir(const char *pathname) //删除一个空目录。
// 目录文件读函数(只有内核才可以写)
#include <dirent.h>
DIR *opendir(const char *pathname)
struct dirent *readdir(DIR *dp)
int closedir(DIR *dp)
void seekdir(DIR *dp, long loc)
//位置函数
//改变进程的当前工作目录(cd命令)
# include <unistd.h>
int chdir(const char *pathname)
int fchdir(int filedes)
//获取当前工作目录(pwd命令)
#include <unistd.h>
char *getcwd(char *buff, size_t size)
进程控制
进程ID(进程号)
#include <unistd.h>
pid_t getpid(void) // 获取调用者的进程ID
pid_t getppid(void) // 获取调用者的父进程ID。
创建进程(fork)
#include <unistd.h>
pid_t fork(void)
// 功能:创建一个新的进程,新进程是旧进程的副本。旧进程叫父进程,新进程叫子进程。
// 返回值:fork函数调用一次,返回两次。在父进程中返回子进程的ID,在子进程中返回0,出错返回-1
// 以下是一个例子:
pid = fork(); //创建一个子进程
if(pid < 0) //判断子进程是否创建成功
{
printf("子进程创建失败,很遗憾!\n");
exit(1);
}
//走到这里,fork()成功。现在,父进程和子进程同时开始运行了
//执行后续代码的可能是父进程,也可能是子进程
if(pid == 0)
{
//子进程,因为子进程的fork()返回值会是0;
//这里专门针对子进程的处理代码
......
}
else
{
//这里就是父进程,因为父进程的fork()返回值会 > 0(实际返回的是子进id程)
//这是专门针对父进程的处理代码
......
}
执行程序(exec)
// 父进程fork一个子进程后,子进程往往需要调用一个exec函数来运行一个新程序。
// exec函数用一个新程序替换调用进程原有的代码、数据、堆栈等,新程序从头开始执行。exec不产生新的进程,所有调用前后的进程号不发生变化。
// exec函数:
#include <unistd.h>
int execl(const char *pathname, const char *arg, 参数列表, NULL)
int execv(const char *pathname, char *const argv[])
int execle(const char *pathname, const char *arg, 参数列表, NULL, char *const envp[])
int execve(const char *pathname, char *const argv[], char *const envp[])
int execlp(const char *filename, const char *arg0, 参数列表, NULL)
int execvp(const char *filename, char *const argv[])
/*
说明:
1) execl和execlp比较直观,类似命令行输入,推荐使用。
2)execve是系统调用,其他是库函数。
3)返回值:正常不返回,出错返回-1
*/
// 例子:
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execv("/bin/ps", ps_argv);
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
execve("/bin/ps", ps_argv, ps_envp);
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execvp("ps", ps_argv);
文件共享
fork之后,所有父进程打开的文件描述符都被复制到子进程中。父子进程相同的文件描述符指向相同的内核文件表,具有相同的文件偏移量。如果父子进程同时操作相同的文件,则需要同步机制,否则会产生混乱。一般情况下,应该尽量回避父子进程同时操作相同文件的情况。
- 父进程等待子进程结束后再运行
- 父子进程运行不同的程序段,使用不同的文件。
进程结束
- 正常结束:main函数执行完;main函数中执行return;任意位置执行exit、_exit、_Exit
- 非正常结束:收到某个信号而结束
- 无论进程如何结束,都会执行内核中的一段代码:关闭所有打开文件,释放占用的内存。
- 进程结束时,内核会保存其终止状态,直到该进程的父进程取走其状态。
- 父进程结束,其子进程成为孤儿进程,由进程1(init进程)领养。
- 子进程先于父进程结束,如果父进程未取走其状态,则该进程仍然占有一定的内核资源,成为“僵死进程”。
- 由于僵死进程占用资源,因此当一个长期运行的服务类程序调用fork后,应处理僵死进程。
wait
#include <sys/wait.h>
pid_t wait(int *status)
/*
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
*/
// 例子:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
pid_t pc,pr;
pc=fork();
if(pc<0) /* 如果出错 */
printf("error ocurred!\n");
else if(pc==0){ /* 如果是子进程 */
printf("This is child process with pid of %d\n",getpid());
sleep(5); /* 睡眠5秒钟 */
}
else{ /* 如果是父进程 */
pr=wait(NULL); /* 在这里等待 */
printf("I catched a child process with pid of %d\n",pr);
}
exit(0);
}
// 其他函数
pid_t waitpid(pid_t pid, int *stat, int options)
waitid、wait3、wait4等
进程间通信(IPC)
进程间通信包括:半双工管道、半双工命名管道(FIFO)、全双工管道、全双工命名管道、消息队列、信号量、共享存储、套接字、STREAMS。
主机内通信常用半双工管道,网络通信常用套接字。
半双工管道
所有UNIX系统都提供的一种通信方式。通信方向是双向的,但只能选择其中一种。只能在具有公共祖先的进程之间使用,通常是父子进程之间。
#include <unistd.h>
int pipe(int filedes[2])
// 创建一个管道,若成功返回0,不成功返回-1;
// 由参数filesdes返回两个文件描述符: filedes[0]、filesdes[1],filedes[0]为读而打开,filedes[1]为写而打开。
// (单个进程)创建管道之后,将数据写入fd[1],从fd[0]中读出。
// fork之后,选择通信方向,如果父进程写、子进程读,则父进程close fd[0],子进程close fd[1];反之,子进程关闭fd[0],父进程关闭fd[1]。
// 规则:(1)写端关闭时,read函数返回0,表示文件结束;(2)读端关闭时,write函数返回-1,并且出现SIGPIPE异常(信号)。
// 常量PIPE_BUF规定了内核管道缓冲区大小,每次write的字节数需小于PIPE_BUF
// 例1(父进程写,子进程读):
int fd[2]; char line[MAXLINE];
pipe(fd);
if (pid = fork() > 0) //父进程
{
close(fd[0]);
write(fd[1], "hello world!\n", 12);
}
else if (pid == 0) //子进程
{
close(fd[1]);
read(fd[0], line, MAXLINE);
}
// 例2(用管道连接父子进程的标准输出和输入):
if (pid = fork() > 0) //父进程
{
close(fd[0]);
dup2(fd[1], 1);
}
else if (pid=0) //子进程
{
close(fd[1]);
dup2(fd[0],0);
execlp(……)
}
...... //父进程代码
命名管道(FIFO)
# 命名管道是一种文件
$ mkfifo f1
$ ls -l f1
$ cat < f1 # 在终端1中接收管道内容
$ ls -l > f1 # 在终端2中向管道输入,此时终端1会输出结果
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode)
信号
信号是一种异步事件,通常是非正常情况,如:人为终止进程、除数为0、非法内存访问等。所谓“异步”,是指信号的出现是随机的。
信号的产生
- 人为按下中断键 Ctrl_C
- 用户使用kill命令
- 进程调用kill函数
- 程序运行错误,除数为0、非法内存访问
- 软件产生的信号:闹钟超时、写一个读端已关闭的管道、网络传来带外数据。
信号的处理
- 系统默认处理或忽略
- 捕捉信号:通知内核在某种信号发生时调用一个用户函数,如下
signal
#inclucde <signal.h>
void (*singnal(int sig, void (*func)(int)))(int)
/* 理解
void (*func)(int) 一个函数指针,所指向的函数需要一个整型参数,无返回值。
signal(int sig, void (*func)(int)), signal函数有两个参数,一个整型,一个函数指针。
void (*signal(…))(int) signal的返回值也是一个函数指针,所指向的函数需要一个整型参数,无返回值。
令Sigfunc代表一个无返回值有1个int参数的函数
所以定义可以简化成:
Sigfunc* signal(int, Sigfunc*)
*/
/* sig的取值
SIGABRT (Signal Abort) 程序异常终止。
SIGFPE (Signal Floating-Point Exception) 算术运算出错,如除数为 0 或溢出(不一定是浮点运算)。
SIGILL (Signal Illegal Instruction) 非法函数映象,如非法指令,通常是由于代码中的某个变体或者尝试执行数据导致的。
SIGINT (Signal Interrupt) 中断信号,如 ctrl-C,通常由用户生成。
SIGSEGV (Signal Segmentation Violation) 非法访问存储器,如访问不存在的内存单元。
SIGTERM (Signal Terminate) 发送给本程序的终止请求信号。
*/
// 例1:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void sighandler(int);
int main()
{
signal(SIGINT, sighandler);
while(1)
{
printf("开始休眠一秒钟...\n");
sleep(1);
}
return 0;
}
void sighandler(int signum) // 这个参数的意思是错误的类型,即sig的取值
{
printf("捕获信号 %d,跳出...\n", signum);
exit(1);
}
// 例2(捕捉多个信号):
signal(SIGINT, my_sig);
signal(SIGALRM, my_sig);
void my_sig(int signo)
{
if (signo == SIGINT)
{
......
}
else if(signo == SIGALRM)
{
......
}
}
alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
// 按秒设置下一次闹钟的时间。
// 返回值:如果以前设置过闹钟,再次设置时返回上一次设置时间的余数;否则返回0。
// alarm函数设置的时间到达时,产生SIGALRM信号。该信号的默认动作是终止程序。
其他注意事项
进程控制中的信号处理 - fork时,子进程继承父进程的信号处理。 - exec时,恢复系统默认设置。
重入问题 - 信号处理程序可以被中断,中断时可能导致信号处理程序重新进入(再次被调用)。 - printf不可重入,大多数标准IO库函数不可重入。 - 信号处理程序应尽可能简单,尽可能不用库函数。