Unix程序设计课堂知识点整理
Linux编程基础
[TOC]
1.1 从Unix到Linux
为了提升UNICS系统的性能与兼容性,采用高级语言对其进行重构,并确定该操作系统名称为UNIX,这就是最早的 UNIX 操作系统(相对于 Multics ,UNIX 具有单一的意思)
GNU通用公共许可协议(GNU GPL)是一个广泛被使用的自由软件许可协议条款,最初由Stallman为GNU计划而撰写,GPL授予程序接受人以下权利,或称“自由”:
⚫ 以任何目的运行此程序的自由;
⚫ 再发行复制件的自由;
⚫ 改进此程序,并公开发布改进的自由
1.2 Linux概述
Linux是一个类Unix(Unix-like)的操作系统,在1991年发行了它的第一个版本
1991年11月,芬兰赫尔辛基大学的学生 Linus Torvalds写了个小程序,取名为Linux,放在互联网上。
1993,在一批高水平黑客的参与下,诞生了Linux 1.0 版
1994年,Linux 的第一个商业发行版 Slackware 问世
1996年,美国国家标准技术局的计算机系统实验室确认 Linux 版本 1.2.13(由 Open Linux 公司打包)符合 POSIX 标准
1.3 GNU & Linux
1.4 Linux 内核
Linux内核采用的是双树系统
一棵是稳定树,主要用于发行
另一棵是非稳定树或称为开发树,用于产品开发和改进
Linux内核版本号由3位数字组成
2.2 Vi编辑器使用
1.vi的工作模式
输入模式:输入字符为命令,可进行删除、修改、存盘等操作 。
命令模式:输入字符作为文本内容。
末行模式:命令模式下输入“:/?”三个中任意一个,可移到屏幕最底一行。
(1)命令模式
输入模式下,按ESC可切换到命令模式,常用命令:
:q! | 离开vi,并放弃刚在缓冲区内编辑的内容 |
---|---|
:wq | 将缓冲区内的资料写入磁盘中,并离开vi |
:ZZ | 同wq |
:x | 同wq |
:w | 将缓冲区内的资料写入磁盘中,但并不离开vi |
:q | 离开vi,若文件被修改过,则要被要求确认是否放弃修改的内容,此指令可与:w配合使用 |
(2)输入模式
输入以下命令即可进入vi输入模式
a(append) | 在光标之后加入资料 |
---|---|
A | 在该行之末加入资料 |
i(insert) | 在光标之前加入资料 |
I | 在该行之首加入资料 |
o(open) | 新增一行于该行之下,供输入资料用 |
O | 新增一行于该行之上,供输入资料用 |
dd | 删除当前光标所在行 |
x | 删除当前光标字符 |
X | 删除当前光标之前字符 |
U | 撤消 |
F | 查找 |
ESC | 离开输入模式 |
2.Vi其他功能命令
(1)复制粘贴
yw | 将光标所在之处到字尾的字符复制到缓冲区 |
---|---|
yy | 复制光标所在行到缓冲区 |
#yy | 如:6yy表示拷贝从光标所在行往下数6行文字 |
p | 将缓冲区内的字符贴到光标所在位置 |
(2)查找/替换
?字符串 | 从当前光标位置开始向后查找字符串 |
---|---|
/字符串 | 从当前光标位置开始向前查找字符串 |
n | 继续上一次查找 |
Shift+n | 以相反的方向继续上一次查找 |
(3)环境设置
:set ai | 自动缩进,每一行开头都与上一行的开头对齐 |
---|---|
:set nu | 编辑时显示行号 |
:set dir=./ | 将交换文件.swp保存在当前目录 |
:set sw=4 | 设置缩进的字符数为4 |
:syntax on 或者 :syntax=on | 开启语法着色 |
3.1 GCC编译器介绍
GCC是一个强大的工具集合,它包含了预处理器、编译器、汇编器、链接器等组件。它会在需要的时候调用其他组件。
输入文件的类型和传递给gcc的参数决定了gcc调用具体的哪些组件。
GCC 参数选项
Usage:
gcc [options] [filename]
Basic options:
-E: | 只对源程序进行预处理(调用cpp预处理器) |
---|---|
-S: | 只对源程序进行预处理、编译 |
-c: | 执行预处理、编译、汇编而不链接 |
-o: | output_file: 指定输出文件名 |
-g: | 产生调试工具必需的符号信息 |
-O/On: | 在程序编译、链接过程中进行优化处理 |
-Wall: | 显示所有的警告信息 |
-I dir: | 在头文件的搜索路径中添加dir目录 |
-L dir : | 在库文件的搜索路径列表中添加dir目录 |
3.2 GCC编译过程
1、预处理
2、编译成汇编代码
3、汇编成目标代码
4、链接
1.预处理
预处理:使用-E参数
gcc –E –o gcctest.i gcctest.c
使用wc命令比较预处理后的文件与源文件,可以看到两个文件的差异
2.编译成汇编代码
预处理文件—->汇编代码
使用-S说明生成汇编代码后停止工作
gcc –S –o gcctest.s gcctest.i
直接编译到汇编代码
gcc –S gcctest.c
3.编译成目标代码
汇编代码à目标代码
gcc –x assembler –c gcctest.s
直接编译成目标代码
gcc –c gcctest.c
使用汇编器生成目标代码
as –o gcctest.o gcctest.s
4.编译成执行代码
目标代码à执行代码
gcc –o gcctest gcctest.o
直接生成执行代码
gcc –o gcctest gcctest.c
3.3 GCC编译优化
优化编译选项有:
-O0
缺省情况,不优化
-O1
-O2
-O3
等等
3.4 头文件和库函数目录
1. GCC –I dir 参数使用
头文件和gcc不在同一目录下,用 –I dir指明头文件所在的目录。
#include <>:在默认路径“/usr/include”中搜索头文件
#include “”:在本目录中搜索
解决办法:
gcc opt.c –o opt –I ./
修改main
1
2
3
2. GCC创建函数库
函数库:公用函数定义为函数库,供其他程序使用。函数库分为静态库和动态库。
静态库:程序编译时会链接到目标代码中,程序运行时不再需要静态库。程序生成的可执行程序比较大。后缀名为“.a”
动态库:程序编译时不会链接到目标代码,在程序运行时载入,运行时需要动态库存在。动态库可方便多个程序共享一个函数库。后缀名为”.so”
函数库的生成:由编译过的.o文件生成。
创建静态库:
1.将需要生成函数库的函数执行gcc –c,生成.o文件
gcc –c hello.c
2.由.o文件创建静态库,静态库命名格式为:lib静态库名.a
ar -rv libmyhello.a hello.o
使用静态库:在调用静态库的程序编译时指定静态库名
$gcc –o hello main.c –L. –lmyhello
$./hello
创建动态库:
1.由.o文件生成动态库,动态库的命名:lib动态库名.so
1 | gcc –shared –fPIC –o libmyhello.so hello.o |
2.使用动态库:用gcc命令指定动态库名进行编译,编译之前需将动态库文件复制到系统默认库函数目录/usr/lib中或者设置搜索路径。 sudo ldconfig
1 | gcc –o hello main.c –L. –lmyhello |
gdb commands
Gdb using
$gdb filename
gdb将装入名为filename的可执行文件。
在编译时需要使用-g选项
list
help
设置断点 break
显示断点信息 info breakpoints
清除已经定义的断点 clear
delete [bkpoints-num]删除指定/全部断点
print [bkpoints-num] 输出断点处变量的值
quit:退出gdb
实例
调试子命令总结
file :装入想要调试的可执行文件
kill:终止正在调试的程序
list:列出正在执行的程序清单
next:执行一行代码但不进入函数内部
step:执行一行代码并进入函数内部
run: 执行当前正在调试的程序
quit:终止gdb调试
break:设置断点 (break 行号)
watch:设置观察点,观察表达式的值是否发
生变化
info: 查看断点信息
info breakpoints、info watchpoints
info break 显示当前断点清单,包括到达
断点处的次数等。
info files 显示被调试文件的详细信息。
info func 显示所有的函数名称。
info local 显示当函数中的局部变量信息。
info prog 显示被调试程序的执行状态。
info var 显示所有的全局和静态变量名称
delete: 删除某个或所有的断点
delete 断点号 或 delete
disable: 使断点失效(但仍存在)
enable: 使断点有效
clear: 清除断点信息
clear 断点所在行号
clear 函数入口
continue: 继续执行程序直到程序结束
5.1 Make 引入
Make的引入:
Ø文件数量太大,手工gcc编译不方便
Ø仅需要编译已经做了修改的源代码文件;其他文件只需要重新连接
Ø记录哪些文件已改变且需要编译,哪些文件仅仅需要连接很难
make & makefile
Multi-file project
IDE—Eclipse
make
make & makefile
makefile描述模块间的依赖关系;
make命令根据makefile对程序进行管理和维护;make判断被维护文件的时序关系
make
make [-f filename] [targetname]
使用方法:
v make 自动找当前目录下名为Makefile/makefile的文件
v make –f 文件名 找当前目录下指定文件名的文件
makefile的组成
- 显式规则:明确指出目标文件的生成规则
- 隐式规则:需要make自动推导的规则
- 变量定义:声明时赋值,引用时加”$”
- 文件指示:引用外部的文件
- 注释:#
Makefile的显式规则
规则
一条规则包含3个方面的内容,
1)要创建的目标(文件),
2)创建目标(文件)所依赖的文件列表;
3)通过依赖文件创建目标文件的命令组
规则一般形式
1 | *target* ... : *prerequisites* ... |
每条规则由一个带冒号的“依赖行”和一条或多条以tab开头的“命令行”组成
目标1 [目标2…]:[依赖文件列表]
[\t 命令]
…
ex:
1 | *make_test:make_main.o* *wrtlog.o* |
冒号左边是目标,冒号右边是依赖文件
目标和依赖文件均是由字母、数字、句点和斜杠组成的字符串
目标或依赖文件的数目多于一个时,以空格分隔
一个简单的makefile
1 | edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o |
Make的工作过程
default goal
在缺省的情况下,make从makefile中的第一个目标开始执行
Make的工作过程类似一次深度优先遍历过程
Makefile的变量
自定义变量
例:
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o exe $(object)
特殊变量
$@:表示目标文件;
$^:表示所有依赖目标的集合,以空格分隔;
$<:表示第一个依赖文件;
Makefile的隐式规则(自动推导)
make能够自动推导文件以及文件依赖关系后面的命令
例:
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)
main.o : def.h
Makefile的伪目标
lmakefile使用.PHONY关键字来定义一个伪目标,具体格式为:
.PHONY : 伪目标名称
例:
.PHONY : clean
clean :
rm $(object)
总结: C语言编程步骤
- 编辑:vi ,emacs,gedit,Eclipse…
- 编译: gcc
- 调试:gdb
- 运行: ./executable file
- 项目管理:make
扩展
gdb命令
命令 | 解释 | 示例 |
---|---|---|
file <文件名> | 加载被调试的可执行程序文件。 因为一般都在被调试程序所在目录下执行GDB,因而文本名不需要带路径。 | (gdb) file gdb-sample |
r | Run的简写,运行被调试的程序。 如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处。 | (gdb) r |
c | Continue的简写,继续执行被调试程序,直至下一个断点或程序结束。 | (gdb) c |
b <行号> b <函数名称> b *<函数名称> b *<代码地址>d [编号] | b: Breakpoint的简写,设置断点。两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置。 其中在函数名称前面加“*”符号表示将断点设置在“由编译器生成的prolog代码处”。如果不了解汇编,可以不予理会此用法。d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。 | (gdb) b 8 (gdb) b main (gdb) b *main (gdb) b *0x804835c(gdb) d |
s, n | s: 执行一行源程序代码,如果此行代码中有函数调用,则进入该函数; n: 执行一行源程序代码,此行代码中的函数调用也一并执行。s 相当于其它调试器中的“Step Into (单步跟踪进入)”; n 相当于其它调试器中的“Step Over (单步跟踪)”。这两个命令必须在有源代码调试信息的情况下才可以使用(GCC编译时使用“-g”参数)。 | (gdb) s (gdb) n |
si, ni | si命令类似于s命令,ni命令类似于n命令。所不同的是,这两个命令(si/ni)所针对的是汇编指令,而s/n针对的是源代码。 | (gdb) si (gdb) ni |
p <变量名称> | Print的简写,显示指定变量(临时变量或全局变量)的值。 | (gdb) p i (gdb) p nGlobalVar |
display …undisplay <编号> | display,设置程序中断后欲显示的数据及其格式。 例如,如果希望每次程序中断后可以看到即将被执行的下一条汇编指令,可以使用命令 “display /i pc”其中pc”其中pc 代表当前汇编指令,/i 表示以十六进行显示。当需要关心汇编代码时,此命令相当有用。undispaly,取消先前的display设置,编号从1开始递增。 | (gdb) display /i $pc(gdb) undisplay 1 |
i | Info的简写,用于显示各类信息,详情请查阅“help i”。 | (gdb) i r |
q | Quit的简写,退出GDB调试环境。 | (gdb) q |
help [命令名称] | GDB帮助命令,提供对GDB名种命令的解释说明。 如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。 | (gdb) help display |
.o文件是二进制文件
雨课堂题目整理
文件I/O
1.1 文件属性
- 文件属性数据结构struct stat ——文件控制块
1 | struct stat { |
命令查看:ls -l file
- stat/fstat/lstat函数
获取文件属性
1 |
|
1.2 文件类型
**1.**文件类型
Unix/Linux系统支持的文件类型:
- Directory(d):目录文件
- Link(l):链接文件
- Pipe(p):管道文件
- Block Device(b):块设备文件
- Character Device(c):字符设备文件
- Regular(-):普通文件
- Socket(s):套接字文件
查看文件类型
使用命令:ls –l /dev/sda1
例2.1设计一个程序,要求列出当前目录下的文件信息,以及系统“/dev/sda1”和“/dev/lp0”的文件信息。
1 |
|
**2.**获取文件类型
st_mode存储文件类型和许可权限,形式如下:
type3 type2 type1 type0 suid sgid sticky rwx rwx rwx
用来确定文件类型的宏:
S_ISBLK – 测试块文件
S_ISCHR – 测试字符文件
S_ISDIR – 测试目录
S_ISFIFO – 测试FIFO
S_ISREG – 测试普通文件
S_ISLNK – 测试符号链接
1 |
|
1.3 文件存取权限
**1.**文件存取权限
读的权限:显示目录文件,进入目录
写的权限:目录下创建文件
执行的权限:显示目录文件,进入目录,创建文件
**2.**改变文件存取权限——命令
例2.3 设计一个程序,要求把系统中“/home/mylinux目录下的myfile文件权限,设置成文件所有者可读可写,其他用户只读权限。
1 |
|
1 | chmod 函数 |
3. 改变文件存取权限
4. 默认文件存取权限——umask
新创建的文件和目录的默认权限是(root)
File: -rw-r–r– 644
Directory: drwxr-xr-x 755
Why?
umask: 包含未被设置为权限位的==八进制数字(即无x位(可执行位))==。默认002为普通用户,022为root用户。666-644=022
例2.4 设计一程序,要求设置系统文件和目录的权限掩码。
1 |
|
1 | umask 函数 |
1.4 文件其他属性
**1.chown/fchown/**lchown 函数
1 | 改变文件所有者 |
2.获取文件存取时间——stat
1 |
|
**3.**获取文件大小——stat
1 |
|
2.1 两种I/O方式
1. 无缓冲和缓冲I/O
无缓冲I/O
- read/write ->系统函数
- 文件描述符
- POSIX.1 and XPG3标准
缓冲 I/O
- 标准I/O库实现
- 处理很多细节, 如缓存分配, 以优化长度执行I/O等.
- 流 -> FILE类型指针
**2.**无缓冲 I/O 系统调用
基本 I/O
- open/creat, close, read, write, lseek
- dup/dup2
- fcntl
2.2 文件描述符
1.文件描述符概述
非负整数
int fd;
(in <unistd.h>)
STDIN_FILENO (0), STDOUT_FILENO (1), STDERR_FILENO (2)
文件操作一般步骤:
open-read/write-[lseek]-close
2. 进程打开文件的内核数据结构
2.3 无缓冲 I/O函数
**0.**错误处理
1 | UNIX方式 |
1. creat 函数
1 | create a file or device |
例2.7 **设计一程序,要求在“/**home”目录下创建一个名称为“2-7file”的文件,并且把此文件的权限设置为所有者具有只读权限,最后显示此文件的信息。
1 |
|
参数 “mode”
“mode”: 指定创建的新文件的存取权限
参数 “mode” & umask
umask: 一种文件保护机制
新建文件的初始存取权限
2. Open 函数
1 | Open and possibly create a file or device |
例2.8设计一个程序,要求在当前目录下以可读写方式打开一个名为“2-8file”的文件。如果该文件不存在,则创建此文件;如果存在,将文件清空后关闭。
1 |
|
参数 “flags”
1 | “flags”: 指定文件存取方式 |
3. close 函数
1 | Close a file descriptor |
4. read/write 函数
1 | Read from a file descriptor |
例2.9设计一个程序,完成文件的复制工作。要求通过read函数和write函数复制“/etc/passwd”文件到目标文件中,目标文件名在程序运行时从键盘输入。
1 |
|
5. lseek 函数
read/write 定位文件指针
1 |
|
该指令的“那里”:
SEEK_SET: the offset is set to “offset” bytes指针位移量为设定值
SEEK_CUR: the offset is set to its current location plus “offset” bytes指针位移量为当前位移加设定值
SEEK_END: the offset is set to the size of the file plus “offset” bytes指针位移量为文件尾加设定值
空洞文件
使用lseek修改文件偏移量后,当前文件偏移量有可能大于文件的长度
在这种情况下,对该文件的下一次写操作,将加长该文件
这样文件中形成了一个空洞。对空洞区域进行读,均返回0
6. dup/dup2 函数
1 | 复制文件描述符 |
假设进程已打开文件描述符0、1、2
调用dup2(1, 6),dup2返回值是多少?
然后再调用dup(6),dup返回值是多少?
7. fcntl函数
1 | 查看、修改打开的文件描述符 |
lcmd取值:
F_DUPFD 复制文件描述符
F_GETFD 获得文件描述符
F_SETFD 设置文件描述符
F_GETFL 获取文件描述符当前模式
F_SETFL设置文件描述符当前模式
F_GETLK 获得记录锁
F_SETLK 设置记录锁
1 | 分析以下程序运行情况 |
1 | 分析以下程序运行结果 |
3.1 标准I/O库
为什么要设计标准I/O库?
直接使用API进行文件访问时,需要考虑许多细节问题
例如:read、write时,缓冲区的大小该如何确定,才能使效率最优
标准I/O库封装了诸多细节问题,包括缓冲区分配
I/O效率示例
1 |
|
原因
- Linux文件系统采用了某种预读技术
- 当检测到正在进行顺序读取时,系统就试图读入比应用程序所要求的更多数据
- 并假设应用程序很快就会读这些数据
- 当BUFFSIZE增加到一定程度后,预读就停止了
标准 I/O 缓冲
标准I/O库提供缓冲的目的:尽可能减少使用read、write调用的次数,以提高I/O效率。
通过标准I/O库进行的读写操作,数据都会被放置在标准I/O库缓冲中中转。
3.2 文件流
1 | typedef struct { |
3.3 标准 I/O 函数
- 流 open/close
- 流 read/write
- 每次一个字符的I/O:fgetc,fputc
- 每次一行的I/O: fgets,fputs,gets,puts
- 直接I/O(二进制I/O): fread,fwrite
- 格式化I/O:scanf,printf,fscanf,fprintf
- 流定位:fseek,ftell,frewind
- 流刷新:fflush
1. 流打开/关闭
例2.10设计一个程序,要求用流文件I/O操作打开文件“2-10file”,如果该文件不存在,则创建文件。
1 |
|
1 | Open a stream |
**2.流读/**写
对流有三种读写方式
每次读写一个字符
每次读写一行
每次读写任意长度的内容
(1)输入/出一个字符
1 | 输入 |
(2) 输入/出一行
1 |
|
(3) 二进制流输入/输出
1 |
|
例2.11设计两个程序,要求一个程序把三个人的姓名和账号余额信息通过一次流文件I/O操作写入文件“2-11file”,另一个格式输出账号信息,把每个人的账号和余额一一对应显示输出。
1 | /*程序:把帐号信息从文件读出*/ |
(4) 格式化 I/O
1 |
|
3.流定位
1 |
|
例2.12 设计一程序,要求用fopen函数打开系统文件“/etc/passwd”,先把位置指针移动到第10个字符前,再把位置指针移动到文件尾,最后把位置指针移动到文件头,输出三次定位的文件偏移量的值。
1 |
|
4. 流刷新
刷新文件流。把流里的数据立刻写入文件—fork前使用fflush
#include <stdio.h>
int fflush(FILE *stream);
自动刷新:
- 流关闭fclose;
- exit终止;
- 行缓冲“\n”;
- 缓冲区满;
- 执行输入操作读文件:printf(“hello”);scanf(“%d”,&a)
5. 流缓冲
- 三种类型的缓冲
- 块(全)缓冲block buffered (fully buffered):一般C库函数写入文件是全缓冲的
- 行缓冲line buffered:引用标准交互设备的流stdin,stdout
例: for(i=1;i<=10;i++) fputc(c,stdout); fputc("\n",stdout);
- 无缓冲Unbuffered:标准错误流stderr
全缓冲
- 在填满标准I/O缓冲区后,才进行实际I/O操作(例如调用write函数)
- 调用fflush函数也能强制进行实际I/O操作
行缓冲
- 在输入和输出遇到换行符时,标准I/O库执行I/O操作
- 因为标准I/O库用来收集每一行的缓存的长度是固定的,所以,只要填满了缓存,即使没有遇到新行符,也进行I/O操作
- 终端(例如标准输入和标准输出),使用行缓冲
不带缓冲
- 标准I/O库不对字符进行缓冲存储
- 标准出错是不带缓冲的,为了让出错信息尽快显示出来
6. 流和文件描述符
确定流使用的底层文件描述符
#include <stdio.h>
int fileno(FILE *fp);
根据已打开的文件描述符创建一个流
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);
4.1 目录文件
mkdir/rmdir
chdir/fchdir, getcwd
读目录操作
opendir/closedir
readdir
telldir
seekdir
例2.13设计一程序,要求读取当前目录文件中所有的目录结构。
1 |
|
1. 读目录
数据结构
DIR, struct dirent
操作函数
opendir/closedir
readdir
telldir
seekdir
2. 数据结构
DIR
目录流对象的数据结构
in <dirent.h>
typedef struct __dirstream DIR;
struct dirent
目录项
Defined in <dirent.h>
ino_t d_ino; /* inode number /
char d_name[NAME_MAX + 1]; / file name */
3. 操作函数
目录的打开、关闭、读、定位
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
int closedir(DIR *dir);
struct dirent *readdir(DIR *dir);
off_t telldir(DIR *dir);
void seekdir(DIR *dir, off_t offset);
目录扫描程序——ls -R命令
1 | DIR *dp; |
mkdir/rmdir 函数
创建一个空目录
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
(Return: 0 if success; -1 if failure)
删除一个空目录
#include <unistd.h>
int rmdir(const char *pathname);
(Return: 0 if success; -1 if failure)
chdir/fchdir 函数
Change working directory
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
(Return: 0 if success; -1 if failure)
当前工作目录是进程的属性,所以该函数只影响调用chdir的进程本身
cd(1) command
4.2 链接文件
ln 命令
link/unlink 函数
给一个文件创建一个链接.
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
(Return: 0 if success; -1 if failure)
删除文件链接
#include <unistd.h>
int unlink(const char *pathname);
(Return: 0 if success; -1 if failure)
symlink/readlink
ln –s命令
创建一个符号链接
#include <unistd.h>
int symlink(const char *oldpath, const char *newpath);
(Return: 0 if success; -1 if failure)
读取符号链接的值
#include <unistd.h>
int readlink(const char *path, char *buf, size_t bufsiz);
(Return: the count of characters placed in the buffer if success; -1 if failure)
例2.14设计一程序,要求为“/etc/passwd”文件建立软链接“2-14link”,并查看此链接文件和“/etc/passwd”文件。
1 |
|
例2.15设计一程序,要求为“/etc/passwd”文件建立硬链接“2-15link”,并查看此链接文件和“/etc/passwd”文件。
1 |
|
4.3 设备文件
1.设备文件名
ls –C 列出当前系统加载的设备对应的文件
ls –li 列出当前终端设备的属性
2.设备文件读写
open,read,write,close,stat
例2-16 向终端pts1写入100个“unix”。
1 |
|
扩展
fseek()
C 库函数 int fseek(FILE *stream, long int offset, int whence) 设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。
声明
下面是 fseek() 函数的声明。
1 | int fseek(FILE *stream, long int offset, int whence) |
参数
- stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
- offset – 这是相对 whence 的偏移量,以字节为单位。
- whence – 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量 | 描述 |
---|---|
SEEK_SET | 文件的开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件的末尾 |
rewind()
将文件指针重新指向文件开头
readdir()
头文件:#include <sys/types.h> #include <dirent.h>
定义函数:struct dirent * readdir(DIR * dir);
函数说明:readdir()返回参数dir 目录流的下个目录进入点。结构dirent 定义如下:
struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};
返回值:成功则返回下个目录进入点. 有错误发生或读取到目录文件尾则返回NULL.
fdopen()
头文件:#include <stdio.h>
定义函数:FILE * fdopen(int fildes, const char * mode);
**函数说明:fdopen()会将参数fildes 的文件描述词, 转换为对应的文件指针后返回.参数mode 字符串则代表着文件指针的流形态, 此形态必须和原先文件描述词读写模式相同. 关于mode 字符串格式请参考fopen(). **
返回值:转换成功时返回指向该流的文件指针. 失败则返回NULL, 并把错误代码存在errno 中.
opendir()
头文件:#include <sys/types.h> #include <dirent.h>
函数:DIR *opendir(const char *name);
含义: opendir()用来打开参数name 指定的目录, 并返回DIR*形态的目录流, 和open()类似, 接下来对目录的读取和搜索都要使用此返回值.
readdir()
头文件:#include<sys/types.h> #include <dirent.h>
函数:struct dirent *readdir(DIR *dir);
含义:readdir()返回参数dir 目录流的下个目录进入点。
struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};
fileno()
功 能:把文件流指针转换成文件描述符
相关函数:open, fopen
表头文件:#include <stdio.h>
定义函数:int fileno(FILE *stream)
函数说明:fileno()用来取得参数stream指定的文件流所使用的文件描述词
返回值 :返回和stream文件流对应的文件描述符。如果失败,返回-1。
范例:
#include <stdio.h>
main()
{
FILE *fp;
int fd;
fp = fopen(“/etc/passwd”, “r”);
fd = fileno(fp);
printf(“fd = %d\n”, fd);
fclose(fp);
}
文件描述词是Linux编程中的一个术语。当一个文件打开后,系统会分配一部分资源来保存该文件的信息,以后对文件的操作就可以直接引用该部分资源了。文件描述词可以认为是该部分资源的一个索引,在打开文件时返回。在使用fcntl函数对文件的一些属性进行设置时就需要一个文件描述词参数。
以前知道,当程序执行时,就已经有三个文件流打开了,它们分别是标准输入stdin,标准输出stdout和标准错误输出stderr。和流式文件相对应的是,也有三个文件描述符被预先打开,它们分别是0,1,2,代表标准输入、标准输出和标准错误输出。需要指出的是,上面的流式文件输入、输出和文件描述符的输入输出方式不能混用,否则会造成混乱。
telldir()
头文件:#include <dirent.h>
定义函数:off_t telldir(DIR *dir);
函数说明:telldir()返回参数dir 目录流目前的读取位置. 此返回值代表距离目录文件开头的偏移量返回值返回下个读取位置, 有错误发生时返回-1.
exit()和_eixt()区别
_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit() 函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。
文件流指针
在应用编程层面,程序对流的操作体现在文件流指针FILE上,在操作一个文件前,需要打开该文件,而使用ANSI C库函数fopen()打开一个文件后,将返回一个文件流指针与该文件关联,所有针对该文件的读写操作都通过该文件流指针完成,以下是应用层所能访问的FILE结构体,因此,结构体成员可以在用户空间中访问。
typedef struct _IO_FILE FILE;
struct _IO_FILE{
int _flags;
char* _IO_read_ptr; //如果以读打开,当前读指针
char* _IO_read_end; //如果以读打开,读区域结束位置
char* _IO_read_base; //Start of putback+get area
char* _IO_write_base; //如果以写打开,写区起始区
char* _IO_write_ptr; //如果以写打开,当前写指针
char* _IO_write_end; //如果以写打开,写区域结束位置
char* _IO_buf_base; //如果显示设置缓冲区,其起始位置
char* _IO_buf_end; //如果显示设置缓冲区,其结束位置。
…
int _fileno; //文件描述符
…
}
在此结构体中,包含了I/O库为管理该流所需要的所有信息,如用于实现I/O的文件描述符、指向流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数和出错标志等。
Linux文件存储结构
大部分的Linux文件系统(如ext2、ext3)规定,一个文件由目录项、inode和数据块组成:
- 目录项:包括文件名和inode节点号。
- Inode:又称文件索引节点,包含文件的基础信息以及数据块的指针。
- 数据块:包含文件的具体内容。
Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。
目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码。
雨课堂题目整理
!==[image-20210610214926804]==(D:\SyncDisk\笔记整理\Linux\image-20210610214926804.png)
进程编程
1.1. 进程
进程:一个或多个线程执行的地址空间,线程执行时需要系统资源
1.2. 进程启动
System call “fork” 系统调用fork
Process resources
struct task_struct
System space stack
…
System call “exec”系统调用exec
The entry of C programs C程序入口
1.3. 进程终止
进程终止的五种方式
Normal termination 正常终止
Return from “main” function 从main函数返回
Call “exit” function 调用exit函数
Call “_exit” function 调用_exit函数
Abnormal termination 异常终止
Call “abort” function 调用abort函数
Terminated by a signal 信号终止
1.4. 进程分类
Foreground process前台进程
要求用户启动它们或与它们交互的进程称为前台进程。
前台进程不结束,终端就不会出现系统提示符,直到进程终止。
缺省情况下,程序和命令作为前台进程运行。
Background process 后台进程
独立于用户运行的进程称为后台进程。
用户在输入命令行后加上“&”字符然后按
Shell不等待命令终止,就立即出现系统提示符,让该命令进程在后台运行,用户可以继续执行新的命令。
Daemon 守护进程
总是运行在后台的系统进程。
守护程序通常在系统启动时启动,并且它们一直运行到系统停止。
守护进程常常用于向用户提供各种类型的服务和执行系统管理任务。
守护程序进程由 root 用户或 root shell 启动,并只能由 root 用户停止。
2.1. 进程标识符
2.2. 进程创建
1.fork
fork: create a child process
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
returned value:
pid of child (in the current (parent) process),
0 (in child process),
-1 (failure)
fork创建子进程代码结构
……
pid = fork();
if (pid<0) {perror(“fork()”);exit(1);}
else if (pid==0) { child process }
else { parent process }
文件共享
所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项
1 | int main(int argc,char *argv[]) |
1 | 单个进程 |
fork 应用场合
进程复制自己,使父子进程同一时刻执行不同的代码——网络服务
进程要执行另一个不同的程序:fork-exec——shell
Question:效率问题?父子进程各自占一段逻辑地址空间,fork之后立即exec,地址空间浪费。
“写—复制”
2. vfork
vfork
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:类似fork,创建一个新进程,效率髙。
与fork区别:
(1) vfork创建的进程与父进程共用地址空间
(2) vfork创建子进程后,阻塞父进程,直到子进程调用exec或exit,内核才唤醒父进程。
1 | int global=5; |
sleep函数
函数原型:
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
seconds:暂停时间(秒)
3.exec 系列函数
用一个新的进程映像替换当前的进程映像,执行新程序的进程保持原进程的一系列特征:
pid, ppid, uid, gid, working directory, root directory …
#include <unistd.h>
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 * const argv[]);
int execvp(const char *file, char * const argv[]);
int execve(const char *filename, char * const argv[], char * const envp[]);
1.两大类:
execl开头:参数以列表形式arg0,arg1…NULL结束。
execv开头:参数以指向字符串数组argv[]指针形式,arg[0]必须为程序名。
2.含有字母p的函数可以使用相对路径,根据环境变量PATH查找文件;其他函数必须使用绝对路径;
3.含有字母e的函数,需要通过指向envp[]数组的指针指明新的环境变量,其他函数使用当前环境变量。
1 | 用exec函数使新进程执行“/bin/ps” 程序。 |
fork和exec一起使用
父子进程各自执行不同的代码,进程要执行另一个程序.
1 | printf("%% "); /* print prompt */ |
2.3 进程终止
1.exit函数
函数原型:
#include <stdlib.h>
void exit(int status);
status:进程状态
功能:正常终止目前进程的执行,把参数status返回给父进程,进程所有的缓冲区数据会自动写回并关闭未关闭的文件。
2._exit函数
函数原型:
#include <stdlib.h>
void ——exit(int status);
status:进程状态
功能:立刻终止目前进程的执行,把参数status返回给父进程,并关闭未关闭的文件。不处理标准I/O缓冲区。
2.4 父子进程关系
1. 两种进程概念
父进程在子进程前终止——孤儿进程
Orphan process——init
子进程在父进程前终止——可能成为僵尸进程
SIGCHLD signal 忽略SIGCHLD信号
Handled by wait/waitpid in parent 父进程中用wait/waitpid处理
Not handled by wait/waitpid in parent -> zombie父进程没有用wait/waitpid处理->僵尸进程
2. 僵尸进程
僵尸进程:已终止运行,但尚未被清除的进程。
子进程运行结束后(正常或异常),它并没有马上从系统的进程分配表中被删掉,而是进入僵死状态(Zombie),一直等到父进程来回收它的结束状态信息。
1 | main() |
**3.**僵尸进程解决方法
僵尸进程的proc结构一直存在直到父进程正常结束或系统重启
如何消除僵尸进程?
方法一:wait/waitpid阻塞父进程,子进程先终止
方法二:父进程不阻塞,两次fork
方法三:使用signal信号处理
方法一:wait example
例3.5设计一个程序,要求复制进程,子进程显示自己的进程号后暂停一段时间,父进程等待子进程正常结束,打印显示等待的进程号和等待的进程退出状态。
1 | main() |
wait & waitpid functions
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
waitpid的第一个参数pid的意义:
pid > 0: 等待进程id为pid的子进程。
pid == 0: 等待与自己同组的任意子进程。
pid == -1: 等待任意一个子进程
pid < -1: 等待进程组号为-pid的任意子进程。因此,wait(&stat)等价于waitpid(-1, &stat, 0)
waitpid例子
1 |
|
方法二:两次fork Example
1 | int main(void) |
方法三:singnal函数处理
Singnal函数处理:
(1)设置信号处理函数
signal(SIGCHLD,fun)
(2)忽略子进程终止信号
signal(SIGCHLD,SIG_IGN)
内核回收
4.1 登录方式
终端登录
网络登录
4.2 进程组和会话期
进程组
一个或多个过程的集合
getpgrp/setpgid functions
会话期Session
一个或多个进程组的集合。
getsid/setsid function
setsid函数
4.3 守护进程Daemon
精灵进程或守护进程
后台执行, 没有控制终端或登录 Shell 的进程
ps –aux 命令查看
Init:进程1,启动系统服务
Keventd:为内核中运行的函数提供进程上下文
Kswapd:页面调出守护进程
bdflush,kupdated:调整缓存中的数据写到磁盘
portmap:将RPC程序号映射为端口号
inetd(xinetd):侦听网络接口,获取网络服务进程请求
注意:大多数守护进程都以超级用户(用户ID为0)特权运行。没有一个守护进程具有控制终端,其终端名设置为问号(?)。
ps axj命令查看
daemon特征:
sid,pid,pgid相同,均为pid
ppid为1
tty为?
daemon进程实现规则
编程规则
首先调用fork,然后使父进程exit
调用setsid创建一个新的会话期 setsid()
将当前工作目录更改为特定目录chdir(./)
进程的umask设为0 umask(0)
关闭不需要的文件描述符 close()
1 | int daemon_init(void) { |
编写守护进程的要点
(1)创建子进程,终止父进程
pid=fork();
if(pid>0)
{exit(0);} /终止父进程/
(2)在子进程中创建新会话
setsid函数用于创建一个新的会话,并担任该会话组的组长,其作用:
①让进程摆脱原会话的控制;
②让进程摆脱原进程组的控制;
③让进程摆脱原控制终端的控制。
而setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
(3)改变工作目录
改变工作目录的常见函数是chdir。
(4)重设文件创建掩码
文件创建掩码是指屏蔽掉文件创建时的对应位。
把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。
设置文件创建掩码的函数是umask。
(5)关闭文件描述符
通常按如下方式关闭文件描述符:
for(i=0;i<NOFILE;i++)
close(i);
或者也可以用如下方式:
for(i=0;i<MAXFILE;i++)
close(i);
守护进程编写
例3.7 设计两个程序,主程序和初始化程序。要求主程序每隔10秒向/tmp目录中的日志报告运行状态。初始化程序中的init_daemon函数负责生成守护进程。
1 | /*主程序每隔一分钟向/tmp目录中的日志3-7.log报告运行状态*/ |
注意:fopen函数必须具有root权限。如果没有root权限,可以看到守护进程的运行,但不会在文件里写入任何字符。
例3-8:设计一个程序,要求运行后成为守护进程,守护进程又复制出一个子进程,守护进程和它的子进程都调用syslog函数,把结束前的状态写入系统日志文件。
1 |
|
注意:调用openlog、syslog函数,操作的系统日志文件“/var/log/syslog”或“/var/log/messages ”,必须具有root权限。
openlog函数说明
syslog函数说明
5.1 信号概念
信号
Software interrupt软件中断
Mechanism for handling asynchronous events异步事件
Having a name (beginning with SIG)
Defined as a positive integer (in <signal.h>)
信号产生
按终端键,硬件异常,kill(2)函数,kill(1)命令,软件条件,…
SIG信号(1-31)是从UNIX系统中继承下来的称为不可靠信号(也称为非实时信号)。
SIGRTMIN~SIGRTMAX是为了解决前面“不可靠信号”问题而进行更改和扩充的信号,称为可靠信号(也称为实时信号)。
可靠信号(实时信号):支持排队,发送用户进程一次就注册一次,发现相同信号已经在进程中注册,也要再注册。
不可靠信号(非实时信号):不支持排队,发送用户进程判断后注册,发现相同信号已经在进程中注册,就不再注册,忽略该信号。
名称 | **说明 ** | 默认操作 |
---|---|---|
SIGABRT | 进程异常终止(调用abort函数产生此信号) | |
SIGALRM | 超时(alarm) | 终止 |
SIGFPE | 算术运算异常(除以0,浮点溢出等) | |
SIGHUP | 连接断开 | 终止 |
SIGILL | 非法硬件指令 | |
SIGINT | 终端终端符(Clt**-C)** | 终止 |
SIGKILL | 立即结束程序运行(不能被捕捉、阻塞或忽略) | 终止 |
SIGPIPE | 向没有读进程的管道写数据 | |
SIGQUIT | 终端退出符(Clt-\) | 终止 |
SIGTERM | 终止(由kill命令发出的系统默认终止信号) | 退出 |
SIGUSR1 | 用户定义信号 | 退出 |
SIGUSR2 | 用户定义信号 | 退出 |
SIGSEGV | 无效存储访问(段违例) | |
---|---|---|
SIGCHLD | 子进程停止或退出 | 忽略 |
SIGCONT | 使暂停进程继续 | 继续/忽略 |
SIGSTOP | 暂停一个进程(不能被捕捉、阻塞或忽略) | 终止 |
SIGTSTP | 终端挂起符(Clt-Z) | 停止进程 |
SIGTTIN | 后台进程请求从控制终端读 | |
SIGTTOUT | 后台进程请求向控制终端写 |
信号处理
忽略信号
不能忽略的信号:
SIGKILL, SIGSTOP
一些硬件异常信号
执行系统默认动作
捕捉信号
5.2 信号相关命令
Kill
• 暂停 kill –STOP
• 恢复 kill –CONT
• 终止 kill –KILL
函数 | 功能 |
---|---|
kill | 发送信号给进程或进程组 |
raise | 发送信号给进程自身 |
alarm | 定时器时间到,向进程发送SIGALRM信号 |
pause | 没有捕捉信号前一直将进程挂起 |
signal | 捕捉信号SIGINT, SIGQUIT时执行信号处理函数 |
sigemptyset | 初始化信号集合为空 |
---|---|
sigfillset | 初始化信号集合为所有信号集合 |
sigaddset | 将指定信号加入到指定集合 |
sigdelset | 将指定信号从信号集中删除 |
sigprocmask | 判断检测或更改信号屏蔽字 |
1 信号发送——kill & raise
kill: send signal to a process
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
(Returned Value: 0 if success, -1 if failure)
The “pid” parameter
pid > 0: 发送信号给进程id为pid的进程。
pid =0:发送信号给与自己同组的所有进程。
pid = -1: 发送系统内所有进程
pid < -1: 发送给进程组号为-pid的所有进程。
raise: send a signal to the current process
#include <signal.h>
int raise(int sig);
(Returned Value: 0 if success, -1 if failure)
例3-9 设计一程序,要求用户进程复制出一个子进程,父进程向子进程发出SIGKILL信号,子进程收到此信号,结束子进程的运行。
分析:用户进程fork子进程后,子进程使用raise函数发送SIGSTOP信号,使自己暂停;父进程使用kill函数向子进程发送SIGKILL信号,子进程收到信号结束。
1 | int main () |
2 信号处理—— “signal” 函数
为编号为sgn的信号安装一个新的信号处理程序。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
(Returned Value: the previous handler if success, SIG_ERR if error)
The “handler” parameter
a user specified function, or
SIG_DFL, or
SIG_IGN
1 | static void sig_usr(int); |
3 alarm & pause 函数
alarm: 为信号的传送设置闹钟
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
(Returned value: 0, or the number of seconds remaining of previous alarm)
pause: wait for a signal
#include <unistd.h>
int pause(void);
(Returned value: -1, errno is set to be EINTR)
**4.**信号阻塞
有时既不希望进程在接收到信号时立刻中断进程的执行,也不希望此信号完全被忽略掉,而是延迟一段时间再去调用信号处理函数,这个时候就需要信号阻塞来完成。
信号集处理函数
#include <signal.h>
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,int signum);
参数:set 信号集 ;signum 信号
返回值:若成功返回0,若出错返回-1。
sigismember若真返回1,若假返回0,若出错返回-1。
1 |
|
信号集处理函数sigprocmask
#include <signal.h>
功能:检测或更改信号屏蔽字
函数:int sigprocmask(int how,const sigsett_t *set,sigset_t *oldset);
参数:how 操作方式set
how决定函数的操作方式。
SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中。
SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合。
SIG_SETMASK:将当前的信号集合设置为信号阻塞集合。
返回值:若成功返回0,若出错返回-1。
1 |
|
程序先定义信号集set,然后把信号SIGINT添加到set信号集中,最后把set设置为信号阻塞集合。运行程序时,进程进入死循环。按“ctrl+c”系统并没有中断程序,SIGINT信号屏蔽掉了。按”ctrl+z”来结束程序。
信号实例
1 | void handler() |
6.1 进程间通信
IPC: Inter-Process Communication 进程间通信
IPC机制
shared file
signal
pipe, FIFO (named pipe), message queue, semaphore, shared memory
socket
Simple Client-Server or IPC model
6.2 pipe 概念
Pipe
Pipe mechanism in a shellShell中的管道机构
e.g. cmd1 | cmd2
Pipe is half-duplex 半双工
管道只能在共同祖先的进程间使用
管道也是文件
命名管道(FIFO)
pipe 函数
The pipe function: create a pipe
#include <unistd.h>
int pipe(int filedes[2]);
(Returned value: 0 if success, -1 if failure)
A pipe: First In, First Out
filedes[0]:read, filedes[1]: write
单个进程使用管道
父子进程使用管道
管道使用(1):单个进程
类似共享文件
pipe—write——read
管道使用(2)多进程
父-子进程/子-子进程
使用模式:
pipe——fork——write(写进程)
——read(读进程)
注意:pipe-fork顺序
管道使用(3)用于标准输入输出
管道:shell中的形式
cmd1 | cmd2
重定向 cmd > file
实现代码
执行cmd1前
if (fd[1] != STDOUT_FILENO) {
if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)
err_sys(“dup2 error to stdout);
}
执行cmd2前
if (fd[0] != STDIN_FILENO) {
if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO)
err_sys(“dup2 error to stdin);
}
1 | int main() |
od –c file 以字符方式显示文件内容,如果没指定文件则以标准输入作为默认输入
6.3 popen & pclose 函数
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
fp=popen(cmd,”r”)的pipe实现:
FILE * popen(char * cmd,char * type)
{ int fd; int p;
pipe(fd);
p=fork();
if(p==0) { close(1);dup(fd[1]);
execl(cmd,cmd,NULL); }
else { wait();
fp=fdopen(fd[0],”r”); return fp; }
}
例3-13 设计一程序,要求用popen创建管道,实现“ls –l|grep fifo”的功能。
1 | int main () |
使用popen函数读写管道,实际上也是调用pipe函数建立一个管道,再调用fork函数建立子进程,接着会建立一个shell环境,并在这个shell环境中执行参数指定的进程。
例3-14 在程序中获得另一个程序的输出
popen_two.c
gcc myuclc.c -o myuclc
gcc popen_two.c -o two
输入大写字母
按Ctrl+D结束程序
1 | 程序头文件 |
6.4 FIFO: named pipe命名管道
管道和命名管道
相同点
不同点
文件系统中
内存传输数据
同步:一个重要的考虑
mkfifo(1), mkfifo(3), mknod(1), mknod(2)
创建FIFO
命令行方式:mknod filename p
mkfifo filename
程序方式:mkfifo: make a FIFO special file (a named pipe)
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
(Returned value: 0 if success, -1 if failure)
例: mkfifo(“/tmp/myfifo”,0666)
访问FIFO
命令行方式
(1)cat < /tmp/myfifo 读FIFO文件
(2)echo hello >/tmp/myfifo 向FIFO写数据
(3)cat < /tmp/myfifo &
echo “hello” >/tmp/myfifo
程序方式:用open打开一个FIFO
Review: “open” system call
int open(const char *pathname, int flags);
“flags” parameter
必须指定的互斥模式:
O_RDONLY, O_WRONLY, O_NONBLOCK
O_RDONLY:若无进程写方式打开FIFO,open阻塞
O_RDONLY |O_NONBLOCK:若无进程写方式打开FIFO,open立即返回文件描述符
O_WRONLY:若无进程读方式打开FIFO,open阻塞
O_WRONLY| O_NONBLOCK:若无进程读方式打开FIFO,open返回ENXIO错误,-1
例3-15 两个程序通过FIFO传递数据,一个生产者程序创建并打开一个FIFO,向管道中写入数据。(3-15fifo_p.c)一个消费者程序,从FIFO中读取数据(3-15fifo_c.c)。
1 |
FIFO的应用(1)
用FIFO复制输出流
mkfifo fifo1
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2
tee命令读取标准输入,把这些内容同时输出到标准输出和(多个)文件中
FIFO的应用(2)
C/S应用程序
例3-16 client.c, server.c
1 | //Client.h |
例3-17 l设计两个程序,要求用命名管道FIFO实现简单的聊天功能。
Zhang.c
Li.c
1 | int main() |
7.1 System V IPC的共同特征
进程间通信(interprocess communication)
IPC objects
信号量(semaphore set)
消息队列(message queue)
共享内存(shared memory)
shell命令
ipcs -q/-m/-s
ipcrm –q/-m/-s
ipcrm -Q/-M/-S
标识符与关键字
创建IPC对象时指定关键字(key_t key;)
key的选择;预定义常数IPC_PRIVATE;ftok函数
引用IPC对象:标识符
内核将关键字转换成标识符
许可权结构
和文件类比
struct ipc_perm
SV IPC System Calls Overview
功能 | 消息队列 | 信号量 | 共享内存 |
---|---|---|---|
分配一个IPC对象,获得对IPC的访问 | msgget | semget | shmget |
IPC操作: 发送/接收消息,信号量操作,连接/释放共享内存 | msgsnd/ msgrcv | semop | shmat/ shmdt |
IPC控制:获得/修改状态信息,取消IPC | msgctl | semctl | shmctl |
ftok函数
创建函数
key_t ftok( char * filename, int id);
功能说明
将一个已存在的文件名(该文件必须是存在而且可以访问的)和一个整数标识符id转换成一个key_t值
在Linux系统实现中,调用该函数时,系统将文件的索引节点号取出,并在前面加上子序号,从而得到key_t的返回值
创建IPC对象
key:可由ftok()函数产生或定义为IPC_PRIVATE常量
flag:包括读写权限,还可包含IPC_CREATE和IPC_EXCL标志位,组合效果如下
7.2 Message queue
消息队列
消息队列是消息的链表,存放在内核中并由消息队列标识符标识。
First in, first out
message type: 优先级=
块数据
消息队列——程序结构
proto.h:约定的消息队列通信格式
send.c
receive.c
proto.h:约定的消息队列通信格式:
#define KEYPATH “/etc/services”
#define KEYPROJ ‘a’
struct msg_st {
char * msg;
long type;
…
}
消息队列——系统函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int flag);
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
int msgctl(int msqid, int cmd, struct shmid_ds *buf);
msgget
Ø函数原型 int msgget(key_t key, int flag);
Ø参数说明
ü key:待创建/打开队列的键值
ü flag:创建/打开方式
常取IPC_CREAT|IPC_EXCL|0666
若不存在key值的队列,则创建;否则,若存在,则打开队列
0666表示与一般文件权限一样
Ø返回值
成功返回消息队列描述字,否则返回-1
Ø说明
IPC_CREAT一般由服务器程序创建消息队列时使用
若是客户程序,须打开现有消息队列,而不用IPC_CREAT
msgsnd
Ø函数原型
int msgsnd(int msqid, struct msgbuf *msgp, size_t size, int flag);
Ø说明
flag有意义的标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待
Ø内核须对msgsnd( )函数完成的工作
ü检查消息队列描述符、许可权及消息长度
若合法,继续执行;否则,返回
ü内核为消息分配数据区,将消息正文拷贝到消息数据区
ü分配消息首部,并将它链入消息队列的末尾
ü修改消息队列头数据,如队列中的消息数、字节总数等
ü唤醒等待消息的进程
msgrcv
Øint msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
Ø参数说明
l msgid:消息队列描述字
l msgp:消息存储位置
l size:消息内容的长度
l type:请求读取的消息类型
l flag:规定队列无消息时内核应做的操作
IPC_NOWAIT:无满足条件消息时返回,此时errno=ENOMSG
IPC_EXCEPT:type>0时使用,返回第一个类型不为type的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将丢失
msgctl: message control operations
函数原型
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
“cmd” 参数
IPC_STAT: 把msqid_ds结构中的数据置为消息队列的当前关联值
IPC_SET: 在进程有足够权限的前提下,把消息队列的当前关联值置为msqid_ds结构给出的值
IPC_RMID: 删除消息队列
消息队列——Example
A C/S application
一台服务器,多个客户机:只需要一个队列。与FIFO实现相比
消息队列属性
消息队列创建后,操作系统在内核中分配了一个名称为msqid_ds的数据结构用于管理该消息队列。
在程序中可以通过函数msgctl对该结构进行读写,从而实现对消息队列的控制功能。
成员说明:
1)msg_perm:IPC许可权限结构。
2)msg_stime:最后一次向该消息队列发送消息(msgsnd)的时间。
3)msg_rtime:最后一次从该消息队列接收消息(msgrcv)的时间。
4)msg_ctime:最后一次调用msgctl的时间。
5)msg_cbytes:当前该消息队列中的消息长度,以字节为单位。
6)msg_qnum:当前该消息队列中的消息条数。
7)msg_qbytes:该消息队列允许存储的最大长度,以字节为单位。
8)msg_lspid:最后一次向该消息队列发送消息(msgsnd)的进程ID。
9)msg_lrpid:最后一次从该消息队列接收消息(msgrcv)的进程ID。
使用:
msg_sinfo.msg_qbytes = 1666;
msgctl(msgqid,IPC_SET,&msg_sinfo)
例3-18 msg_stat.c
7.3 Shared memory
共享内存是内核为进程创建的一个特殊内存段,它可连接(attach)到自己的地址空间,也可以连接到其它进程的地址空间
最快的进程间通信方式
不提供任何同步功能
共享内存实现途径
mmap()系统调用
将普通文件在不同的进程中打开并映射到内存
不同进程通过访问映射来达到共享内存目的
POSIX共享内存机制(Linux 2.6未实现)
System V共享内存
在内存文件系统tmpfs中建立文件
文件映射到不同进程空间
mmap()
lmmap()系统调用使得==进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。==
#include < unistd.h >
#include <sys/mman.h >
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
int munmap( void * addr, size_t len )
int msync ( void * addr , size_t len, int flags)
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
addr:指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。
len:映射到调用进程地址空间的字节数,从被映射文件开头offset个字节开始算起。
prot :指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。
flags;指定映射对象的类型,映射选项和映射页是否可以共享。由以下几个常值指定:MAP_SHARED , MAP_PRIVATE 必选其一。
fd:为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANONYMOUS,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,只能用于具有亲缘关系的进程间通信)。
offset参数一般设为0,表示从文件头开始映射。
函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。
mmap()用于共享内存的两种方式 :
(1)使用普通文件提供的内存映射:适用于任何进程之间;需要打开或创建一个文件,然后再调用mmap();调用代码如下:
fd=open(name, flag, mode);
if(fd>0)
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE,
MAP_SHARED , fd , 0);
(2) 使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;
l调用代码如下:
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS ,-1 , 0);
由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。在调用fork()之后,子进程继承父进程匿名映射后的地址空间,也继承mmap()返回的地址
不必指定具体的文件,只要设置相应的标志即可
munmap()
int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
msync()
int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
例3-19mmap.c l设计一个程序,要求复制进程,父子进程通过匿名映射实现共享内存。
1 | typedef struct |
使用特殊文件提供匿名内存映射,适用于具有亲缘关系的进程之间。==一般而言,子进程单独维护从父进程继承下来的一些变量。而mmap函数的返回地址,由父子进程共同维护。==
System V IPC共享内存的实现
通过映射到tmpfs中的shm文件对象实现共享主存
- 每个共享主存区对应tmpfs中的一个文件(通过shmid_kernel关联)
创建过程
- 从主存申请共享主存管理结构,初始化相应shmid_kernel结构
- 在tmpfs中创建并打开一个同名文件
- 在主存中建立与该文件对应的dentry及inode结构
- 返回相应标识符
System V shared memory
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int flag);
void *shmat(int shmid, void *addr, int flag);
int shmdt(void *addr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
mmap和System V
1.System V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。
2.System V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。
7.4 信号量
用来协调进程对共享资源的访问
相关函数semget,semop,semctl
所需头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
相关函数——semget
创建一个新信号量或取得一个已有信号量,原型为:int semget(key_t key, int num_sems, int sem_flags);
参数key****:整数值
参数num_sems:指定需要的信号量数目,几乎总是1。
参数sem_flags:一组标志,信号量不存在时创建一个新的信号量,指定IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。
指定IPC_CREAT | IPC_EXCL,创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
返回值:成功返回一个相应信号标识符(非零),失败返回-1.
相关函数——semop
操作一个或一组信号 ,原型为:
int semop(int sem_id, struct sembuf *sem_opa, size_t nsops);
semid:信号集的识别码,可通过semget获取。
sem_opa:指向存储信号操作结构的数组指针,信号操作结构的原型如下
struct sembuf{
short sem_num; //信号量集中的信号量编号0,1……
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,一个是+1。
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号并在进程没有释放该信号量而终止时,操作系统释放信号量
};
lnsops:信号量操作结构的数量,大于或等于1
相关函数——semctl
该函数用来直接控制信号量信息,它的原型为
int semctl(int sem_id, int sem_num, int command, …);
第四个参数,它通常是一个union semum结构,定义如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
例3-20sem1.c
1 | int main(int argc, char *argv[]) |
例3-21sem2.c
1 | static int set_semvalue() |
雨课堂题目整理
1 |
|
网络编程
0 服务器和客户机的信息函数
**1.**字节序列转换
每一台机器内部对变量的字节存储顺序不同,而网络传输的数据是一定要统一顺序的。所以对内部字节表示顺序与网络字节顺序(大端)不同的机器,一定要对数据进行转换
真正转换还是不转换是由系统函数自己来决定的。
头文件:include <arpa/inet.h>
unsigned short int htons(unsigned short int hostshort):
主机字节顺序转换成网络字节顺序,对无符号短型进行操作4bytes
unsigned long int htonl(unsigned long int hostlong):
主机字节顺序转换成网络字节顺序,对无符号长型进行操作8bytes
unsigned short int ntohs(unsigned short int netshort):
网络字节顺序转换成主机字节顺序,对无符号短型进行操作4bytes
unsigned long int ntohl(unsigned long int netlong):
网络字节顺序转换成主机字节顺序,对无符号长型进行操作8bytes
**2.**地址格式转换
头文件:#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
转换字符串到网络地址 。 返回:1成功;-1出错
const char* inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
转换网络二进制结构到ASCII类型的地址
返回:成功,指向结果的指针;出错,NULL
1 Socket 基础
socket是网络编程的一种接口,它是一种特殊的I/O,用socket函数建立一个Socket连接,此函数返回一个整型的socket描述符,随后进行数据传输。
一个IP地址,一个通讯端口,就能确定一个通讯程序的位置。为此开发人员专门设计了一个套接结构,就是把网络程序中所用到的网络地址和端口信息放在一个结构体中。
一般套接口地址结构都以“sockaddr”开头。socket根据所使用的协议的不同可以分TCP套接口和UDP套接口,又称为流式套接口和数据套接口。
UDP是一个无连接协议,TCP是个可靠的端对端协议。传输UDP数据包时,LINUX不知道也不关心它们是否已经安全到达目的地,而传输TCP数据包时,则应先建立连接以保证传输的数据被正确接收。
三种类型套接字
流套接字(SOCK_STREAM)
可靠的、面向连接的通信。
使用TCP协议
数据报套接字(SOCK_DGRAM)
无连接服务
使用UDP协议
原始套接字(SOCK_RAW)
允许对底层协议如IP、ICMP直接访问
1. socket套接字的数据结构
两个重要的数据类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,如IP地址、通信端口等。
sockaddr——虚拟定义的地址(取决于协议):
1 | struct sockaddr |
sockaddr_in(AF_INET中使用的地址定义):
1 | struct sockaddr_in |
2. 基于连接的服务
Server程序的作用
程序初始化
持续监听一个固定的端口
收到Client的连接后建立一个socket连接
与Client进行通信和信息处理
接收Client通过socket连接发送来的数据,进行相应处理并返回处理结果
通过socket连接向Client发送信息
通信结束后中断与Client的连接
Client程序的作用
程序初始化
连接到某个Server上,建立socket连接
与Server进行通信和信息处理
接收Server通过socket连接发送来的数据,进行相应处理
通过socket连接向Server发送请求信息
通信结束后中断与Server的连接
3. 无连接的服务
UDP编程的适用范围
部分满足以下几点要求时,应该用UDP
面向数据报
网络数据大多为短消息
拥有大量Client
对数据安全性无特殊要求
网络负担非常重,但对响应速度要求高
例子:ICQ、视频点播
具体编程时的区别
socket()的参数不同
UDP Server不需要调用listen和accept
UDP收发数据用sendto/recvfrom函数
TCP:地址信息在connect/accept时确定
UDP:在sendto/recvfrom函数中每次均需指定地址信息
UDP:shutdown函数无效
2 TCP编程
基于TCP协议的编程,其最主要的特点是建立完连接后,才进行通信。
常用的基于TCP网络编程函数及功能
头文件
#include <sys/types.h>
#include <sys/socket.h>
1.基于TCP网络编程函数
socket: 创建用于通信的端点并返回描述符.
int socket(int domain, int type, int protocol);
“domain” parameter
指定通信域,即选择协议族,如 AF_INET,AF_INET6 …
“type” parameter
指定通信语义。 三种主要类型: SOCK_STREAM, SOCK_DGRAM, SOCK_RAW
“protocol” parameter
usually 0 (default).
bind: binds a name to a socket
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
inet_aton & inet_ntoa
互联网地址操作例程
int inet_aton(const char *cp, struct in_addr *inp);
char* inet_ntoa (struct in_addr in);
inet_ntoa将一个32位数字表示的IP地址转换成点分十进制IP地址字符串
listen
listen: listen for connections on a socket
int listen(int s, int backlog);
参数说明:
s:socket()返回的套接口文件描述符。
backlog:进入队列中允许的连接的个数。进入的连接请求在使用系统调用accept()应答之前要在进入队列中等待。这个值是队列中最多可以拥有的请求的个数。大多数系统的缺省设置为20。
accept
accept()函数将响应连接请求,建立连接
int accept(int sockfd,struct sockaddr *addr,int *addrlen);
accept缺省是阻塞函数,阻塞直到有连接请求
sockfd: 被动(倾听)的socket描述符
如果成功,返回一个新的socket描述符(connected socket descriptor)来描述该连接。这个连接用来与特定的Client交换信息
addr将在函数调用后被填入连接对方的地址信息,如对方的IP、端口等。
connect
connect: initiate a connection on a socket (connect to a server).
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
主动的socket
servaddr是事先填写好的结构,Server的IP和端口都在该数据结构中指定。
**send/**recv
send/recv: 面向连接
int send(int s, const void *msg, size_t len, int flag);
s:发送数据的套接口文件描述符。
msg:发送的数据的指针
len:数据的字节长度
flag:标志设置为0。
int recv(int s, void *buf, size_t len, int flag);
s:读取的套接口文件描述符。
buf:保存读入信息的地址。
len:缓冲区的最大长度。
flag:设置为0。
sendto/recvfrom
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socketlen_t tolen);
int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
close & shutdown
close
int close(int sockfd);
shutdown
int shutdown(int sockfd, int how);
how: SHUT_RD, SHUT_WR, SHUT_RDWR
shutdown直接对TCP连接进行操作,close只是对套接字描述符操作。
例4.1:服务器通过socket连接后,向客户端发送字符串“连接上了”。在服务器上显示客户端的IP地址或域名。
主要语句说明:
服务端
建立socket:socket(AF_INET, SOCK_STREAM, 0);
绑定bind:bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr);
建立监听listen:listen(sockfd, BACKLOG);
响应客户请求:accept(sockfd,(struct sockaddr *)&remote_addr, &sin_size);
发送数据send:send(client_fd, “连接上了 \n”, 26, 0);
关闭close:close(client_fd);
客户端:
建立socket:socket(AF_INET, SOCK_STREAM, 0);
请求连接connect:connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr));
接收数据recv:recv(sockfd, buf, MAXDATASIZE, 0);
关闭close:close(sockfd);
1 | //服务端源程序代码 |
3 UDP编程
基于UDP协议的编程,其最主要的特点是不需要用函数bind把本地IP地址与端口号进行绑定,也能进行通信。
常用的基UDP网络编程函数及功能:
例4.2:服务器端接受客户端发送的字符串。客户端将打开liu文件,读取文件中的3个字符串,传送给服务器端,当传送给服务端的字符串为”stop”时,终止数据传送并断开连接。
主要语句说明:
服务端:
建立socket:socket(AF_INET,SOCK_DGRAM,0)
绑定bind:bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet));
接收数据recvfrom:recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_clnt,&len);
关闭close:close(sockfd);
客户端:
建立socket:socket(AF_INET, SOCK_STREAM, 0);
读取liu文件:fopen(“liu”,”r”);
发送数据sendto:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr,sizeof(adr_srvr));
关闭close:close(sockfd);
1 | //服务端源程序代码: |
4 网络高级编程
I/O Models
Block mode 阻塞模式
Non-block mode 非阻塞模式
I/O multiplexing I/O多路复用
多进程并发
多线程并发
阻塞方式
阻塞方式:在数据通信中,当服务器运行函数accept() 时,假设没有客户机连接请求到来,那么服务器就一直会停止在accept()语句上,等待客户机连接请求到来,出现这样的情况就称为阻塞。
非阻塞方式
阻塞与非阻塞方式的比较
errno - EWOULDBLOCK
非阻塞的实现
int flags;
if ( (flags=fcntl(sock_fd, F_GETFL, 0)) < 0)
err_sys();
flags |= O_NONBLOCK;
if ( fcntl(sock_fd, F_SETFL, flags) < 0)
err_sys();
I/O 多路复用
基本思想:
先构造一张有关描述符的表,然后调用一个函数(如select),该函数到这些描述符中的一个已准备好进行I/O时才返回,返回时告诉进程哪个描述符已准备好进行I/O.
“select”
select: synchronous I/O multiplexing.
#include <sys/select.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_ZERO(fd_set *set);
FD_SET(int fd, fd_set *set);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
多进程并发
1 | listenfd = socket(...); |