学习标记整理

SI/O of linux/unix
------------------------------------------------------
1、linux/unix 中文件名的最大长度是 NAME_MAX,路径的最大长度是 PATH_MAX
2、用于打开和关闭一个文件流的函数有三个
(1)、fopen
(2)、fclose()
(3)、freopen()
3、我们可以使用 fputs(stream,buffer)向文件写入内容,
可以使用 fgets(buffer,sizeof(buffer),stream) read string from stream.
4、无格式 IO
(1)、字符 I/O:所谓字符 I/O,就是每次只读出 1 个字符
函数接口:fgetc(stream) and getc(stream) getchar(void)
fputc(int c,stream) and putc(int c,stream),putchar(int
c)
fgetc(stream):get the character from stream one by one,return int
getc(stream):just like fgetc(stream)
getchar(void):just like getc(stdin)
fputc(int c,stream):trans (int)c to (u_char)c,then write to stream
putc(int c,stream):like fputc(int c,stream)
putchar(int c):same ...
(2)、行 I/O:所谓行 I/O,就是每次处理的是一行
函数接口:
fgets(char* s,int count,FILE* fp):read count characters
gets(char*s):just read one line end with '\n'
getline(char** ptr,size_t *n,FILE* fp):read line
getdelim(char** ptr,size_t *n,int delimiter,FILE* fp)
5、文件定位
函数接口:
ftell(stream):return the current pos in stream
fseek(stream,long int offset,int whence):change the pos of stream
rewind(stream):set pos in the file's bugin
fgetpos(stream,fpos_t* pos):get the current pos
fsetpos(stream,const fpos_t* pos):set stream's pos
6、文件结束和错误指示器
feof(stream):return 0 if EOF
ferror(stream):return 0 if ERROR
7、流缓冲
(1)、全缓冲:以整个缓冲区为单位进行读写,每次都读写固定 BUFFERSIZE 大小的内容。
(2)、行缓冲:只有遇到行结束标志才进行读写
(3 )、无缓冲:没有设置缓冲
函数接口:
setbuf(fp,char* buf):可以设置 buff 的大小,要关闭缓冲则将 buf 设置位 NULL
setvbuf(fp,char* buf,int type,size_t size):
type 可以位一下之一:
_IOFBUF->全缓冲
_IOLBUF->行缓冲
_IONBUF->无缓冲
可以通过 size 指定缓冲的大小,只有当设置位无缓冲时 size 才无效。
fflush(fp):刷新缓冲区
8、格式 I/O
(1)、格式输出
printf(const char* format...):output to std
1fprintf(stream,const char* format...):output to stream
sprintf(char* buf,const char* format...):out put to buf
(2)、格式输入
scanf(const char* format...):read from std
fscanf(stream,const char* format...):read from stream
sscanf(char* buf,const char* format...):read from array buf.
9、临时文件
tmpnam(char* s)
tempnam(const char* dir,const char* pfx):用户可以自己指定文件路径和文件名前缀
tmpfile(void):生成一个临时文件并且打开它
10、文件描数字的打开、创建、关闭
函数接口:
int open(const char* filename,int flags,mode_t mode/*this is
choosed...*/)
=>这个函数将创建一个文件描数字,并且将这个文件描数字返回,参数位文件名字,打开标

和可选的创建模式,但是 mode 只有在创建文件时才是有效的。
int create(const char* filename,mode_t mode)
=>这个函数将创建一个新的文件,返回创建好的文件的文件描数字,需要指定文件名和创建
文件的 mode,这个函数其实等价于 fd=open(filename,O_WRONLY|O_CREAT|
O_TRUNC,mode)
int close(int filedes)
=>这个函数就是关闭一个文件描数字,需要注意的是当我们调用这个函数关闭一个文
件描数字
的时候,这个不再被使用的文件描数字可以被被再次使用,我们可以想象文件描数字其实
就是一个数组的下标,这个数组就是保存文件项的数组,当一个文件被关闭,那么这个文件所
占有的数组项将可用
ssize_t read(int filedes,void* buffer,size_t nbytes)
=>从已经打开的与文件描数字 filedes 相连接的文件中读取 nbytes 个字节的内容到
buffer 中
ssize_t write(int filedes,const void* buffer,size_t nbytes)
=>向已经打开的与文件描数字 filedes 相连接的文件中把 buffer 中前 nbytes 字节写进

无论是 read 还是 write,都将返回所操作的字节数目,不一定就是 nbytes,有可能遇到
文件尾而不能继续操作...
11、问价描数字的文件位置
函数接口:off_t lseek(int filedes,off_t offset,int whence)
这个函数和 fseek(在流中)是一样的,其中的 whence 只能取下列中一个:
SEEK_SET:从文件开始出开始偏移
SEEK_CUR:从当前文件位置开始偏移
SEEK_END:从文件尾部开始偏移
那么,如何移动文件位置到文件尾部呢? ---> lseek(fd,0L,SEEK_END)
移动到文件开始处可以这样做
---> lseek(fd,0L,SEEK_SET)
-------------------------------
需要注意的一点是,当我们用 lseek 来设置文件位置后,如果设置的文件位置大于当前文件大小,这
也是被允许的,这样的
话,下一次开始写将扩展该文件,这种情况称为在文件中生成了空洞!这些位置将被 0 填充。
【notice 】
(1)、lseek 只是会改变文件的位置,而不会进行任何 I/O 操作,所以它不会改变 inode
中的文件大小,也就是说,即使我们设置的文件位置超出了文件的大小,它也不会导致当前文尾的
改变,只是下次打开文件并且以 O_APPEND 模式下时会自动扩展文件
(2)、每当用 O_APPEND 标志打开文件执行 write 操作时,当前文件首先移动到 inode 给出的文
件大小
2所指定的位置(所以会自动扩展),这也是实现在这种模式下只能从当前文件尾添加内容的依据。
12、dup 和 dup2 函数
函数接口:
int dup(int old):复制 old 文件描数字到最小可用新描数字(返回值)
int dup2(int old,int new):复制 old 文件描数字到 new 文件描数字,如果 new 文件描
数字一句被占用,那么
系统将首先关闭 new 文件描数字,然后再复制过去;如果 old==new,那么只返回 new 但是不
关闭。
什么叫做输入输出重定向?
==>改变一个特定文件描数字对应的文件或者管道
13、fdopen 和 fileno、fcntl
函数接口:
FILE* fdopen(int filedes,const char* opentype):将 filedes 指定的文件描数
字转换位文件流
int
fileno(FILE* stream):获取文件流的文件描数字
int
fcntl(int filedes,int cmd,...):用它可以对一句打开的文件描数字进行各
种操作,例如,重复一个
文件描数字、查询或者设置文件描数字标签、查询或者设置文件描数字状态标签等
|-------------fcntl 命令常数名字及其含
义----------------------------------|
F_DUPFD
重复文件描数字
F_GETFD
获取文件描数字标签
F_SETFD
设置文件描数字标签
F_GETFL
获取文件状态标签
F_SETFL
设置文件状态标签
F_GETLK
获取文件锁
F_SETLK
设置文件锁
F_SETLKW
等待完成的设置文件锁
F_GETOWN
获取收到 SIGIO 信号的进程或者进程组
F_SETOWN
设置....
|--------------------------------------------------------------------
---|
-
14、文件状态标签
什么叫文件状态标签呢? ---->文件状态标签指明文件的打开属性,他们由 open 的 flag 参数指
定。
文件状态标签可以分为三类,如下:
|---------------文件状态标签----------|
(1)、访问方式
-------------------------------------------------------------------
O_RDONLY,O_WRONLY,O_RDWR
-------------------------------------------------------------------
(2)、打开时标志
-------------------------------------------------------------------
O_CREAT
->create the file if not exist now
O_EXCL
->当 o_creat 和此同时设置时,open 失败,保证已经存在的文件不会被覆

O_NONBLOCK ->设置为费阻塞 I/O
O_NOCITY
->...
O_TRUNC
->...
-------------------------------------------------------------------
3(3)、I/O 操作方式
-------------------------------------------------------------------
O_APPEND ->APPEND FILE IN THE END OF THIS CURRENT FILE
O_NONBBLOCK ->...
O_ASYNC
->..
O_SYNC
->..
O_DSYNC
->...
O_RSYNC
->...
-------------------------------------------------------------------
15、什么叫做阻塞 I/O?
调用必须等待所有操作完成,即读写到数据(read)才能返回,非阻塞 I/O 就是:如果操作不能及
时完成,
函数就会返回错误信息。
有两种方法可以指定非阻塞 I/O
(1)、在 open 时指定 O_NONBBLOCK
(2)、对已经打开的描数字,调用 fcntl 来设置
16、readv and writev
函数接口:
ssize_t
ssize_t

readv(int fildes,const struct iovec* iov,int iovcnt)
writev(...):
->writev 将 iov[0],iov[1]...iov[iovcnt-1]指定的存储区中的数据写到 fildes 指定
文件描数字中,返回值是写出的数据总字节数。readv 则按 iov[0]...iov[iovcnt-1]规定
的顺序和长度,分散的从 fildes 中读出数据放到相应的位置里面。
17、文件与目录
什么叫做文件呢?文件由那些部分组成?
答:文件本质上是一个存放数据的容器,文件由数据块和 inode 组成。可以用 stat 命令查看某
个文件的 inode 信息。
18、stat、fstat、lstat 函数
函数接口:
int stat(const char* filepath,struct stat* buf):获取指定文件路径文件的
inode 信息,存在 buf 中
int
lstat(const char* filepath,struct stat* buf):当给定的文件不是符号
链接时,和 stat 一样,否则 stat 将返回
链接所引用的文件 inode,而 lstat 返回链接本身的 inode
int
fstat(int filedes,struct stat* buf):获取指定文件描数字的 inode 信息,
保存在 buf 中
19、文件类型
文件类型由 stat 结构的 st_mode 给出,unix 文件有普通文件、目录、符号链接、特别文件、
FIFO、套接字等。
目录:目录是一种特殊的文件,但是用户不能写目录。目录是由目录登记项组成的一张表,目录中
4的每个文件和子目录在其中有一个登机项,每个
登记项用来映射文件名到它的 inode,结构很简单,一个 inode 号和对应的文件名。
-------------------------------------------------------------------------
-----------------------------------------------------
链接:unix 文件系统提供一种使得多个文件名表示同一个文件的机制,这种机制就称为链接。系统
简单的通过在目录中建立一个新的登机项来表示这种
链接,这个登记项有一个新的文件名字和要链接的 inode 号。一个文件无论有多少个链接,磁盘上
只有一个描述文件的 inode,每个文件的链接数保存在
stat 结构中的 st_nlink 上面,最大允许的链接数量是:LINK_MAX
硬链接(链接)的缺点:
(1)、inode 号是系统内部管理的一个称为 i_list 表格的索引,unix 为每个操作系统维护着一
个 i_list 表格,所以,inode 号在跨文件系统时 inode 号
可能相同,所以硬链接不能进行在跨文件系统之上
(2)、只有超级用户可以创建目录的硬链接
函数接口:
int link(const char* exitingpath,const char* newpath)
----------------------------------------------------------------------------
--------------------------------------------------
符号链接:符号链接是指向例外一个文件的特殊文件,是为了克服硬链接不能跨文件系统的缺陷而
设计的,符号链接不是指向 inode,符号链接有自己的
inode,符号链接保存所指向文件的路径名。而且,符号链接可以指向一个不存在的文件,但是直
到这个文件被创建,这个链接才能有效,当然反过来,
如果一个符号链接所指向的文件被删除了,那么这个符号链接依然存在,相当于指向了不存在的文
件,直到这个文件再次被创建的时候这个符号链接才是
有效的!
函数接口:
int symlink(const char* path,const char* sympath):创建一个符号链接文字
sympath,这个链接文件指向 path
int readlink(const char* pathname,char* buff,int bufsize):读出 pathname
中的内容,保存在 buff 中,然后关闭文件
20、特别文件
什么叫做特别文件呢?
=>特别文件也称为设备文件,特别文件不包含数据,他们的作用是将物理设备映射到文件系统
中的文件名,在 unix 系统中,每一种设备都至少与一个特别文件相
链接,特别文件可以用 mknod 系统调用来创建,并且与内核的一个软件相连,这个软件就称为设备
驱动程序
有两个类型的特别文件。块特别文件和字符特别文件
什么叫做块特别文件呢?
块特别文件与块设备相连,块设备按特定大小并且随机访问的块来执行 I/O,比如硬盘。
什么叫做字符特别文件呢?
字符特别文件与字符设备相连,字符设备可以存储和传递任意大小的数据,有一些字符设备可
以一个字符一个字符的传递数据,比如键盘。
21、文件的类型:文件类型保存在 stat 结构里面的 st_mode 里面,st_mode 包含两种信息,文件类
型和文件方式。
可以用相关的宏来测试是不是属于某种文件
S_ISREG(mode_t m): 普通文件
S_ISDIR(mode_t m): 目录文件
S_ISCHR(mode_t m): 字符特别文件
S_ISBLK(mode_t m): 块特别文件
S_ISLNK(mode_t m): 符号链接
5S_ISFIFO(mode_t m): 管道或者 FIFO
S_ISSOCK(mode_t m):套接字
22、文件的属性和用户组
文件的属性和用户组信息可以在 stat 结构中的 st_uid and st_gid
you can use these function to change the uid and gid of the file:
int chown(const char* pathname,uid_t owner,gid_t group)
int lchown(const char* pathname,uid_t owner,gid_t group)
int fchown(int filedes,uid_t owner,gid_t group)
23、文件访问方式
文件访问方式规定了文件访问权限,一共 9 位文件方式位,他们的含义从左到右分别为:
000 000 000
执行、写、读
分为三组,每组三位,每组中从做到右分别表示可执行、可写、可读。
这三组分别位用户自己、同组成员、其他用户
可以在 stat 里面的 st_mode 下面找到这些信息
----------------------------------------------
S_IRUSR:用户读
S_IWUSR:用户写
S_IXUSR:用户执行
S_IRGRP:...
S_IWGRP:...
S_IXGRP:...
S_IROTH:...
S_IWOTH:...
S_IXOTH:...
------------------------------------------------
24、改变文件创建屏蔽
函数接口:
int chmod(const char* filename,mode_t mode)
int fchmod(int filedes,mode_t mode)
25、检验文件是否具有某种权限:
函数接口:
int access(const char* filename,int how):判断调用进程是否允许按参数 how 指定
的方式访问 filename 指定的文件
其中,how 的取值可以为下列:
----------------------------
R_OK: test the read root
W_OK: test the write root
X_OK: test the exe root
F_OK: test exiting
----------------------------
注意:access 函数是根据进程的实际用户 ID 和实际组 ID,而不是有效用户 ID 和有效组 ID 来检
查调用进程是否具有该权限,如果允许
则这个函数将返回 0,否则将返回-1
26、截断文件
上面叫做截断文件呢?
=>截断文件就是将已经打开的文件缩减成我们指定的长度
函数接口:
6int ftruncate(int filedes,off_t length)
int truncate(const char* pathname,offset_t length)
这两个函数都是将已经打开的文件截断位 length 字节的大小的文件
27、文件的时间
应用可以利用 utime 函数来改变文件的时间,还有一个更加准确的函数是 utimes
函数接口如下:
int
utime(const char* pathname,const struct utimebuf* times)
the struct utimebuf is like that:
struct utimebuf{
time_t actime;//文件的访问时间
time_t modetime;//文件的修改时间
};
int utimes(const char* path,const struct timeval values[2])
the struct timeval is like that:
struct timeval{
long tv_sec;//second num
long tv_usec;//micsecond num
};
28、文件的删除与换名字
函数接口:
int unlink(const char* path):用于删除一个文件名,并且减少该文件名的链接计数,
如果链接计数为 0,文件的内容便被删除
普通用户是不能调用 unlink 来删除一个目录的,但是有其他的函数接口可以解决:
int rmdir(const char* pathname)
int remove(const char* pathname)
29、修改文件或者目录的名字
函数接口:
int rename(const char* oldname,const char* newname):将 oldname 修改位
newname
30、目录操作
工作目录:每个进程都有一个目录与之相连,这个目录就是工作目录,下面的函数接口可以获取或
者修改进程的工作目录
char* getwd(char* pathbuf)
char* getcwd(char* pathbuf,size_t size)
int
chdir(const char* pathname)
int
fchdir(int filedes)
---------------------------------------------------
关于创建目录与删除目录
函数接口:
int mkdir(const char* pathname,mode_t mode)
int rmdir(const char* pathname)
-----------------------------------------------------
读目录流(用户是不可能写目录的,只能读):
需要说明的两种数据结构:
DIR 数据结构:这就是目录的结构
dirent 数据结构:每一个目录项的数据结果,至少包括 inode 号和文件名字
函数接口:
DIR*
opendir(const char* dirname)
struct dirent*
readdir(DIR* dirp)
7int
closedir(DIR* dirp)
31、扫描命令行的参数
值。
函数接口:
int
getopt(int argc,char** argv,const char* optstring)
extern
char*
optarg
extern int
optind,opterr,optopt
说明:参数 optstring 给出合法的选项字符,选项字符后面可以跟着冒号 ‘ : ’ 以表示要求有选项
例如:当 optstring="if:ls" 时表示允许选项 -i,-f,-l,-s,而且-f 后面要求有选项
该函数每次获取一个选项,如果存在选项,则该函数返回获取到的选项,对于那些接受选项值
的选项,
它同时设置外部变量 optarg 指向选项值。
当遇到位置选项字符或者缺少选项值时,getopt 返回 ‘ ? ’ ,同时将该选项字符存储于 optopt 中。
getopt 每次处理 argv 数组中的一项,同时设置外部变量 optind 为下一个要处理的元素的下标,

没有更多需要处理时,函数将返回-1
32、环境表
unix 系统用一个称为环境表的数据结构来表示所有的环境变量和变量值,它是一个指针,该数组的
每一个元素
都指向一个形如 “ name=value”的字符串,name 就是环境变量,而 value 就是环境变量的值
我们可以通过系统定义的全局变量 environ 来引用环境表,该变量称为环境指针,我们需要说明如
下语句来引用
系统的环境表:
extern char** environ;
这样我们就可以查看环境变量及其值了。
当然我们不会这么傻,我们有函数接口来直接获取我们需要的环境变量的值,甚至是设置新的环境
变量和值
函数接口:
char* getenv(const char* name):就是取得名字叫做 name 的环境的值,如果不存在则
返回 null
int
putenv(char* str):设置环境变量,str 的格式形如 “ name=value”
33、终止进程
我们可以使用 exit 函数和_exit 函数来终止进程,但这两个函数有一些不同,exit 会在终止之前
清理一些东西,在调用
_exit 函数返回到内核中,而_exit 函数直接终止进程并且返回到内核,这是很不安全的,不推荐
使用,其次,我们可以
使用 exit(EXIT_SUCCESS) and exit(EXIT_FAILURE)来表示程序是否是正常退出,更加复杂
的退出参数自己扩展把!~
函数接口:
void exit(int status);
void _exit(int status);
那么我们现在来说说 exit 函数吧,既然 exit 函数需要在退出之前做一些事情,那么这些事情是由
什么人来做的呢?
这时候我们就需要知道一个注册函数啦=>
int atexit(void(*func)(void))
参数是一个函数指针,没有参数,以参数 func 调用 atexit 导致 func 所指向的函数被注册为程序
正常终止时要执行的函数
,所有注册的函数以与注册的顺序相反的顺序被调用,并且与调用的次数相同。
834、关于流产程序 abort
真的非常有趣,为什么叫 abort 为流产程序呢?
首先,用 abort 来终止程序肯定是异常终止程序,但是有趣的事情是 abort 函数将在工作目录中生
成一个内存转储文件 core
abort 会在退出之前关闭所有已经打开的流,其实所有的异常终止都是由信号造成的,abort 也是
通过生成一个信号来
流产程序的。例子将在后面介绍......
void abort(void)
35、进程的存储空间
进程的地址空间和 PCB 反应了进程所运行程序的当前状态,当进程用 exec 函数装入一个新进程时,
内核便为这个新的进程
创建了一个新的地址空间,进程的地址空间会有多个组成部分,如下:
正文段:存放程序的执行代码,是只读的所以可以防止被恶意写
初始化数据段:那些声明了而且已经初始化的数据变量,如 int hujian=88888
为初始化数据段:(bss 段),没有初始化的变量,由内核赋予初始值
栈:存放函数内的变量等
堆:用于存放动态申请的变量
36、动态存储与释放
函数接口:
void *malloc(size_t size):这个函数将在申请一块大小为 size 的存储空间,但是这个存储
空间的初始值是不确定的,调用成功则
返回分配的首地址,并且保证是严格对齐的,调用失败则返回 null 并且设置 errno 位 ENOMEM
void *calloc(size_t number_of_elem,size_t elem_size):很明显这个函数用来为结构
数组分配空间,第一个参数为数组的大小,
第二个参数位数组元素的大小,分配成功将用 0 来填充,并且返回指向第一个元素的指针
void* realloc(void* ptr,size_t newsize):用于增加或者减少已经分配的内存大小,第
一个参数是已经获得的内存指针,第二个
参数是要重新分配的内存大小,如果连续的延伸能够满足继续分配,函数将不会移动数据,但是如
果不能满足的话,那么该函数将重新
寻找一块内存,并且将数据搬迁到新的数据块上面,然后返回新存储块的指针,所以千万注意不要
丢失指针喔!
void* memalign(size_t size,size_t bound):这个函数是由用户字节定义对齐字节,第一
个参数是要申请的内存大小,第二个参数是
对齐的字节
void* valloc(size_t):该函数只是对 memalign 函数的特殊使用,对齐是在页边界,实现如下:
void* valloc(size_t size){
memalign(size,PGSIZE);
}
37、一个很好玩的函数(两个)
函数接口:
int
setjmp(jmp_buf env):设置返回点
void
longjmp(jmp_buf env,int val):返回 env,val 将作为 setjmp 的返回值,如
果为 0,则 setjmp 仍然返回 1
虽然很好玩,但是因为和编译器密切相关,一般应用很难用到,就不再赘述啦~
38、进程资源
(1)、上面叫做资源呢?简单的讲资源就是能够影响进程的一些因素,比如进程最大存储空间,可
使用的 CPU 时间等等,如果
这些东西没有限制的话,那么一个进程就舒服了,但那肯定是不可能的,我们能做上面呢那?我们应
该知道的一个事实就是,资源
是有限制的,那系统规定的资源限制的最大值称为硬限制,我们不能超过这个限制,但是我们可以
修改我们自己的进程的最大限制,
我们修改的最大限制称为软限制,但是软限制的最大值不能大于硬限制,我们可以减小我们自己的
硬限制,但是这是不可逆转的!
9(2)、我们可以查看和设置资源限制
函数接口:
int
getrlimit(int resource,struct rlimit* rlptr):很明显这是我们获取资
源限制的函数啊
int
setrlimit(int resource,const struct rlimit* rlptr):很明显是设置资
源限制啊
我们需要知道的结构:
struct rlimit{
rlim_t rlim_cur;//这是资源限制的软限制
rlim_t rlim_max;//给出资源限制的最大值,也就是硬限制
};
对了,rlim_t 其实就是一个长整形...
对了还有,函数里面有一个参数叫做 resource,那是上面呢?就是资源啊,资源可以取下面这些
常数:
-------------------------------------------------------
RLIMIT_AS:进程可用虚拟空间的最大字节数
RLIMIT_CORE:core 文件的最大字节数
RLIMIT_CPU:进程能够使用的 CPU 时间
RLIMIT_DATA:进程数据段空间的最大值(初始化数据段,为初始化数据段,堆空间之和的最大
值)
RLIMIT_FSIZE:进程能够创建的最大文件的字节数
RLIMIT_MEMLOCK:可以用 mlock 和 mlockall 锁住在物理存储器的最大虚拟存储字节数
RLIMIT_NOFILE:进程最多能够打开的文件个数
RLIMIT_NPROC:最多能够创建的子进程个数
RLIMIT_RSS:进程应当得到的最大物理内存页数
RLIMIT_STACK:栈的最大字节数
RLIMIT_NLIMITS:系统中不同资源个数
RLIMIT_INFINTY:展示一个常数,表示对指定的资源没有限制
----------------------------------------------------------
(3)、资源使用统计
函数接口:
int
getrusage(int who,struct rusage* rusage):报告当前进程或者该进程的所
有已经终止并且要等待的子进程
的资源使用情况,参数 who 指明是当前进程还是子进程,只能取如下两个值:
---------------------------
RUSAGE_SELF:调用此函数的进程
RUSAGE_CHILDREN:该进程所有已经终止并且在等待的子进程
---------------------------
39、用户信息
用户名:函数接口:char* getlogin(void):返回用户名
用户数据库:
用户数据库将保存所有的用户信息,在现在的 unix 中有两个文件属于保存用户信息的:
/etc/passwd and /etc/shadow
前者称为口令数据库文件,对所有用户开放。后者称为口令影子文件,只有特权用户才能访问。
这连个文件中,没一行都由七个区域组成,两个区域之间用冒号隔开,格式如下
------------------------------------------------------------------
user_name:command(x means no
command):user_id:user_group_id:user_name,address,telephone_job,telephone_hom
e:user_dir:shell
这七个域由一个叫做 passwd 的结构来表示:
struct passwd{
char* pw_name;
char*
pw_passwd;
uid_t pw_uid;
gid_t
pw_gid;
10char*
pw_gecos;
char*
pw_dir;
char* pw_shell;
}//end of struct
我们有两种方式来读取数据库文件,第一种是给定用户名或者用户 id 的情况下查找特定的一项,如
果没有则返回 null
struct passwd*
getpwuid(uid_t uid):根据 uid 来查找信息
struct passwd*
gtpwnam(const char* name):根据用户名来查找信息
下面一种方法就是扫描整个数据库,一个一个用户的读所有用户的数据项
函数接口:
struct passwd*
getpwent(void)
void
setpwent(void)
void
endpwent(void)
有趣的是,每次调用 getpwent,都是指向下一个信息,第一次调用是返回首个信息,然后再
次调用则返回
下一个信息,如果没有了,就返回 null
setpwent 和 endpwent 分别打开和关闭数据库
------------------------------------------------
struct passwd* getpwnam(const char* name)
{
struct passwd* ptr;
//open the database
setpwent();
while((ptr=getpwent())!=NULL){
if(strcmp(name,ptr->pw_name)==0){
break;
}//end if
}//end while
//close the database
endpwent();
return (ptr);
}
--------------------------------------------------
组数据库:
和用户数据库一样 /etc/group and /etc/gshadow 是组数据库,前者包含每个组的信息,
后者包含
系统中所有组的信息每个组对应一行,每一行有四个域,中间有冒号隔开:
如:user:x:500:hujian,libai
lib:x:190:hujian
第一个域是组名,第二个是组 id,第三个域是口令,没有则位 x,第四个是组成员,成员间用
逗号隔开,简单说一下,hujian 不仅属于
user 这个组,还属于 lib 这个组,user 是基本组,lib 是附加组
函数接口:
在看函数接口之前,先看看 group 数据结构:
struct group{
char* gr_name;//group name
char*
gr_passwd;//group passwd
int
gr_gid;//group id
char** gr_mem;//point to member of group
};
下面就是函数接口:
struct group*
getgrgid(gid_t gid):get the group by gid
11struct
struct
void
void
group*
getgrnam(const char* name):get the group by name
group*
getgrent():one by one...
setgrent():open the database
endgrent():close the database
获取附加组 id
函数接口:
int
getgroups(int gidsetsize,gid_t group_list[]);
该函数把调用进程的附加组 id 填进 group_list 中,第一个参数则是限定第二个参数的大小,
实际的附加组个数
由函数返回,如果只是想直到附加组的个数,那么就指定第一个参数位 0,那么第二个参数就不
会被填充。
40、进程的身份凭证
unix 系统提供了三组 id 来表示进程身份凭证
----------------------------------------------
第一组:指明实际用户是谁
->实际用户 id,实际组 id
第二组:用于操作和文件访问权限测试
->有效用户 id,有效组 id,附加组 id
第三组:由 exec 保存
->保存的调整用户 id,保存的调整组 id
-----------------------------------------------
相关说明:
=>实际用户 id 和实际组 id:由 login(1)程序在用户注册时设置。
=>有效用户 id 和有效组 id:是进程当前起作用的 id,也称为现行用户 id 和现行组 id,exec 加
载执行文件时,有效用户 id
和有效组 id 默认设置成实际用户 id 和实际组 id,但是如果文件设置了调整用户/组 id 位以指明该
文件是要以调整用户
id 或者是调整组 id 的方式来执行的话,exec 加载该文件时会将有效用户 id 或者是有效组 id 改为
文件拥有者的用户 id
或者组 id。
有效用户 id 和有效组 id 影响文件的创建和访问,文件创建期,内核将文件的拥有者设置为进程的
有效 id 和有效组 id,
文件访问时,内核根据进程的有效 id 和有效组 id 来判断进程是否有权限访问文件。
=>保存的调整用户 id/组 id:exec 在调整进程 id 之前,先保存进程的当前有效 id,以后可以恢
复。
函数接口:
uid_t
uid_t
gid_t
gid_t
gid_t
getuid():返回进程的实际用户 id
geteuid():返回进程的有效用户 id
getgid():返回进程的实际组 id
getegid():返回进程的有效组 id
getgroups(int count,gid_t groups[]):获取进程的当前附加组 id
关于调整进程的身份:
进程总是可以调整它的有效用户 id/组 id 回到实际用户 id/组 id
函数接口:
int setuid(uid_t uid):如果进程有特权,那么设置进程的实际用户 id 和有效用户 id
为 uid,否则只设置有效
用户 id 为 uid,而且 uid 要么等于实际用户 id,要么等于保存的调整用户 id
int setgid(gid_t gid):same as setuid
int setreuid(uid_t r_uid,uid_t e_uid):-1 代表不设置,其他情况都设置
int setregid(gid_t r_gid,gid_t e_gid):same as setreuid
int seteuid(uid_t uid):只设置有效用户 id,而且要有特权才可以设置位其他,否则只
能设置位实际用户 id 或者保存的调整用户 id
12int
setegid(gid_t gid):same as seteuid
获取进程当前有效用户 id 的用户名:
函数接口:
char*
cuserid(char* string)
注意:getlogin 得到的是运行进程的用户注册名,cuserid 得到的是与进程有效用户 id 相
连的用户名。
41、进程标志
unix 中每个进程都有一个唯一的进程标志,称为进程 id 或者 pid,pid 的类型是 pid_t,它是整
数类型
函数接口:
pid_t getpid():可以获得进程的 pid
pid_t
getppid():可以获得父进程的 pid
关于进程创建:
pid_t
fork():由进程 fork 出一个和自己一模一样的进程,fork 成功时,子进程的返回
值为 0,父进程的返回值
是新创建的子进程的 pid,若 fork 失败,则返回-1 并且将设置 errno 以告知错误信息。
注意,fork 会返回两次,一次是返回子进程,一次是返回父进程,需要用 if 来判断是返回哪
个进程。
if(pid==0) child else father.
42、fork 出来的子进程和父进程之间的区别
相同点:实际用户 id,实际组 id,有效用户 id,有效组 id,附加组 id,会晤期 id 和控制终端,
调整用户 id 标志和调整组 id 标志,当前
工作目录和根目录,文件创建屏蔽,信号屏蔽与设置,任何打开的文件描数字和执行时关闭标志,
环境变量,所有相连的共享存储段,
资源限制。
不同点:有自己唯一的 id,有各自不同的付父进程 id,进程操作文件描数字不会影响父进程,但文
件位置相同,子进程耗费的时间量
被设置为 0,子进程不继承父进程的文件锁,定时器,任何悬挂信号将被清除,但继承信号屏蔽和信
号动作。
vfork:
pid_t
vfork():该函数和 fork 函数一样,不同的有两点,fork 创建进程的整个地址空
间副本,而 vfork 不复制这一个副本,
第二个是 fork 允许父进程和子进程相互独立的运行,但是 vfork 不允许,vfork 在借出父进
程的地址空间给子进程的同时阻塞
父进程,父进程被悬挂直到子进程调用了 exec 或者_exit,此时内核返回地址空间给父亲进程
并且将其唤醒。
现在我们应该直到一件事情就是,什么叫做 “ 写- 复制 ” ,用这样的方法时,父进程的数据和栈空间临
时称为只读的并且标志位 “ 写- 复制 ” ,
子进程一开始与父进程共享存储页,如果子进程或者父进程企图修改某页时,缺页中断将会出现,
于是复制该页成为新的可写页,这样,
只有那些修改了的也需要复制,而不是复制整个地址空间,如果子进程调用 exec 或者 exit,这些
也将会转换为他们原来的保护并且将
“写- 复制 ” 去掉。
43、执行程序(装载程序)
exec 函数组:
int execl(const char* path,const char* arg0,...,(char*)0):
int
execlp(const char* file,const char* arg0,...,(char*)0):
int
execle(const char* path,const char* arg0,...,const char*
envp[]):
13int
int
int
envp[]):
execv(const char* path,const char* argv[]):
execvp(const char* file,const char* argv[]):
execve(const char* path,const char* argv[],const char*
一点解释:p 结尾的函数意味着第一个参数是文件名 file,并且使用 PATH 环境变量来寻找可执行
文件。
l 结尾的函数意味着函数中含有新程序的参数表
e 结尾的函数具有 env 数组作为参数,而不适用当前的环境变量表
44、等待进程完成
函数接口:
pid_t
wait(int *stat_loc):该函数首先检查调用进程是否有任何已经终止了的子
进程,
如果有的话,它立刻返回,如果没有的话,wait 将阻塞调用进程直到有一个子进程结束,
wait 将返回
已经终止的子进程的 pid,并且将该进程的终止状态存储在 stat_loc 中,当调用进程同时有
多个子进程
终止时,wait 将任意返回一个,如果调用进程没有任何子进程,函数将返回-1,并且设置
errno
下面是一些用于检查 wait 或者 waitpid 返回状态的宏定义:
-----------------------------------------------------------------------
WIFEXITED(stat_val):正常终止,则位真
WIFSIGNALED(stat_val):异常终止,值为真
WIFSTOPPED(stat_val):是被停止的子进程,则为真
WEXITSTATUS(stat_val):可以获得个体 exit 传递的出口状态
WTERMSIG(stat_val):引起子进程终止的信号数量
WSTOPSIG(stat_val):引起子进程停止的信号数
------------------------------------------------------------------------
45、通过两次调用 fork 来避免僵死进程
原理:
第一个子进程调用 fork 生成另一个子进程来执行程序并先于第二个子进程
而终止,由于第一个子进程的父进程调用了 waitpid,它不会成为系统的
僵死进程。而第二个子进程没有调用 wait 来等待,但是由于父亲进程(第
一个子进程)先于它而终止使得它被 init(pid=1)继承,这时它是活跃的
所以不会称为僵死进程,当它要终止时,init 进程将调用 wait 释放它的
proc 结构,这是多么神奇的事情啊
46、system 函数
这是一个集成的函数,它可以实现由一个程序执行另一个程序
函数接口:int sysytem(const char* command)
47、获取和设置进程组
什么叫进程组呢?
我们只需要直到进程组有唯一的进程组 id,每个进程组有一个进程组长,进程组长的进程 id 也就
是进程组
的进程 id,下面就是函数接口:
pid_t
getpgrp():获取进程组的 id
int
setpgid(pid_t pid,pid_t pgid):改变进程 pid 的进程组 id 为 pgid
48、会晤期
会晤期是一个或者多个进程组的集合,每一个进程属于一个会晤期和一个进程组。每个会晤期有一
个会晤期主席,它就是
14创建会晤期的那个进程,下面是一个可以创建一个会晤期的函数接口:
pid_t setsid();
需要注意的是,当会晤期主席终止时,它会结束会晤期。
49、控制终端
每一个会晤期可以有一个控制终端,进程可以通过控制终端进行输入输出等操作。
需要注意的几点是:
每个进程可以有一个控制终端。
每个会晤期可以控制终端也可以没有控制终端。
下面这个函数将可以得到控制终端的文件名:
char*
ctermid(char* ptr):将保存在 ptr 中,但是如果 ptr 是空指针的话,那么该
函数将返回得到的
控制终端文件名,一般是 /dev/tty
50、信号处理
上面叫做信号呢?
在 unix/linux 里面,信号是作为通知进程发生了某种事件的手段,并且其发生常常与进程当
前的活动无关,信号也称为
软终端,它提供了一种处理异步事件的方法,产生信号的原因有很多,比如:
(1)、用户按下了某个终止键
(2)、程序异常
(3)、kill 函数允许进程发送任何信号给其他进程或者进程组
(4)、当发生了某种必须让进程知道的事件时,也会产生信号
(5)、企图读写终端的后台进程会得到作业控制信号
(6)、当进程超越了 CPU 或文件大小的限制时
生成信号的事件可以归为三类:程序错误,外部事件,显示请求
信号的生成可以是同步的也可以是异步的,同步信号与程序中的某个操作有关并且在那个操作
进行的同时产生,异步信号
是进程之外的事件生成的信号,一般外部事件总是生成异步信号。
当一个信号到达进程时,进程可以选择如下动作来处理到达的信号:
(1)、忽略信号
(2)、捕获信号
(3)、执行系统默认动作
(4)、流产
(5)、终止
(6)、忽略
(7)、挂起
(8)、继续
一些术语:
生成信号(发送信号):信号出现
交付(接收):进程识别了信号
悬挂:信号已经生成,但是还没有交付
对于每一种信号,在某个时刻它对应的动作安排称为信号的布局。
信号屏蔽字:每个进程有一个信号屏蔽字,就是来限制信号的识别,只识别我们需要的信号就可以
了。
51、打印信号数对应的描述信息
函数接口:
void psignal(int signo,const char* msg):
52、产生信号
15的
函数接口:
int
raise(int sig):简单的发送信号 sig 给调用它的进程,如果安装了信号 sig
信号句柄,则 raise 函数将在句柄函数返回之后才返回
int
kill(pid_t pid,int sig):向进程 pid 发送信号,sig 为 0 则表示空信号,空信
号一般用来
检测 pid 的合法性,如果进程 pid 不存在的话,向这个进程发送空信号是不会成功的,函数将
返回-1 并且设置
errno 信号,函数成功则返回 0,,参数 pid 可以是下面的四种情况之一:
(1)、pid>0:信号发送给进程号为 pid 的进程一个 sig 信号
(2)、pid==0:发送给所在进程组的所有进程 sig 信号
(3)、pid<-1:信号发送给进程组 id 为 |pid| 并且发送者有权向他发送信号的进程一个
sig 信号
(4)、pid==-1:广播信号,到所有有权向他发送该信号的进程
53、设置信号的动作
首先给出函数接口:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler):参数 signum 指
明是哪一种信号,handler 就是我们
要做的动作,可以是下列三种情况之一:
(1)、SIG_DFL:采用默认动作,由系统指定动作函数
(2)、SIG_IGN:忽略该信号,但是有一些信号是不能忽略的,比如 SIGKILL,SIGSTOP
(3)、信号句柄,也就是我们自己定义的函数指针
需要注意的是,信号句柄应该是一个只有一个参数而且没有返回值的函数,参数指明信号的类
型。
当 signal 函数调用成功时,返回值就是上述三种情况之一,如果调用出错,那么它将返回
SIG_ERR
并且设置 errno。
54、sigaction 函数
函数接口:
int sigaction(int signum,const struct sigaction* act,struct
sigaction*oact):
struct sigaction{
void
(*sa_handler)();
void
(*sa_sigaction)(int,siginfo_t*,void*);
sigset_t sa_mask;
int
sa_flags;
};
和 signal 函数一样,sigaction 函数的第一个参数用来指定信号,第二个参数是新的信号动
作,为 null
时则不改变信号的动作,第三个参数将获得老的信号动作。
55、一个编译器的修饰符
volatile:告诉编译器这个变量会在某些地方可能会被修改,所以编译器就不会对这个变量做
一些不必要的优化。
56、sigset_t 类型和信号集操作
信号屏蔽是被阻塞的信号集合,unix 中表示信号集合的数据类型是 sigset_t,下面是操作信号集
合的函数接口:
int sigemptyset(sigset_t *set):初始化信号集合为空,即不包含任何信号,总是返回
0
16int sigaddset(sigset_t* set,int signo):向 set 中增加 signo 信号,成功返回 0,
失败返回-1
int sigdelset(sigset_t* set,int signo):从 set 中删除 signo 信号,成功返回 0,
失败返回-1
int sigismember(const sigset_t* set,int signo):测试 signo 是否在 set 中,若
在返回 1,不在返回 0,失败返回-1
57、设置信号屏蔽
函数接口:
int
sigprocmask(int how,const sigset_t* set,sigset_t *oset):用于改变
或者测试调用进程的信号屏蔽。
如果 set 不是 null 的话,它指向用于改变信号屏蔽的信号集合,此时参数 how 指明如何改变
信号屏蔽,oset 将返回原先
的信号屏蔽,下面是 how 的可取值:
(1)、SIG_BLOCK:阻塞 set 所指向集合的信号,即将它们加入到当前的信号屏蔽中
(2)、SIG_UNLOCK:放开 set 所指信号集合中的信号,即将它们从当前信号集合中除去
(3)、SIG_SETMASK:用 set 所指项的信号集合作为新的信号屏蔽集合,抛弃原来的信号屏

58、检查悬挂信号
函数接口:
int sigpending(sigset_t* set):将悬挂信号保存在 set 中
59、等待信号
函数接口:
int
pause(void):仅仅是等待信号出现,放在循环里面用,用来等待信号出现
int
sigsuspend(const sigset_t* sigmask):用参数所指项的信号集合临时替代调
用进程的信号屏蔽,
然后挂起调用进程直到有不属于 sigmask 的信号到达
60、信号栈
所谓信号栈就是信号句柄所在的栈空间,一般情况下信号栈和用户栈在一个位置,所以不安全,
我们需要
建立自己的信号栈来安放信号句柄。
函数接口:
int
sigaltstack(const stack_t* ss,stack_t* oss):很明显 ss 是我们自己指定
的信号栈,oss 是原来的信号栈
stack_t 至少包含一个栈开始地址,栈大小,和栈状态标志,对于栈状态标志,只能取下面两
种情况:
SS_ONSTACK:表示栈将开始生效,ss_sp+ss_size 这块空间就是我们定义的信号栈
SS_DISABLE:这表示不用此栈,此时忽略 ss
函数成功成功返回 0,失败返回-1,这个函数只是准备好了一个信号栈,至于哪些信号能用这
个信号栈,我们需要在
用 sigaction 函数时通过 SA_ONSTACK 标志来指定。
61、关于时间的函数接口
time_t
time(time_t* tloc):返回从 unix 纪元开始的总秒数,如果 tloc 不是空指针,
那么时间也可以保存在 tloc 里面
double
difftime(time_t time_current,time_t time_start):计算两个时间之间的
间隔秒数
int
gettimeofday(struct timeval* restrict tp,void *restrict tzp):
struct
timeval{ time_t tv_sev;suseconds_t tv_usec;};
上面这个函数将返回更加精确的时间,保存在 tp 中。
int
settimeofday(const struct timeval* tp,const void* tzp):设置时间,tzp
在 linux 下应该位空。
int
adjtime(const struct timeval* delta,struct timeval*
olddelta):这个函数用来调整系统时钟,
delta 就是我们需要调的时间数量,为负数表示要调慢,正数表示要调快。
17struct tm* gmttime(const time_t* time):将 time 转换为可读形式
struct tm*
localtime(const time_t* time):得到本地时间
time_t
mktime(struct tm* brokentime):将可读的时间转换为秒
62、格式化日期与时间
char*
asctime(const struct tm* tmptr):返回字符串形式的时间
char*
ctime(const time_t* timeval):是将原始日期转换为可读形式,返回结果和
asctime 一样
size_t
strftime(char* s,size_z maxsize,const char* fomat,struct tm*
timeptr):
这个函数和 sprintf 函数一样,将时间 tmptr 按照 fomat 的格式保存在 s 数组中,maxsize 为 s
的大小,函数将返回实际
转换的字符数量,但是不包括终止符号,所以如果 maxsize 等于返回值,那么说明数组大小太小了。
char*
strptime(const char* buf,const char* fomat,struct tm* timeptr):
这个函数是第一个函数的逆函数
63、CPU 时间和墙钟时间
程序的运行时间有 CPU 时间和墙钟时间,CPU 时间就是程序占用 CPU 的时间,是一个固定值,
墙钟时间表示进程从开始到
运行结束的时间,可能包含了其他进程运行的时间,所以不能用墙钟时间来表示程序的性能。
我们可以用下面的函数接口来实现得到 CPU 时间,其实 time 函数只能得到墙钟时间:
clock_t
clock():返回进程的当前 CPU 时间,包括用户时间和系统时间,类型
clock_t 表示系统内部的
时间单位(1/CLOCKS_PER_SEC),CLOCKS_PER_SEC=1000000,为了统计程序运行的 cpu
时间,一般在程序的开始和结束
处调用 clock,然后两次调用的结果相减。
64、times 函数
函数接口:
clock_t
times(struct tms* buffer):此函数和 clock 函数一样返回时间,但
是返回的时间更加精确,函数
的返回值为当前的墙钟时间,我们应该知道的事情是,系统时钟发出一次中断信号称为一个滴
答,我们可以用函数
sysconf(_SC_CLK_TCK)来得到这个滴答值,我们来看看 tms 的信息:
struct
tms{
clock_t
tms_utime:用户态下执行进程用去的 CPU 时间
clock_t
tms_stime:系统为进程服务用去的时间
clock_t
tms_cutime:子进程的 tms_utime+tms_cutime
clock_t
tms_cstime:子进程的 tms_stime+tms_cstime
};
65、睡眠函数
unsigned int sleep(unsigned int seconds):睡眠 seconds 秒或者捕获了某种信号
并且信号句柄已经返回
66、设置定时器
每一个进程都有三个独立无关的间隔定时器:
墙钟定时器(ITIMER_REAL):到期会发送 SIGALRM 信号个给进程
虚拟定时器(ITIMER_VIRTUAL):仅当进程运行在用户态时才走动,会发送 SIGVTALRM 信号
剖面定时器(ITIMER_PROF):用户态或者内核态都会走,会发送 SIGPROF 信号
函数接口:
unsigned int alarm(unsigned int seconds):一次性的定时器,只能设置墙钟定时器,
每一个进程只有一个
墙钟定时器,再次设置时将返回上一次未完成的秒数。
18int
setitimer(int which,const struct itimerval* value,struct
itimerval *oldvalue)
int
getitimer(int which,struct itimerval* value)
struct
itimerval{
struct timeval it_interval;//定时间隔时间
struct timeval it_value;//定时开始时间
};
参数 which 指明是哪一种定时器,value 就是我们设置的定时器,它是一个连续的定时器,函
数将会返回上一个
定时器在 oldvalue 里面,第二个函数可以得到定时器
67、实时时钟与设定
unix 里面设定有多种类型的时钟,每一种时钟用时钟 id 来标志,这个时钟 id 是
clockid_t(int),在 linux 中,有下列的时钟:
CLOCK_REALTIME:这是系统范围内的时钟
CLOCK_MONOTONIC:在 linux 里面,是从系统启动以来的时间,不能改变
CLOCK_PROCESS_CPUTIME_ID:这是进程范围的时钟,他给出进程特定的 cpu 时间
CLOCK_THREAD_CUPTIME_ID:这是线程范围的时钟,给出线程特定的 cpu 时间
struct timespec{
time_t tv_sec;//seconds
long
tv_nsec;//nseconds
};
函数接口:
int
clock_getres(clockid_t clock_id,struct timespec *res):得到特定时
钟的分辨率,出错将返回 1
int clock_gettime(clockid_t clock_id,struct timespec* res):得到当前时间
int clock_settime(clockid_t clock_id,const struct timespec* res):设定时

68、实时睡眠
函数接口:
int
nanosleep(const struct timespec* req,struct timespec* rem):和
sleep 一样,剩余时间将存在 rem 中,成功返回 0,失败返回-1
69、创建和删除实时定时器
int timer_create(clockid_t clockid,struct sigevent* restrict
evp,timer_t* restrict timerid):
第一个参数是时钟类型,第二个参数用于设定支持此定时器的信号交付,第三个参数存放生成
的定时器 id
int
timer_delete(timer_t timeid):删除 timeid
70、设置实时定时器
int
timer_gettime(timer_t timerid,struct itimerspec
*value):value 中将保存定时器剩余时间
int
timer_settime(timer_t timeid,int flags,const struct
itimerspec* restrict value,
struct itimerspec* restrict ovalue):就是设置,flags=TIMER_ABSTIME
71、定时器超期计数
什么叫做超期计数呢?
就是定时器信号没有得到及时处理的次数。
int
timer_getoverrun(timer_t timeid);
1972、与终端设备有关的两个函数接口
int
isatty(int fd):如果文件描述字 fd 与一打开的终端设备相连,则函数返回
1,否则返回 0
char* ttyname(int fd):如果文件描述字 fd 与一个打开的终端设备相连,则返回那
个终端的名字,否则返回 NULL
73、文件锁
文件锁有读锁和写锁:读锁可以是多个,他的目标是防止其他进程来跟改正在读的文件内容。读锁
也被成为 “ 共享锁 ” 。
写锁只能有一个。他的作用是隔离文件使得他所写的内容保持一致,有互斥锁的区域不能有其他锁。
所以写锁也被称为 “ 互斥的锁 ” 。
函数接口:
int
fcntl(int filedes,int cmd,struct flock* lock):
struct flock{
short l_type;//the lock's type,can be =>F_RDLCK,F_WRLCK,F_UNLCK
off_t l_start;//offset
short l_whence;//where ->SEEK_SET,SEEK_CUR,SEEK_END
off_t l_len;//[l_start,l_start+len]
pid_t l_pid;//the process
};
cmd 可以取得几个命令值:
F_GETLK,F_SETLK.
74、当 len=0 时,他允许锁住文件的任意区域,解这类锁时,需要注意,当我们扩充了文件内容之后,
解锁时的区域长度应当包括新扩充的部分。
75、
我们需要知道的一些关于文件锁和文件与进程之间关系的内容:
(1)、锁是和进程相连接的,我们需要知道什么叫锁,及其设置锁的目的是什么,我们设置锁的目
的就是为了不让别的进程来和自己这个进程
一起访问或者修改文件的内容,所以,就算时 fork 出来的子进程也不会继承父进程的文件锁,
这也是很好理解的。
(2)、锁之和文件相连而不和文件描数字相连,尽管我们设置锁时使用的是文件描数字。这意味着
当进程用描数字来关闭一个文件时,他在此
文件上设置的所有锁都将被释放,即使这些锁是这个进程用其他文件描数字设置的。
(3)、进程终止时,系统会关闭这个进程打开的文件,从而释放该进程设置的所有文件锁。
76、建议锁和强制锁
fcntl 函数不仅可以提供建议锁机制,还可以提供强制锁机制。
建议锁就是建议,内核并不管你是不是遵守建议锁机制,read 或者 write 可以凌驾在建议锁机制
之上。
也就是说,建议锁就是用户自己的锁机制,和内核没有关系,所以在使用建议锁的时候,我们应该
遵守
建议锁的机制。而强制锁则由内核管理,排除一切不合法的文件访问与操作。
强制锁通过设置 id 来设置。
77、信号驱动的 I/O
所谓信号驱动的 i/o,也就是异步 i/o,但本质上还是同步 i/o。
采用信号驱动 i/o,当在描述字上有数据到达时,进程会收到一个信号,此时对该描数字进行输入
20输出不会阻塞。
linux 上有两个可用于信号驱动的 i/o 的信号:
SIGIO,SIGURG,后者只用于通知网络链接上到达了带外数据。
如果在文件描数字上设置了 O_ASYNC,则当该描数字上由 i/o 活动时会生成 SIGIO 信号,另外,对
于套接字,当其上
到达带外数据时,总是会生成 SIGURG 信号。
进程可以调用 fcntl 函数来指定接收信号的进程或者进程组,当然也可以获取接收信号的进程或者
进程组:
(1).set the process/process group for the signal
int fcntl(filedes,F_SETOWN,pid):pid 为正数时,表示进程 id,负数表示进程组 id,出错
返回-1
int
fcntl(filedes,F_GETOWN):返回进程或者进程组,正数是进程,负数是进程组
实现信号驱动的 i/o 的步骤应该为:
(1)、用 signalaction 来建立信号句柄
(2)、用 fcntl 来设置接收信号的进程或者进程组
(3)、如果要接收的是 SIGIO 信号,则需要用 fcntl 与 F_SETFL 来设置文件描数字的 O_ASYNC
标志使得能生成该信号。
78、多路转接 i/o
不少应用需要同时接收来自多个输入通道的输入,并且只要有输入到达就必须接收,这类应用应该
多数是时间驱动的应用。
函数接口:
int select(int nfds,fd_set* rfds,fd_set* wfds,fd_set* efds,struct
timeval* timeout);
select 函数告诉内核,调用他的进程需要等待多种 i/o 事件,并且仅当这些事件中的一个或
者多个出现时,或者
指定的时间已经过去时,才会唤醒调用他的进程。
我们先来解释一下最后一个参数的意义:
(1)、当 timeout=null 时,表明进程愿意永远等待没有事件限制
(2)、当 timeout=0 时则正好相反,表示进程不想等待,检查描数字后立即返回
(3)、当 timeout>0 时,表面进程愿意等待一段时间
nfds 参数指明要测试的描数字的个数,最多可以用宏定义 FD_SETSIZE 来获得
参数 rfds,wfds,efds 分别给出要求内核测试的读、写和另外条件的描数字集合
一些宏定义:
void FD_ZERO(fd_set* fdset):初始化为空集合
void FD_CLR(int filedes,fd_set* fdset):将 filedes 从所描述的集合中清除
int
FD_ISSET(int filedes,fd_set* fdset):判断是否属于,返回 1 表示属于,
0 表示不属于
void FD_SET(int filedes,fd_set* fdset):添加描述字集合
关于函数返回值:
(1)、返回值大于 0,指出三个描数字集合中已经就绪的描数字个数
(2)、等于 0,意味着在有描数字就绪之前计时器已经到期
(3)、-1,意味着出现了错误
所以,只有核实了 select 函数返回值大于 0 时检查集合才有意义。
79、函数 poll
函数接口:
int poll(struct pollfd fds[],nfds_t nfds,int timeout):
poll 和 select 函数在本质上是一致的,poll 检查一组文件描数字以查看是否有任何悬
挂事件,并且可以有选择
的为等待某个描数字上的时间而指定时间。
下面我们看看 pollfd 这个结构:
struct pollfd{
int
fd;//要检查的描述字
short
events;//感兴趣的事件
short
revents;//fd 上发生的事件
21};
poll 将返回 revents 的值,events 和 revents 的取值可以如下:
POLLIN:有可读的数据
POLLPRI:有可读的高优先数据
POLLOUT:可以无阻塞的写数据
POLLERR:出现错误
POLLHUP:出现挂起
POLLNAVL:描数字引用的不是已经打开的文件
fds 指定 nfds 的元素个数,timeout 和 select 一样。
80、异步 i/o
首先我们应该知道什么叫同步和异步 i/o。
同步 i/o:同步 io 操作导致发出请求的进程被阻塞直到 io 操作完成
异步 i/o:异步 io 操作在 io 期间不导致发出请求的进程被阻塞
下面是一些异步 io 函数:
----------------------------------------------------------------
aio_read: 对指定的文件描数字启动读请求
aio_write:
对指定的文件描数字启动写请求
aio_error:
返回指定操作的错误状态
aio_return: 返回已经完成操作的状态
aio_suspend: 悬挂调用进程直到指定的请求中至少有一个完成
aio_cancel: 删除悬挂在文件描数字上的一个或者多个请求
lio_listio: 启动一组 io 请求
aio_fsync:
异步的将系统缓冲区中含义的文件已经修改的数据写到永久存储
----------------------------------------------------------------------
异步 io 控制块:这是一个数据结构,用来传递给内核所需要的信息,因为异步 io 是由内核完成的。
我们就来看看这个数据结构吧:
struct aiocb
{
int
aio_fildes;//要读写的文件描数字
off_t
aio_offset;//文件位置
volatile void* aio_buf;//数据缓冲区的地址
size_t
aio_nbytes;//要传递的字节数
int
aio_reqprio;//请求优先位移
struct sigevent
aio_sigevent;//信号结构,我们待会再说
int
aio_lio_opcode;//要执行的操作,只用与 lio_listio
}
aio_sigevent 用于指定当异步 io 操作完成时以什么方式通知进程,他另外一个结构:
struct sigevent
{
union sigval
sigev_value;//应用定义的值
int
sigev_signo;//要生成的信号
int
sigev_notify;//通知类型
void(*sigev_notify_function)(union sigval);//通知函数
pthread_attr_t*
sigev_notify_attributes;//通知属性
}
其中,通知类型如下:
SIGEV_NONE:事件发生时不产生信号
SIGEV_SIGNAL:信号发生时将生成信号
SIGEV_THREAD:信号发生时将执行指定的通知函数
异步 io 的一般流程:
22(1)、调用 open 函数来打开指定的文件获得文件描数字,然后使得文件位置指向文件开始并选择
合适的标志
(2)、创建并且填充异步 io 控制块 aiocb
(3)、如果使用信号,建立信号句柄来捕获异步 io 操作完成信号
(4)、调用 aio_read,aio_write,lio_listio 请求异步 io 操作,或者调用 aio_sync 使得
异步 io 与磁盘同步
(5)、如果应用需要等待 io 操作完成,调用 aio_suspend,或者继续执行调用 aio_error 来查
询操作是否完成
(6)、io 操作完成后,调用 aio_return 抽取返回值
(7)、调用 close 关闭文件。close 函数在关闭文件之前将等待所有异步 io 完成
81、存储映射 i/o
read 和 write 函数效率是很低的,可以想象更高层次的抽象的效率.....
因为对于 read 和 write,每一个 i/o 活动都需要调用一次系统调用来完成,而且,这种方式的
i/o 活动,对于有
多个进程访问的文件,每个进程的地址空间中都含义这个文件的副本,增加了内存开销。
所谓存储映射 i/o,就是为了解决这个问题的。存储映射 i/o 在内核缓冲区中最多只有一个文件的
副本。
当多个进程需要访问该文件时,这个文件的地址都将被映射到进程的地址空间。
进程可以对一个文件建立两种类型的的映射---共享的和私有的。
对于共享映射,内核使得所有对文件共享页的写操作都直接作用于该页的共享副本,并且当这一页
刷新时,将它
存回硬盘。对于私有映射,写文件的存储映射页导致复制该页的一个副本,并且针对此副本进行更
新,但是这种更新
不影响底层文件,也不会被写回到硬盘。
函数接口:
void*
mmap(void* addr,size_t len,int prot,int flags,int
filedes,off_t off):
调用这个函数的效果是:映射文件描数字 filedes 指定的文件的[off,off+len]区域到调用
进程的内存
[paddr,paddr+len]区域。
(1)、paddr:这是函数的返回值,也就是映射区在内存的开始地址
(2)、addr:应用指定的内存映射区的开始地址,如果为 0,则内核会选择一个合理的地址,
但是不会覆盖原来的映射
(3)、len:映射的字节数,如果 len 不是页大小的整数倍,那么将被填充至页的倍数,多余
的部分将被 0 填充,但是不能修改
(4)、prot:指明映射区域的保护权限,可以取值如下:
a:PORT_READ:可读区域
b:PORT_WRITE:可写区域
c:PORT_EXEC:可执行区域
d:PORT_NONE:不能被访问的区域
值得注意的是,为映射区域指定的保护权限必须和 open 指定的权限一致。
(5)、flags:指明映射区域的属性
a:MAP_FIXED:要求返回值必须等于 addr,为了兼容性的考虑,最好不要使用
b:MAP_SHARDE:共享
c:MAP_PRIVATE:私有
需要注意的是,三个必须要指定一个。
(6)、filedes:已经打开的文件描数字
(7)、off:给出文件中要映射的开始字节位置,off 需要是字对齐的
解除映射:
函数接口:
int
len 指明长度
23
munmap(void* paddr,size_t len):paddr 应该是 mmap 返回的值,想要存回磁盘,需要下面这个函数:
int
msync(void* addr,size_t len,int flags):
addr 是存储映射区的某个地址,len 确定从该地址开始的连续字节数,flags 指明控
制这块区域的方式:
a:MS_ASYNC:执行异步写,写操作一旦开始函数将返回
b:MS_SYNC:执行同步写,等待操作完成函数返回
c:MS_INVALIDATE:作废与指定映射区不一致的高速缓冲副本
a 和 b 必须而且只能选一个,c 用于作废与指定映射区域数据不一致的其他映射,也就
是说,如果一个程序有多个
进程都将同一个文件映射到存储器,其中一个调用了 sync(修改内容写回磁盘),则
其他所有进程的映射区都
将更新为与此进程一样的。只对 MAP_SHARED 有效。
82、unix 中的几种 IPC 机制
(1)、匿名管道和 FIFO 有名管道
(2)、消息队列,信号量和共享存储
(3)、套接字
83、管道:管道是连接一个进程的输出到另一个进程的输入的一种方法
--------------
---->in|
pipe
|--->out
--------------
创建管道:
函数接口:int pipe(int fdes[2]):fdes 将含有用于管道的两个文件描数字,一个
作为输入,一个作为输出
调用函数成功后,内核在系统内部创建一条管道,fdes[0]设置为读而打开,与输入端相
连,fdes[1]设置为写而打开,
与输出端相连。调用成功返回 0,否则返回-1 ,错误在 errno 里面。
管道的特点:
(1)、没有名字
(2)、两个描数字同时打开
(3)、不允许文件定位,读和写都是顺序的
在管道通信中,任何写到 fdes[1]的数据都可以从 fdes[0]读出来,数据在管道中是连续
的字节流,按先入先出处理,比如 fdes[1]
写入字符 abc,fdes[0]读出的将是 abc。
父子进程之间的管道通信:
当进程调用 fork 派生子进程时,子进程将继承父进程的所有打开的文件描数字。所以,
如果进程在 fork 之前创建 pipe 的话,
那么 fork 之后,父子进程将都能够访问构成管道的这两个文件描数字,于是就可以利
用管道在父子进程之间通信了。
父子进程之间的管道有两种通信方式,一种是从父进程往子进程发送数据,另外一种
则相反。但是不能同时存在。
为了避免通信混乱,我们需要选择一个通信方向,如果选择从父进程往子进程发送数
据,那么那么父进程要关闭他的
fdes[0](read),子进程要关闭他的 fdes[1](write).否则相反。
需要注意的一点是:如果管道不需要的一端不关闭的话,它绝对不会返回 EOF!
84、连接标准输入和标准输出的管道
父子进程通过管道通信的典型做法是:由子进程重复管道的文件描数字到标准输入或者标准输出,
然后调用 exec 执行新程序,新
程序继承标准输入输出,由此就实现了父子进程之间的通信。
就像下面这些代码一样:
24childpid=fork();
if(childpid==0){
close(0);/*the filedes 0 is the stdin,close the stdin*/
dup(fd[0]);/*let the fd[0](read port) pointer to stdin*/
exec(...);/*exec new program*/
}
我们应该知道的事情是:dup 将复制给定的文件描数字到一个最小的可用的文件描数字上,因为我
们关闭了 0(stdin),所以
管道的输入端将被定向到 stdin,子进程继承了标准输入输出,所以就可以和父进程通信了。
我们还可以用 dup2 来一次搞定:
childpid=fork();
if(childpid==0){
dup2(fd[0],0);/*close stdin,and copy fd[0] to stdin*/
exec(..);/*exec new program*/
}
85、popen 和 pclose
只是对管道的高度封装,降低了灵活性。
函数接口:
FILE*
popen(const char* command,const char* mode):该函数在调用进程与要
执行的命令之间建立一个内部
的半双工管道,然后派生一个子进程,调用 shell 执行 command 给出的命令。mode 指明管道
的读写方式, “ r” 或者 “ w”。
''r->对此流的读操作导致从 shell 命令的标准输出中读取数据
'w'->对此流的写操作导致写至 shell 命令的标准输入中
int
pclose(FILE* stream):该函数将等待由 popen 执行的进程终止,并且返回该进
程的出口状态。
86、管道 io 的原子性
管道在内核中其实是一块缓冲区,当写入到管道的数据块大小超过缓冲区的大小时,内核就需要分
批写入。
这中间就会出现原子性被中断。
87、FIFO 特别文件
管道只用与父子进程之间的通信,因为管道建立在内核中,只有那些有遗传关系的进程才能用到他。
FIFO 可以在没有父子关系的
进程之间通信。他是半双工的,FIFO 与管道的几点不同:
(1)、FIFO 作为特别文件存在于文件系统中
(2)、不同祖先的进程可以通过 FIFO 来共享数据
(3)、当共享完成后,如果不用 unlink 来解除他们,FIFO 文件将一直存在文件系统中。
FIFO 文件一旦打开,就可以像操作管道一样操作它。与管道一样,我们需要有一个进程为了写而打
开他,另一个则为了读而打开他。
可以用 shell 来新建 FIFO 文件:
mknod filename -p
mkfifo a=rw filename
函数接口:
int
mkfifo(const char* path,mode_t mode):新建一个新的特别文件,名字为
path,权限为 mode
int
mknod(const char* path,mode_t mode,dev_t dev):创建一个给定文件类型
的文件,名字为 path,mode 为文件类型,可以如下:
-----------------------------------
S_IFIFO:fifo 文件
25S_IFCHR:字符特别文件
S_IFDR:目录
S_IFBLK:块特别文件
S_IFREG:普通文件
-----------------------------------
dev 参数仅当指明文件类型为 S_IFCHR or S_IFBLK 时才用来指明这个新创建的特别文件的
主设备和副设备号。其他情况将被忽略。
88、FIFO 操作
(1)、打开 FIFO 文件
需要注意的是,不能以 O_RDWR 方式打开 FIFO 特别文件,不能又读又写。如果指定了
O_NONBLOCK 标志,他不仅仅指明对该文件的
读写是非阻塞的,同时也指明打开动作本身是非阻塞的,如果没有设置该标志,open 将阻塞直
到另一个进程以相反的读写方式
打开同一个 FIFO 文件。而对于普通文件而言,不论是否设置该标志,open 都是不阻塞的。
(2)、读写 FIFO 文件
可以用 read 和 write 来读写 FIFO 文件。
读写 FIFO 受打开标志 O_NONBLOCK 的影响。
调用 read 读一个阻塞的空 FIFO 文件,进程将阻塞直到有数据可读,操作完成后返回读取的数
据量。
调用 read 读一个非阻塞的空 FIFO 文件,进程将立即返回-1 并且设置 errno。
同管道读写一样,PIPE_BUF 控制着 FIFO 文件操作的原子性,所以一般数据大小不要超过这个值,
否则就不是原子性的。
89、关于 IPC 的概述(系统 V 的 IPC)
该 ipc 由三种进程间通信组成:消息队列,共享存储,信号量
这三种机制各不相同,具体如下:
(1)、消息队列:提供一种通用的消息传递方法,允许进程发送和接收不同大小的缓冲区数据,消
息的格式
可以由操作系统来设定,页可以由用户进程自己设定,只要通信双方达成协议即可。
(2)、共享存储:允许多个进程访问存储区中同一块数据区,希望通信的进程之间通过一块具有唯
一标志的
共享存储联系在一起,然后就可以进行通信了。
(3)、信号量:允许进程打开和关闭一组信号量集合的标志,这些标志告诉进程发生了什么事情,
那些与同
一个信号量集合标志相连的进程可以读取和修改该信号量集合,信号量本身不传递数据,只是用来
控制对某种资源的访问,
以实现进程之间的同步。
当然,这三种机制都有相似的特点,比如说他们都有唯一的整数标志,都有一个关键字,都使用某
种相同的数据结构,并且都有
类似的获取和控制 ipc 资源的函数接口。
------------------------------------------
关键字和标识:ipc 也是驻存在磁盘上的一种资源,但是和文件不同,每一个 ipc 资源有两个唯一
的标识与之相连,也就是
关键字和标识。他们的作用与文件的文件名和文件描数字类似,关键字就是创建 ipc 资源的名字,
标识就是用来 invoke 该
ipc 资源的句柄,和文件描数字有着相同的作用。
(1)、关键字(KEY)
关键字的类型为 key_t,这是一个长整形。他命名要使用的 ipc 资源,进程通过指定一个关键
字,然后调用相关的函数来
创建一个 ipc 资源。
关键字的值可以取值如下:
26IPC_PRIVATE:这个值通常为 0,他标示总是创建一个新的 ipc 资源。如果用这个关键字调用
创建 ipc 的函数,那么内核总是会
创建一个新的 ipc 资源,所以,这标示进程的私有 ipc 资源,其他进程一般得不到它,创建这
个 ipc 的子进程可以共享该 ipc 资源
非 0 整数:如果关键字不是 0,可以直接指定整数,这标示这个 ipc 可以被多个 使用
我们需要知道的是,希望访问同一个 ipc 资源的进程必须使用相同的关键字,下面需要介绍一个函

key_t ftok(const char* path,int id):这个函数将根据两个参数返回一个关键字,
该关键字可作为其他函数的参数。
参数 path 必须是一个已经存在的文件,id 只有低 8 位有效,对于命名同一个文件的 path,相
同的 id 返回相同的关键字,当用
不同的 id 时,返回不同的关键字。
还有,该函数返回的关键字是根据 inode 来确定的,所以如果将 path 删除后再创建,调用相
同的 id 返回的关键字可能不一样。
(2)、标识(ID)
每一个独立的消息队列,信号量集合、共享存储段都由一个唯一的正整数所标识,ipc 关
键字类似于文件名,标识相当于文件
描数字,用关键字获得该资源的标识之后,该关键字不再有用,后继对 ipc 资源的访问
将靠这个 id。
需要知道的一点时:关键字的唯一性是说所有资源中的唯一性,而标识的唯一性是对与同
一种 ipc 资源的唯一性,不同资源
的标识可以相同,但是所有资源的关键字必须不同。
-----------------------------------
ipc 资源描述结构与成员 ipc_perm:
每一个 ipc 都是一个资源,所以需要有权限,来规定哪些进程可以访问资源。创建 ipc 资
源的称为该 ipc 资源的创建者,
该进程拥有对这个资源的绝对控制权,这也是该 ipc 资源的属主,当然这个资源是可以被
转让的,转让后的资源变为
转入进程的资源,获得资源的进程成为该 ipc 资源的当前属主。但是 ipc 资源的原始创建
者是不会变的。
每一个 ipc 资源被创建时,系统都会在内核中创建他的标识和一个数据结构,并且设置他
的用户,用户组及其他的权限
----------------------------------------
关于 ipc_perm:
struct ipc_perm
{
uid_t uid;/*current*/
gid_t gid;
cuid_t cuid;/*creator*/
cgid_t cgid;
mode_t mode;
}
其中,mode 是一个位串,他的每一位确定 ipc 资源的一种访问权限,访问权限与文件的访
问权限是相同的。但是执行权限总是
0,因为执行 ipc 资源是没有任何价值的。
系统会根据该数据结构的值来判断一个进程是否与权限来 invoke 这个 ipc 资源:
(1)、进程具有特权,比如超级用户,那么可以访问
(2)、进程的有效用户 id 和 ipc 资源中的 cuid 或者 uid 相匹配,那么可以访问
(3)、有效用户 id 不匹配,但是进程的有效组 id 与 cgid 或者 gid 匹配,则可以访问
(4)、都不匹配,但是设置了其他用户访问位,那么可以访问
(5)、其他情况,一律无法访问该 ipc 资源
-----------------------
27ipcs 和 ipcrm:
ipc 资源会驻村在磁盘上,直到被删除,或者系统重启。所以党我们不再需要某个 ipc 资
源时,最好删除
这个 ipc 资源。ipc 资源在系统中是没有名字的,所以不能像删除文件一样删除 ipc,但
是我们可以使用
ipcs 命令来查看系统里面的 ipc 资源。用 ipcrm 来删除不再需要的 ipc 资源。
ipcs 可以指定想要查看的 ipc 资源类型,-q,-m,-s 分别表示消息队列,共享存储和信
号量 ipc 资源
ipcrm 需要指定想要删除的类型和 id。
90、消息队列
消息队列和管道类似,都是半双工的,有读方和写方,但是与管道不同,管道是连续的字节流,但
是消息队列则可以是自己定义的结构。
消息还可以有优先级。
每一个使用消息队列的进程都可以进行两种操作:发送消息和接收消息。
同一个消息队列可以有多个进程发送消息,也可以有多个进程接收消息。消息按照他们到达的顺利
连接在队列尾部,
新发送的消息放在队列的尾部,接收消息则从消息头部拿出消息,只要消息被读,则从队列删除。
当然,进程在发送和接收消息之前,需要先从操作系统得到一个消息队列,我们可以通过 msgget
来向操作系统申请
一个消息队列,每一个消息队列相连一个内核数据结构 msgqid_ds。他记录消息队列的有关信息。
我们可以通过 msgctl 函数来得到这个结构的信息,当然也可以用该函数来删除我们不再需要的消
息队列。
下面就是消息正文,消息正文被存放在 分配的消息缓冲区中,消息缓冲区是一个结构,我们只需要
让我们自己的消息
结构的第一个是 long int 类型并且大于 0 就可以,其他字段自有定义,我们都可以发送。
那为什么要求一定要有第一个字段呢?
(1)、给应用提供多路转接来自多个进程的消息的能力,我们可以根据这个字段来区别消息的发送
者,然后
我们可以返回正确的消息给发送者
(2)、可以给队列中的消息优先级,然后有选择的读取。
-----------一大波函数接口即将来临--------------
int
msgget(key_t key,int flag):key 为消息队列的关键字,flag 需要给出创建标
志和访问标志,创建标志
可以选择如下两个,访问标志和文件一模一样:
a:IPC_CREAT:如果不存在与 key 相连的 ipc 资源,则创建他
b:IPC_EXCL:若已经存在与 key 相连的 ipc 资源,则失败
该函数调用成功将返回与 key 相连的消息队列的 ID,如果这是一个新创建的消息队列,该函数将创
建与该 ID 相连的
数据结构 msqid_ds
为了创建一个消息队列,要么指定 key 为 IPC_PRIVATE,那么总是创建一个新的消息队列,当然,
除了可以创建新的
消息队列,我们还可以用该函数来获取某个已经存在的消息队列的 id。比如:
mqid=msgget(1234,0):这个函数将返回已经与关键字 1234 关联的消息队列的 id,因为我们不
需要上面限制,所以 flag=0。
-----------------------------------------------
消息队列的查询,设置与删除:
函数接口:
int msgctl(int msqid,int cmd,struct msqid_ds* buf):
该函数将对消息队列 msqid 执行 cmd 指定的操作,cmd 的取值和意义如下:
28(1)、IPC_STAT:复制消息队列的内核数据结构 msqid_ds 到 buf
(2)、IPC_SET:设置内核数据结构 msqid_ds 的值为 buf
(3)、IPC_RMID:删除 msqid 指定的消息队列
注意:执行第一个 cmd 的进程需要有对该消息队列的读权限,执行后两个 cmd 的进程只能
是消息队列的
创建者,拥有者和特权用户,此外,只有特权用户才能改变消息队列的字节数。
---------------------------------------------------
发送和接收消息:
函数接口:
int msgsnd(int msqid,constvoid* msgp,size_t msgsz,int msgfg):
int msgrcv(int msqid,void* msgp,size_t msgsz,long int
msgtyp,int msgflg):
关于发送消息:
函数将向指定的消息队列发送一条消息,消息的内容为 msgp,msgsz 为消息的
正文大小,不包括消息类型。
msfg 用来处理当进程阻塞时的动作,在下面的情况中,进程将阻塞:
(1)、消息队列中的总字节数已经等于 msg_qbytes(已经满了)
(2)、系统中的所有消息队列中的消息数量达到上限
当发生上面的情况时,如果 msgfg 设置标志:IPC_NOWAIT,则发生阻塞时消息
不被发送。调用进程将设置 errno 并且返回失败
当没有设置 IPC_NOWAIT 标志时,发生上面情况时,调用进程将阻塞直到发生下
面的事情之一:
(1)、导致阻塞的调价不再存在
(2)、消息队列标志已经被移除
(3)、调用进程收到信号
函数调用成功将会更新与消息队列相连的数据结构的相关信息。所发送的消息将
在消息队列中排队以等待
其他进程来读取该消息。
关于接收消息:
函数将从消息队列中读消息到指定的 msgp 中,调用成功则返回读到的消息大小,读完
消息后将消息删除。
并且将会更新与消息队列连接的数据结构的相关信息。
msgtyp 指明要接收的消息类型:
(1)、msgtyp==0,接收队列中的第一个消息
(2)、msgtyp>0 接收类型等于 msgtyp 的第一个消息,这种方式只接收特定类型的
消息
(3)、msgtyp<0 接收类型小于或者等于 msgtyp 绝对值的第一个最低类型的消息,
实现优先级
第五个参数指明当所希望的消息不在消息队列中时的动作,以及接收的消息大小大于
msgsz 时的动作:
(1)、如果接收的消息大于 msgsz,如果设置了 MSG_NOERROR 时,所接收的消息将
只取 msgsz 大小。如果
没有设置该标志,那么函数返回错误
(2)、IPC_NOWAIT 标志的意义和用法和发送消息时一样
91、共享存储段
共享存储段有大小和存储物理地址,想要访问共享存储段的进程可以连接这段存储区域到自己的地
址空间。共享存储将一直驻存在存储器中
直到共享存储标志被删除,或者没有任何连接连接他们,或者系统被重启。
29-----------------------------------
创建和获得共享存储区:int shmget(key_t key,size_t size,int shmflg):
参数 key 是共享存储段的关键字,为 IPC_PRIVATE 表示总是创建一个新的共享存储段,当不等于
这个值时,shmget 的动作
将由参数 shmflg 决定。(IPC_CREAT and IPC_EXCL)
参数 size 指明要求的共享存储段的大小,当 key 指定的共享存储已经存在时,size 必须要么为
0,要么不得大于该已经存在
的共享存储段的大小。
函数调用成功则返回与 key 相连的共享存储段的 id,然后将更新或者创建共享存储段(全为 0)和
与参数 key 相连的数据结构
shmid_ds 的值。
该函数调用失败将返回-1,并且设置 errno 。
-----------------------------------
查看,设置,删除共享存储段:
int chmctl(int shmid,int cmd,struct shmid_ds* buf):
用法和意义和消息队列一样,只是需要注意几点:
当用 IPC_SET 命名改变了 shmid 指定的共享存储段的读写权限时,他并不影响那些已经建立
连接的进程,但
对那些想要来建立连接的进程将开始起作用。
函数调用成功返回 0,失败返回-1,并且设置 errno
------------------------------------
共享存储段的连接与分离:
我们想要访问和操作共享存储段,就要知道他在哪里,也还要有适当的权限来做这样的事情。
我们需要将共享存储段映射到我们的进程的地址空间里面,然后才能读写他。当然如果我们不

再用这个共享存储段的时候,可以将这个映射解除。
函数接口:
void*
shmat(int shmid,const void*shmaddr,int shmflg):
int
shmdt(const void* shmaddr):
函数 shmat 指明要连接到的进程地址,如果 shamaddr 不为 0,那么就按设定的来,如果为
0,那么 os 会
为你自动的找到合适的空间然后完成连接。一般就为 0 吧。这个地址将会被返回。shmflg 的作
用是设定
一些权限,我们只需要知道 SHM_RDONLY 的用于,他指明连接的共享存储段是只读的,如果进
程有读权限
并且指明了该标志,共享存储段将以只读的方式连接。如果没有指明,则以读写方式连接。
调用成功则返回连接的实际地址,出错则返回 0.
第二个函数将完成解连接的功能。他的参数应该是我们第一个函数的返回值。注意该函数将不
会删除共享
存储段,想要删除共享存储段,就需要用 shmctl。我们还需要更新共享存储段的内核数据结构
的某些字段。
操作系统将根据某些字段的值来做出决策。
92、信号量
信号量是为了实现避免出现竞争而存在的,主要用于两种情况:
(1)、互斥:防止多个进程在同一时间访问一个共享资源。
当多个资源都需要访问共享资源时,可以用一个二值信号量来控制资源的访问。
当有一个进程需要访问资源时,可以执行一个 p 操作,当有进程正在使用资源时,这个 p 操作

被阻塞直到正在只用资源的进程执行了 v 操作。这样,任意时刻都将只有一个进程在使用共享
资源。
我们将访问共享资源的程序代码成为临界区。在进入和出去临界区时需要改变信号量以得到某
30种许可。
(2)、生产/消费模型
由协同工作的进程来使用,一个进程生产数据,另一个进程使用数据。他们之间通过信号量来
达到同步
目的。信号量应该初始化为 0,表明当前没有数据可消费,任何想要消费数据的进程的 p 操作都
将被阻塞。
直到生产着生产出了数据并且执行了 v 操作。
在 unix 中,为了防止死锁,信号量采用 “ 要么全有,要么全无 ” 的实现方式。
即:同时请求所需要的所有信号量,如果有一个被阻塞,那么其他均不能获得。这样就避免了死
锁的发生。
-------------------------------------------------
创建和获取信号量标志
信号量标志引用的是一组信号量而不是单个信号量。如果我们只是想操作单个信号量,
那么我们可以指定一个由单个信号量组成的信号量集合。每个信号量是一个 sem 结构体。
每一个信号量集合和一个内核数据结构 semid_ds 相连,我们可以在开发者文档里面找到他们
int
semget(key_t key,int nsems,int semflg):
这个函数将创建或者获取和 key 关键字相连的信号量集合的标识。参数 key 是信号量的关键字,
IPC_PRIVATE 表示
总是创建一个新的信号量集合,如果不是这个值的话,那么就取决于 semflg 里面的指定了,
这和消息队列或者
共享存储段是一样的。
参数 nsems 指定信号量的数量,需要大于 0,如果是一个已经存在的信号量集合,那么这个信
号量的个数需要在 0 到
已经存在的信号量数量之间,否则函数将返回错误。
需要知道的一点是,信号量集合中的第一个信号量的编号为 0,最后一个信号量的编号为
nsems-1
-----------------------------------------------------
信号量的查询、设置与删除
int semctl(int semid,int semnum,int cmd,[union semun arg]):
semnum 将指定我们需要选择的信号量的编号,参数 cmd 是操作命令,最后一个参数将配合
cmd 完成工作。
其中,cmd 参数的可选范围如下:
-----------------------------------------
SETVAL:设置单个信号量的值
GETALL:返回信号量集合中所有信号量的值
SETALL:设置信号量集合中所有信号量的值
IPC_STAT:得到 semid_ds 的结构,将放在适当的位置返回给调用者
IPC_SET:设置 semid_ds 结构,需要指定一个 buf
GETVAL:返回单个信号量的值
GETPID:返回最后一个操作该信号量集合的进程 id
GETNCNT:返回 semncnt 的值(等待对该信号量执行 p 操作的进程的数量,需要注意的是,这
个变量里面的是只需要等待
一个资源可用就可以的进程等待数量,而还有另外一个变量里面的是需要所有等待的资源都可
用才行的进程数量)
GETZCNT:返回 semzcnt 的值,他的含义见上文
IPC_RMID:删除指定的信号量集合
我们需要知道的一个联合结构体:
也就是参数中可选的最后一个参数:
union semun
{
31};
int
val;//同于 SETVAL
struct semid_ds*
buf;//用于 IPC_STAT/IPC_SET
unsigned short*
array;//用于 GETALL/SETALL
------------------------------------------------------------------------
信号量的操作:
int
semop(int semid,struct sembuf* sops,size_t nsops):
该函数对指定的信号量集合 semid 执行 sops 指定的操作,nsops 是操作的数量,也就是 sops 的
大小。
现在我们来看一下 sembuf 的结构信息:
struct sembuf
{
unsigned short int sem_num;//信号量的编号
short int sem_op;//信号量操作
short int sem_flg;//操作在状态
}
第一个成员指明操作的信号量编号。对于第二个成员,可以有三种情况:
sem_op=>
(1)、小于 0:减少一个信号量的值,减少的值为 abs(sem_op),相当于请求所控制的资源,当
为-1 时,相当于 p 操作
如果信号量的值大于 abs(sem_op),则操作成功,否则如果没有设置 IPC_NOWAIT 则一直等待到满
足条件
(2)、大于 0:增加一个信号量的值,为 1 时相当于 v 操作
(3)、等于 0:等待信号量变为 0 这对应于等待信号量控制的所有资源可用,如果信号量已经为
0,返回。否则如果为设置 IPC_NOWAT 则阻塞
对于成员 sem_flg:
(1)、IPC_NOWAIT:...
(2)、SEM_UNDO:如果设置,当进程退出时,执行信号量解除动作,对于指定了该标志的每一个
信号量,有一个内部变量
semadj 与之相连,他的作用是在进程死亡时调整信号量的值。
具体而言:如果某个信号量的值减少了,那么 semadj 的值就增加了,如果信号量的值增加了,那
么 semadj 的值将减少。这样
在进程死亡时,semadj 将会加到信号量中,也就是解除了信号量。
需要注意的是:当对信号量执行 SEM_UNDO 时,对同一个信号量的相反操作也需要指定该标志。
该函数调用只有当集合中的所有操作都完成时才会返回,如果集合中的某个操作无法完成的话,则
所有信号量的操作都不会被
执行,此时,如果这个信号量操作设置了 IPC_NOWAIT,则立即返回,否则进程被阻塞直到出现下
述情况之一:
(1)、所有信号量的操作都完成
(2)、进程收到一个信号
(3)、信号量集合被删除
93、套接字与网络通信
套接字不仅支持本地无关联两个网络之间的通信,还支持网络通信。他是双向的。
--------------------------------------------------------------------
关于套接字 socket:
从编程的角度来看,套接字就是相当于网络,就比如文件描数字就是磁盘一样。
int socket(int domain,int type,int protocol):
该函数在通信域 domain 里面创建一个类型为 type,使用协议 peotocol 的套接字,并返回
这个套接字描数字,socket 和 open 一样返回最小的未使用的描数字。
套接字和文件描数字可以使用相同的接口来操作,比如可以用 read 读数据,write 来写数据,
close 来
关闭描数字等。但是和文件描数字一样,套接字描数字不支持文件定位。
32参数 domain 指明通信域,该参数指明了通信使用的网络协议族,协议族不同,地址结构也不同
最常用的几个域是:
(1)、AF_UNIX:unix 通信域,也就是同一台计算机上两个进程之间的通信,要求将文件系统路
径名
作为套接字的地址。
(2)、AF_INET:网络通信,使用 ipv4
(3)、AF_INET6:网络协议,使用 ipv6
参数 type 指明套接字的类型,常用的如下:
(1)、SOCK_STREAM:字节流套接字,简称流套接字,提供面向连接的,双向的,可靠的数据流,
数据
没有记录边界,可支持带外数据,一对相连的流套接字类似于管道,只是数据流是双向的。
(2)、SOCK_DGRAM:数据包套接字,他支持双向通信,但是不可靠。没有边界,每次往返写数据
时,数据
便成为一个包,数据包套接字没有连接,应用必须为每一个包指定接收者的地址
参数 peotocol 是指定使用的协议,一般为 0 就可以,因为只要我们设定了 domain 和 type,那么
协议就是唯一的。
两个互相通信的进程必须使用相同的协议,不然无法识别。
socket 如果调用失败则返回-1,可以在 errno 里面看到错误信息。
--------------------------------------------------------------------
socket 只是创建一个套接字描数字,socketpair 则可以创建一对套接字。
int
socketpair(int dimain,int type,int protocol,int filedes[2]):
该函数将创建一对没有命名的套接字,放在参数 filedes 里面,套接字偶对是一个全双工的
通信通道,在其两端都可以读写。
-------------------------------------------------------------
当我们不再需要一个套接字时,可以用 close 来关闭,就像关闭文件描数字一样。
但是,用 close 关闭套接字后,这个套接字将不再存在。
我们可以仅仅断开连接,用下面的函数:
int shutdown(int socket,int how):
how 指定怎么关闭:
SHUT_RD:停止接收数据
SHUT_WR:停止发送数据
SHUT_RDWR:停止接收和发送数据
94、地址
socket 函数只是在本地创建了一个资源,如果希望其他进程来访问该资源,那么还必须有一个名字,
也就是地址(页可以是文件
路径名(作为本地套接字通信)),而在网络上通信时,这个地址是 ip 地址加上 port 号。
ip 地址的结构:
in_addr:ipv4
in6_addr:ipv6
-------------------------
typedef uint32_t in_addr_t;
struct in_addr {in_addr_t s_addr;};/*32 bits ipv4 address*/
struct i6_addr {uint s6_addr[6];}/*128 bits ipv6 address*/
--------------------------------------------------
unix 提供了一组函数,可以实现 ip 地址的字符形式与二进制形式的转换:
int
inet_aton(const char* name,struct in_addr* addr):将 name 所表示的 ipv4
地址转换为二进制形式,并保存在 addr 中
char* inet_ntoa(struct in_addr inaddr):将二进制的转换为可读的字符形式的 ipv4 地
33址
int
inet_pton(int family,const char* nameptr,void* addrptr):既可以用于
ipv4,也可以用于 ipv6.作用和上面第一个函数
一样,下面还有一个函数,实现了和第二个函数一样的效果(其实主要是为 ipv6 设计的这两个函
数)
char*
inet_ntop(int family,void* addrptr,char* nameptr,size_t len):
参数 family 可以时 AF_INET AF_INET6,nameptr 指向地址,addrpte 可能是 in_addr 或者
in6_addr,len 这个参数是用来指明
addrptr 这个区域的大小的,可能为 16 或者 46.
如果不足以保存获得的地址的话,函数返回空。
以上的四个函数所返回的 ip 地址的数值都是标准的网络字节顺序,需要自己注意。
95、域名地址
网络中的一台机器除了可以用 ip 地址表示外,还可以用域名来表示,每一个域名对应一个 ip 地址。
处在网络中的计算机可以有多个域名,他们是同一台主机的别名。
unix 系统内部用一个主机网络地址数据库记住主机名与主机 ip 地址之间的映射,我们可以用函数

从该数据库中获得主机的详细信息:
struct hostent* getbyname(const char* name):返回主机 name 的地址信息,如果失
败,返回空指针,
如果成功,则保存这些信息在 hostent 结构中:
----------------------------------------------
struct hostent
{
char*
h_name;//主机的正式名字
char** h_aliases://主机的可选别名
int
h_addrtype;//主机地址类型
int
h_length;//地址长度
char** h_addr_list;//指向主机网络地址数组的指针,他们的类型其实不是字符,而是相
应的地址类型。
}
注意,hostent 结构中的地址总是按照网络字节顺序来的。
参数 name 是域名,也可以时 ip 地址,后一种情况函数不做查询,只是复制给结果然后返回。
struct hostent
gethostbyaddr(const void* addr,size_t length,int type):
该函数将返回地址为 addr 的主机的相关信息,length 参数是地址长度,type 为地址类型,如果
这两个
函数调用失败的话,可以通过查看变量 h_error 的值来确认。在我们使用之前,应该先声明:
extern int h_error;
-------------------------------------
HOST_NOT_FOUND:数据库中不知道的主机
TRY_AGAIN:发生在域名服务器不能连接时,如果再试一次的话,可能就成功了
NO_RECOVERY:遇到了不可恢复的错误
NO_ADDRESS:主机数据库中含义该名字的登记项,但是没有地址
-----------------------
gethostbyname 获得地址信息有两种途径:
(1)、通过网络中的域名系统
(2)、通过本地的主机地址映射文件
------------------------------
当我们需要读多个主机的地址信息时,可以用 sethostent 打开本地数据库,然后用 gethostent
来逐条读取,
最好用 endhostent 来关闭数据库(也是关闭套接字)。
void
sethostent(int stayopen):
struct hostent* gethostent();
34void
endhostent();
函数 sethostent 打开本地主机网络地址数据库,如果 stayopen 不为 0,后继的操作将不会关闭
数据库(TCP),否则
操作完成后将关闭(UDP)。
返回空指针说明已经没有可读地址了。
endhostent 实际上是关闭 TCP 套接字。
96、服务与端口号
我们需要知道的一点是,网络通信中套接字地址由 ip+port 的形式唯一标志。
但是在系统中,一些端口已经被占用,比如 FTP,HTTP 等已经占用了固定的端口号,我们可以用的
端口号
在 5000 之后(unix)
-----------------------------------------------------------------------
|
|
|
|
0
IPPORT_RESERVED
IPPORT_USERRESERVED
65535
------------------------------------------------------------------------
当使用的是没有指定地址的套接字,系统会为他生成一个位于第二个区间的一个值作为端口
号,这种端口号称为动态端口号,瞬间端口号等。
在 unix 系统里面,有一个记录标准服务的数据库,(/etc/services/),用结构 servent 来表
示每一个
服务项:
-------------------------------
struct servent
{
char*
s_name;//服务程序的正式名字
char*
s_aliases;//服务程序的别名
int
s_port;//服务程序对应的端口号,端口号按照网路字节顺序给出
char*
s_proto;//与该服务一起使用的协议名
}
对于标准服务,可以用端口直接指出,比如 21 就是 ftp,但是当标准服务所用的端口号
不确定时,可以用下面的函数来得到:
---------------------------------------------
struct servent*
getservbynama(const char* name,const char* proto):返
回使用协议 proto
并且名字为 name 的有关信息
struct servent* getservbyport(int port,const char* proto):返回使用协议
proto 并且使用端口
号 port 的服务
void
setservent(int stayopen):
struct servent* getservent();
void
endservent();
上面三个函数的用法和前面的提到的类似三个函数的用法是一样的。
97、套接字地址数据结构
(1)、unix 通信域套接字地址结构
struct sockaddr_un
{
sa_family_t sun_family;
char
sun_path[];
}
sun_family 指明地址族,对于 unix 通信域,应该为 AF_UNIX,sun_path 给出 unxi 文件的路径
35名。
他不能与文件系统的文件名重名。由该参数所命名的文件是在 bind()时创建的,此时内核在文件
系统中
为他分配一个 inode 节点,如果这个 inode 没有被解除,这个文件将一直存在于文件系统中,甚至

这个套接字被关闭之和也是如此,所以,当我们关闭一个文件名套接字时,应该用 unlink()或者
remove()从文件系统中删除其文件名。
(2)、ipv4 和 ipv6 通信域套接字地址结构
表示 ipv4 套接字地址结构的是:
struct sockaddr_in
{
sa_family_t
sin_family;//AF_INET
in_port_t
sin_port;//16 位端口号,网络字节顺序
struct in_addr sin_addr;//32 bits ipv4 address
unsigned char
sin_zero[8];//reserve
}
表示 ipv6 套接字的地址结构
struct sockaddr_in6
{
sa_family_t
sin6_family;//AF_INET6
in_port_t
sin6_port;//16 bits port,网络字节顺序
uint32_t
sin6_flowinfo;//ipv6 流标号和优先级信息,网络字节顺序(用于处理实
时服务)
struct in6_addr
sin6_addr;//128 bits ipv6 address
}
(3)、通用套接字地址结构
struct sockaddr
{
sa_family_t sa_family;//AF_UNIX,AF_INET,AF_INET6
char
sa_data[];//套接字的实际地址信息
}
这是通用地址结构,我们需要将专用的地址结构转换为该地址结构,可以这样操作:
--------------------------------------------------------------
struct sockaddr_un name;
name.sa_family=AF_UNIX;
strcpy(name.sun_path,filename);
bind(sockfd,(struct sockaddr*)&name,sizeof(name));
....
------------------------------------------------------------
98、字节顺序
数据是存放在存储器中的,存放数据的基本数据单元是字节。
有的计算机将一个数中最有意义的字节放在存储器地址编号较小的字节(little_endian)
有的计算机将一个数中最有意义的字节挡在存储器地址编号较大的字节(big_endian)
A B C D
---------->
我们用在上面的 ABCD 中分别存储 1,2,3,4,那么在 big_endian 里面表示 0x01020304
而在 litter_endian 里面表示 0x04030201
我们可以用代码来查看我们自己的系统是采用什么字节顺序:
---------------------------------------------------
36如果我们需要封装一个整型数据于一个消息中,并通过套接字传输,那么就需要将字节顺序变化为
网络中的标准网络字节顺序:
uint16_t htons(uint16_t hostshort)
uint32_t htonl(uint32_t hostlong)
uint16_t ntohs(uint16_t netshort)
uint32_t ntohl(uint32_t netlong)
h 代表主机,n 代表网络,l 代表 long,s 代表 short。
所以很好理解上面的函数的功能。
注意:一定要注意的是,当我们需要将一个数据通过套接字发送出去时,一定要保证已经转换为
标准网络字节顺序了,可以通过 htons 或者 htonl 来完成,当我们接收到了来自套接字的数据时,
也应当将接收到的网络字节顺序转化为主机顺序,不同的主机采用的字节顺序是不一样的,如果
不做转化,那么对方收到的数据做运算可能会出现难以预料的结果!!
99、命名套接字
我们用 socket 创建的套接字只是系统中一个没有名字的资源,其他进程没有办法访问他,当我们
调用函数
bind 为套接字指定一个名字时,其他进程才能访问他。
--------------------------------------------------------
int
bind(int socket,const struct sockaddr* address,socklen_t
address_len):
参数 socket 是套接字描数字,参数 address 指向结构类型为 sockaddr 对象的指针,参数
address_len
指明地址的长度。
函数返回 0 代表成功,-1 代表失败(一般是地址格式有误,或者套接字已经被命名)
我们需要注意,无论在那个通信域,都使用 bind 来设置套接字的地址,所以我们需要将我们专用的
地址类型
强制转化为通用地址类型。
100、套接字通信模式
(1)、有连接模式
对于服务程序,他首先创建一个 socket,然后给该 socket bind 一个名字,然后便在等待客户进
程与该套接字
建立连接。在多个客户与该套接字建立连接的情况下,服务进程通过调用 listen()为进入的连接
创建一个连接
队列,通过调用 accept()逐一的接收这些连接,每次调用 accept,都会创建一个新的套接字,
他不同于已经命名
的套接字,这个新的套接字完全只用于与特定客户的通信。
对于客户端,用 socket 创建一个流描数字并且 bind 名字之后,就可以用函数 connect 来与服务
创建连接了。
双方一旦建立了连接,那么就可以实现双向通信了
(2)、数据报模式
这是无连接的,所以较为简单。双方都是对等的,他们的行为一样,每一方是服务方也是客户方。
用 socket 创建一个套接字描述字后,bind,然后就可以用 sendto 或者 recvfrom 来进行通信了
101、流套接字的操作
(1)、请求连接
请求连接是客户端的动作,是主动套接字
int
connect(int socket,const struct sockaddr* address,socklen_t
address_len);
socket 是套接字描数字,address 是要连接的远程套接字地址,address_len 指明套接字
37的长度。
函数成功时,socket 将被连接到 address 给出的地址上,后者必须是已经存在的套接字地址。
成功返回 0,失败返回-1,errno 的情况如下:
---------------------------------
EBADD:socket 不是合法的套接字
EALREADY:已经有一个悬挂的连接正在被处理
ETIMEDOUT:连接超时
ECONNREFUSED:服务端拒绝此连接
EINTR:被信号终端
-----------------------------------
正常情况下,当连接不能立即建立时,函数将等待直到服务程序回答了连接请求。
或者等待了某个时间才返回,如果超时返回,connect 将失败并且流产该连接请求。
如果在连接请求期间收到信号而被终端,失败但是连接请求不会流产,而是异步的建立。
可以对套接字设置为非阻塞方式,从而使得函数不等待直接返回,可以用 fcntl 来达到这个目
的。
如果对套接字设置了非阻塞标志 O_NONBLOCK,当连接不能立即建立时,函数将失败但是会异步
建立,在
连接建立之前如果后继对同一套接字请求连接,将会发生 EALREADY 的错误。
(2)、接收连接
接收连接是服务端的动作,为了接收客户端的连接请求,服务段需要创建一个保存连接请求的
侦探队列,然后建立连接。
int
listen(int socket,int backlog):这个函数用来建立侦探队列
,参数 backlog 指定套接字侦探队列允许的悬挂请求数量。
函数成功返回 0,失败返回-1.
int
accept(int socket,struct sockaddr* address,socklen_t*
address_len);
该函数用于接收 socket 上面的连接请求,在有进程企图与 socket 建立连接时,它会创建一
个新的套接字
与这个客户连接,并且返回这个新的套接字。如果参数 address 不是空指针,那么将会把客户
的套接字地址
保存在这个域里面,第三个参数指明这个地址的长度。
如果没有悬挂的连接请求,除非套接字设置了非阻塞方式,否则 accept 函数将阻塞直到有客
户连接为止。
(3)、获取套接字的地址
int
getsockname(int socket,sockaddr* address,socklen_t address_len)
int
getpeername(int socket,sockaddr* address,socklen_t address_len)
第一个函数用于获取本地套接字的 socket 的地址,第二个函数用于获取与本地 socket 连接
的远程套接字的
地址,address 指出存放地址的对象,address_len 指出存放地址的长度。
成功返回 0,失败返回-1
(4)、多客户服务
服务程序一直循环等待连接,并且每次只为一个客户服务,这种服务成为迭代服务。
并发服务则是对每一个客户连接派生一个子进程来为其服务,自己则继续守护在侦探套接字上
等待连接。
一旦有客户连接请求,accept 立即返回,此时服务进程调用 fork 派生一个子进程来为客户服
务。
子进程首先关闭侦探套接字,然后用 accept 返回的套接字为客户服务,子进程关闭侦探套接

并不影响父进程的侦探套接字,因为引用计数不为 0,则文件不会被删除。
(5)、send 和 recv 函数
ssize_t
send(int socket,const void* buff,size_t length,int flag)
38ssize_t
recv(int socket,const void* buff,size_t length,int flag)
我们可以用 read 和 write 直接操作套接字,这没有问题,因为套接字也是文件,我们可以像
操作文件一样操作
套接字,我们也可以用上面这两个函数来完成读写套接字,只是这两个函数专门用于套接字读
写。
send 将发送一个数据到 socket,如果 flag 为 0,那么和 write 一模一样,当然,flag 可
以取其他值:
MSG_OOB:导致 send 发送的数据成为带外数据(紧急数据 —— 暂且这么理解吧)
MSG_DONTHOUTE:不在消息中包含路由信息
recv 函数与 read 一样,从 socket 中读出数据,当 flag 为 0 时,和 read 一模一样。
flag 的取值可以如下:
MSG_PEEK:窥视套接字上的数据而不实际读出他们
MSG_OOB:读取带外数据
MSG_WAITALL:函数阻塞直到接收到所请求的全部数据
102、套接字选项
int
getsockopt(int socket,int level,int optname,void* optval,socklen_t*
optlen)
int
setsockopt(int socket,int level,int optname,void* optval,socklen_t*
optlen)
-->socket 必须是已经打开的套接字,level 指定选项所属的层次。
-->getsockopt 将获得套接字 socket 在层次 level 上的选项 optname 的值,存放在 optval
里面,应该提供此
此缓冲区的大小 optlen,函数返回时 optlen 将保存实际的选项值大小。
-->setsockopt 将设置已经打开的套接字
103、带外数据
带外数据就是紧急的数据,在 tcp 的带外数据中,只允许一个字节作为带外数据,tcp 将紧急指针
指向这个带外数据。
104、数据包套接字
一种面向无连接的,不可靠的套接字服务。
发送和接收数据:
int recvfrom(int socket,void* buffer,size_t size,int flags,struct
sockaddr* from,size_t* addrlen);
int sendto(int socket,void* buffer,size_t size,int flag,struct
sockaddr* to,size_t* addrlen);
---------------------------
在数据报套接字中使用 connect 函数,他不像在流套接字那样产生连接,而只是为将来在
此套接字上的传输指明一个方向。
当我们用 connect 函数之后,就可以用 read、write 等函数来直接操作数据报套接字了,
而不需要每次都写一大堆的参数。
--------------------------
关于超时处理:因为数据报套接字是面向非连接的,所以我们需要自己处理数据丢失的情况。
我们可以设置超时来解决:
(1)、调用 alarm()使得系统在指定的时间片到期时生成 SIGALRM 信息
(2)、用 select()建立一个时间片来等待套接字就绪
(3)、使用 SO_RCVTIMEO,SO_SNDTIMEO 选项套接字,这两个选项自动对套接字的读写
设置超时处理。
105、什么叫线程
39简单来说,线程是运行在进程上下文中的一个执行流。所以进程内部总是有一个执行流,总有一个
线程。
每一个线程对应于一个执行流。只有一个执行流的进程称为单线程进程,否则称为多线程进程。

我们需要知道的几点:
进程上下文即进程运行所需要的资源和环境,包括各类存储空间和操作系统内核的管理资源。
进程的上下文包括 text、data、虚地址空间,堆空间,共享存储,用户栈等内容。
寄存器上下文保存着程序执行时的当前状态,内核栈用于系统调用时内核中的函数调用。
进程结构是由操作系统为每个进程指定的,内核将根据这个结构来管理进程。
当然,线程也有上下文,但是线程的上下文比进程的上下文小很多,因为线程在于进程之内,
除了作为执行流需要的上下文之外,其他资源都可以与进程共享,线程只复制了使得其作为
可执行流存在的基本资源。
-------------------------------------------------------------------
(1)、每一个线程有自己独立的执行流,一个进程内的所有线程地位平等,没有父子关系,只有兄
关系。
(2)、线程是隶属于进程的,一个进程创建的所有线程都共享该进程的若干资源,这些资源包括进
程的地址
空间,环境变量,文件描数字,信号动作等,如果进程死亡,该进程的所有线程都将死亡。
(3)、线程有自己专用的栈空间和线程专有全局数据。
并发和并行:
------------------------------------------------------------
并发是指多个线程同时处于运行状态并可按任意顺序执行,但是不要求同时都正在被调度执行。
并行是指多个线程被同时调度执行,这必须有多处理机或者多核的支持。
总的来说,并行是并发的子集。
-----------------------------------------------------------------------
线程与进程的比较:
进程的上下文比较大,内核管理进程需要占用较多的资源,而且每个进程的存储空间是独立的。这
导致新派生出来的进程具有和父进程一样大小的空间,增加了存储空间的开销,还有,子进程不能
直接读写父进程的存储单元。
106、Pthread 线程
----------------------------------------------------------------------------
----------------------
线程标志:线程标志由 pthread_t 类型来表示。他有可能不是一个整形,而是一个结构体。
所以,我们比较两个线程的时候需要用函数来获得结果:
pthread_t
pthread_self():该函数可以获得线程自己的 TID
int
pthread_equal(pthread_t t1,pthread_t t2):当两个线程相同时返回非
0,不同返回 0.
----------------------------------------------------------------------------
-----------------------
创建线程:
所有进程一开始就有一个线程,这个线程成为初始线程,初始线程是在进程创建开始就自动存
在的。
每一个线程都有一个开始函数,叫做线程入口函数,主线程的入口函数为 main。我们可以用下
面的
函数接口来创建一个新的线程:
int
pthread_create(pthread_t *restrict thread,const pthread_attr_t*
restrict attr,
void*(*start_routine)(void*),void* restrict arg):
第一个参数是函数返回的新创建线程的 TID,第二个参数是线程的属性,一般为 null,这时候
系统将
使用默认的属性来创建线程,第三个参数为入口函数,只接受一个 void*参数,返回类型也为
void*
40且与
而最后一个参数为入口函数的参数。
新创建的线程将继承创建他的线程的信号屏蔽、调度策略和优先级,但不继承悬挂信号集。并
创建他的线程是平等的,我们在编程的时候不应该对线程的执行顺序做假设,因为这是随机的。
----------------------------------------------------
退出线程:
int
pthread_exit(void* value_ptr):
参数 value_pte 指出线程终止时的出口状态,当另一个线程调用 pthread_join()等待该
线程时,那个
线程便可以得到 value_ptr 指定的值。
线程终止并不释放进程相关的任何资源,比如文件描数字,共享存储,文件锁等。也不会执行
进程级别
的清理动作。
-------------------------------------------------
等待进程终止:
与父进程等待子进程类似,一个线程在创建了若干线程之后,可能会需要等待这些线程完
成工作并且
与自己汇合,线程可以调用下面的函数来等待另一个线程:
int pthread_join(pthread_t thread,void** value_ptr):
这个函数将悬挂调用线程直到 thread 执行返回(线程终止),如果 value_ptr 是非空的,
他所指的对象
将存放线程 thread 线程终止时传递的值,当一个线程需要获得某个终止线程的出口状态
时,就需要
这个函数来完成。
如果一个线程不是分离的,他的资源要在另一个线程调用 pthread_join 与他汇合之后才
能被释放,这种
线程称为可汇合的线程,否则就是分离的线程,分离的线程一旦终止执行,系统将立即回
收其资源。
被另外一个
与进程只能是父进程等待子进程不同,任何线程都可以等待其他线程,但是一个线程只能
线程等待一次。
--------------------------------------------------------------
可汇合线程和分离线程:
由默认属性创建的线程都是可汇合的线程,因此线程结束后停留在线程池里面等待系统释放资
源,这样的话系统的资源
将不能利用最大化,所以,我们在退出某个线程的时候,如果我们不需要等待其他线程,那么
就告诉系统,可以回收该
线程占用的资源了。可以用下面的函数来做这件事情:
int
pthread_detach(pthread_t thread);
----------------------------------------------------------
创建特殊属性的线程:
(1)、分离状态,可以取两个值:
PTHREAD_CREATE_DETACHED:表示分离的线程
PTHREAD_CREAT_JOINABLE:表示汇合的状态,也是系统默认的状态
int
pthread_attr_init(pthread_attr_t *attr):用于初始化一个线程属性,
在使用这个线程属性之前,必须要
先初始化,因为 attr 现在是一个空指针,没有指向的实质性对象,所以需要告诉系统我们
需要一个指向线程属性的指针。
int
pthread_attr_destroy():用于销毁一个线程属性,当用这个线程属性创建了
41线程之后,这个线程属性
就没有什么用了,为了节约内存,我们需要将这个空间释放掉。
int pthread_attr_getdetachstate(pthread_attr_t* attr,int *
detachstate):查看线程属性的分离状态,
函数将返回该线程属性的分离状态或者汇合状态
int
pthread_attr_setdetachstate(const pthread_attr_t* attr,int
* detachstate);
该函数用来设置线程属性的分离状态
(2)、栈大小
(3)、栈地址
int
pthread_attr_getstacksize(const pthread_attr_t* restrict
attr,size_t *restrict stacksize):
用来查看当前线程栈的大小
int pthread_attr_setstacksize(pthread_attr_t* attr,size_t
stacksize):用来设置栈的大小
int
pthread_attr_getstackaddr(const pthread_attr_t* restrict
attr,void**restrict stackaddr):
int
pthread_attr_setstackaddr(pthread_attr_t* attr,void*
stackaddr):
上面两个函数用来获取和设置栈的大小
int
pthread_attr_getstack(const pthread_attr_t* restrict
attr,void** restrict stackaddr,size_t* restrict stacksize):
int
pthread_attr_setstack(pthread_attr_t* attr,void**
stackaddr,size_t stacksize);
上面两个函数将同时完成栈地址和大小的设置和查询。推荐使用。
(4)、栈溢出保护区
int pthread_attr_getguardsize(const pthread_attr_t* restrict
attr,size_t* restrict guardsize):
int
pthread_attr_setguardsize(pthread_attr_t* attr,size_t
guardsize):
上面两个函数用来查看或者设置保护区的大小
-------------------------
int mprotect(guard_addr,pagesize,PROT_NONE):该函数用来设置空间中某一区
间的读写许可,他将设置从
[guard_addr.......pagesize-1]的地址空间为不可读。
-------------------------------------------------------------------------
---------------------
互斥变量:线程之间有时需要同步,所以需要解决线程之间的通信问题。
一般有最基本的三种同步方法:
(1)、互斥执行:当两个线程必须依次访问某个共享对象时,就需要这种同步方法
(2)、条件同步:线程必须等待某个时间发生时,就需要这种同步
(3)、栅栏同步:用于控制线程执行过程中的汇合
-------------------------------------------
互斥变量主要用于多个线程竞争访问某个共享资源。
表示互斥变量的数据类型为:pthread_mutex_t
初始化:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER:静态互斥变量初始化(非 malloc
申请)
int
pthread_mutex_init(pthread_mutex_t* restrict mutex,const
42pthread_mutexattr_t* restrict attr)
int
pthread_mutex_destroy(pthread_mutex_t* mutex)
互斥变量初始化之后处于未锁状态,并且属性不能再改变
-----------------------------------------------------------
互斥变量属性:
unix 定义了两种属性:
(1)、进程共享属性
PTHREAD_PROCESS_PRIVATE:限制在一个进程之内
PTHREAD_PROCESS_SHARED:进程间可用
(2)、类型属性
PTHREAD_MUTEX_NORMAL:基本类型,没有错误检查
PTHREAD_MUTEX_RECURSIVE:递归类型,可以重复加锁
PTHREAD_MUTEX_ERRORCHECK:检查错误
PTHREAD_MUTEX_DEFAULT:默认类型
互斥变量属性类型为:pthread_mutexattr_t
int
pthread_mutexattr_init(pthread_mutexattr_t* attr):初始化,默认属性
int
pthread_mutexattr_destroy(pthread_mutexattr_t* attr):销毁
-------------------------------------------------------------------------
int
pthread_mutexattr_setpshared(pthread_mutexattr_t* attr,int
pshared);
int
pthread_mutexattr_getpshared(const pthread_mutexattr_t* restrict
attr,int *restrict pshared);
int
pthread_mutexattr_settype(thread_mutexattr_t* attr,int type);
int
pthread_mutexattr_gettype(const pthread_mutexattr_t* restrict
attr,int *restrict type);
对于上面的函数,不需要多说什么,看字面意思就好。
-----------------------------------------------------------------
互斥变量的加锁与解锁
int
pthread_mutex_lock(pthread_mutex_t* mutex)
int
pthread_mutex_trylock(pthread_mutex_t* mutex)
int
pthread_mutex_unlock(pthread_mutex_t* mutex)
第二个函数和第一个函数的区别就是,当一个互斥锁申请没有得到满足时,第二个函数将立即返回
并设置 EBUSY 的错误,而第一个函数将被阻塞直到获得申请的锁。
-------------------------------------------------------------------------
---
spin 锁:
当线程对互斥变量上锁 成功时,可以有以下解决方案:
(1)、立即阻塞,即释放线程占用的 cpu 资源,使其从运行状态进入等待状态
(2)、让线程在一个循环中轮询查看能否获得,如果一段时间后还是无法成功,那么进入阻塞
(3)、在一个循环中不断轮询,直到成功
spin 锁就是,当上锁受阻时,线程不必阻塞,而是可以轮询直到获得锁。
----------------------------------------------------------------------
int pthread_spin_init(pthread_spinlock_t* lock,int pshared)
int
pthread_spin_detroy(pthread_spinlock_t* lock);
int
pthread_spin_lock(pthread_spinlock_t* lock);
int
pthread_spin_trylock(pthread_spinlock_t* lock)
int
pthread_spin_unlock(pthread_spinlock_t* lock)
----------------------------------------------------------------------
43----------------------------------------------------------------------
读写锁:
读写锁是专门设计来改善程序并行性的锁机制,他支持对共享数据 “ 共享读,互斥写 ”
可以以读方式上锁,写方式上锁。
-----------------------------------------------------------------------
int
pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER
int
pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,const
pthread_rwlockattr_t* restrict attr);
int
pthread_rwlock_destroy(pthread_rwlock_t* lock);
读写锁和互斥变量一样,也可以设置为在进程内部还是进程之间;
int
pthread_rwlockattr_init(pthread_rwlockattr_t* attr);
int
pthread_rwlockattr_destroy(pthread_rwlockattr_t* attr);
int
pthread_rwlockattr_setpshared(pthread_rwlockattr_t* attr,int
pshared);
int
pthread_rwlockattr_getpshared(pthread_rwlockattr_t* restrict
attr,int *restrict pshared);
int
pthread_rwlock_rdlock(pthread_rwlock_t* rwlock): 以读方式上锁
int
pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock):读方式上锁
int
pthread_rwlock_wrlock(pthread_rwlock_t* rwlock):以写方式上锁
int
pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock):以写方式上锁
int
pthread_rwlock_unlock(pthread_rwlock_t* rwlock):解锁
----------------------------------------------------------------------------
------------
条件变量:线程实现等待唤醒有两种方式
(1)、轮询计数,当计数不为 0 时就自动脱落等待状态
(2)、将自己加入到一个等待队列,然后阻塞自己直到有人来唤醒自己
那什么叫条件变量呢?
条件变量在线程需要等待共享数据满足某个状态时使用,如果状态不满足,他将在该条件变量
上面等待,直到将来另一个线程导致状态满足。并通过相应的条件变量来唤醒为止。
条件变量有三个成分
(1)、条件变量本身
(2)、一个谓词,就是线程用来查看是否需要等待或者被设置的条件
(3)、一个与之相连的互斥变量,是用来保护谓词的
条件变量的结构时是:pthread_cond_t
创建和销毁条件变量:
------------------------------------
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
int
pthread_cond_init(pthread_cond_t* restrict cond,const
pthread_condattr_t* restrict attr)
int
pthread_cond_destroy(pthread_cond_t* cond)
-------------------------------------------------
条件变量的属性:
只能是 PTHREAD_PROCESS_PRIVATE OR PTHREAD_PROCESS_SHARED,表示用于进程之间还是
进程内部。
-------------------------------------------------------------------------
---------------------
int pthread_condattr_init(pthread_condattr_t* attr)
int
pthread_condattr_destroy(pthread_condattr_t* attr)
int
pthread_condattr_getpshared(pthread_condattr_t* attr,int*
pshared);
int
pthread_condattr_setpshared(pthread_condattr_t* attr,int*
pshared);
44需要注意的一点是,当我们设置为在进程之间时,需要将他分配在共享存储段里面,不然不同进程
无法访问。
-------------------------------------------------------------------------
------
等待条件变量:
一个执行正常的等待操作,一个执行定时等待(是终止时间而不是等待时间,设置到终点的时间)
----------------------------------------------------------------------------
------
int
pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t*
restrict mutex)
int
pthread_cond_timedwait(pthread_cond_t* restrict
cond,pthread_mutex_t* restrict mutex,const struct timespec* restrict
abstime)
注意,互斥变量在线程调用该函数之前必须已经上锁,这两个函数在阻塞线程之前自动释放
mutex。
----------------------------------------------------------------------------
---
唤醒条件变量:
(1 )、每次唤醒一个线程,称为 “ 发信号 ”
(2 )、一次就唤醒等待在同一个条件变量上面的所有线程,称为 “ 广播 ”
int
int
pthread_cond_signal(pthread_cond_t* cond);
pthread_cond_broadcast(pthread_cond_t* cond);
如果在某个条件变量上面没有等待的线程,调用该函数不起任何作用(除了浪费资源)
--------------------------------------------------------------------------
关于栅栏同步:
一个线程运行到某个点时,需要等待其他线程都到达这个点后才能继续,这种等齐所有线程
到达同步称为栅栏同步,程序中的这一点称为栅栏同步点,实现栅栏同步需要两个基本函数:
(1)、栅栏初始化函数,用于建立栅栏同步点需要等待的线程个数
(2)、栅栏等待函数,用于阻塞调用者直到指定个数的线程都到达此栅栏同步点为止(唤醒)
107、线程专有数据
共享数据:只有一个存储副本,并且所有线程都可以访问,因为只有一个副本,所以所有线程看到
的都是最后一个访问
共享数据的结果
线程私有数据:每一个线程有各自私有的存储副本的数据,线程可以随意的读取自己的私有数据而
不会影响到别的线程
线程专有数据:对线程私有的或者特定于线程的数据
-------------------------------------------------------------
pthread 实现的线程专有数据采用键/数据相结合的方法。
应用需要给每个线程专有数据连接一个键,这个键是所有线程共享的全局数据,当线程引用一个键
时,引用的是自己的部分。
线程专有数据键的类型为:pthread_key_t
------------------------------------------------------------------------
45创建键:
int
pthread_key_create(pthread_key_t *key,void(*destructor)(void*));
创建键的作用是为每一个线程相连一个指向专有存储空间的指针。这些指针初始化为 NULL,key
一旦创建,进程内的所有线程便可以通过函数 pthread_getspecific()使之指向各自分配的专有
数据空间。
第二个参数是我们指定的一个析构函数,这个函数负责在线程终止时释放与 key 相连的线程专有数
据空间
每一个线程专有数据键只能创建一次,第二次创建将会替代第一次创建的内容。
所以我们可以用下面的函数来只创建一次:
------------------------------------------------------------------------
pthread_once_t once_control=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t* once_control,void(*init_routine)(void));
参数 once_control 用来确定相连的初始化函数是否已经被调用。这个初始化函数就是第二个参数
-------------------------------------------------------------------------
当不再需要一个专有数据键时,可以调用函数 pthread_key_delete 来删除
int
pthread_key_delete(pthread_key_t* key);
--------------------------------------------------------------------
使用线程专有数据:
每一个线程的专有数据键创建时,初始值为 NULL,为了使用专有数据,每一个线程需要给专有数据
键指定值。
int
pthread_setspecific(pthread_key_t key,const void* value);
void*
pthread_getspecific(pthread_key_t key);
108、取消线程
(1)、可取消状态:
PTHREAD_CANCEL_ENALBE(允许取消)
PTHREAD_CANCEL_DISABLE(不允许取消)
(2)、可取消类型
PTHREAD_CANCEL_DEFERRED:延迟取消
PTHREAD_CANCEL_ASYNCHRONOUS:异步取消
--------------------------------------------------------
设置可取消状态和可取消类型:
int
pthread_setcancelstate(int state,int* oldstate);
int
pthread_setcanceltype(int type,int* oldtype);
-------------------------------------------------------------
取消线程和取消点:
我们可以用下面的函数来取消一个线程:
int
pthread_cancel(pthread_t target_thread);
该函数负责向目标线程发送取消线程的消息,但是不负责监督线程取消,而是通知完成即刻返回。
我们可以用下面的函数来建立自己的取消点:
void
pthread_testcancel(void):该函数检查调用线程是否有悬挂着的取消请求。
-------------------------------------------------------------------------
现场清理:
void
pthread_cleanup_push(void (*routine)(void*),void* argv);
void
pthread_cleanup_pop(int execute);
这两个函数需要成对出现,而且不应该被嵌套,应该像括号一样出现,不然无法达到预期的效果。
对于第二个函数,参数非 0 则表示将栈顶的清理函数移出来,然后执行。
109、线程调度
46线程需要操作系统调度才能获得处理机资源,所以需要调度策略和算法。
线程调度模式:他描述一个线程与谁竞争
(1)、进程调度模式:PTHREAD_SCOPE_PROCESS
(2)、系统调度模式:PTHREAD_SCOPE_SYSTEM
用户线程是由应用程序通过调用 pthread 库来创建的,他们想要执行就需要与处理机关联起来。
线程与处理机关联是通过内核线程来实现的。内核线程由线程库调用操作系统提供的系统调用
创建,用户线程只有与操作系统内核线程相关联才能被调度到处理器上面。
(1)、绑定的线程:用户线程与一个内核线程唯一关联
(2)、非绑定线程:可以与进程内的任意一个有效的内核线程相关联
多线程的实现:
(1)、多对一模式:即进程内的所有用户线程都与该进程唯一的一个内核线程相连,也称为用户线
程实现,因为
他是由用户级别的线程库来实现的,不需要操作系统的特殊支持,用户线程库负责调度
(2)、一对一模式:每一个用户线程直接对应一个内核线程,并从始至终的与这个内核线程绑定在
一起,这种模式
是在操作系统内核空间实现的,因此也称为内核线程
(3)、混合模式:即多对一和一对一都存在的,也称为内核线程实现,因为操作系统调度的是内核
线程而不是用户线程
------------------------------------------------------
调度策略与优先级:
三种调度策略:
(1)、SCHED_FIFO:先进先出,具有优先级意识,维护多个优先级队列,每次从队列头取出线程
执行,执行完放在队列尾。
低优先级的线程有可能被高优先级的线程替代,需要注意的是:如果线程是被阻塞的,系统会将他
放在队列的尾部,如果线程
是被取代的,那么这个线程将被放在线程队列里面的最前面,但是这个策略不受时间片的影响,所
以可能会独占处理机。
(2)、SCHED_RR:时间片轮转,如果一个线程的时间片用完了,系统将会把这个线程放在队列的
尾部重新排队,如果这个线程是
被具有更高优先级的线程取代的,则在他恢复执行后,使用被取代前剩余的时间片大小。
(3)、SCHED_OTHER:我也不知道该怎么说。
-----------------------------------------------------------------
查看优先级区间:
int
sched_get_priority_max(int policy):
int
sched_get_priority_min(int policy):
-----------------------------------------------------------------
线程调度属性:
(1)、竞争范围属性,我们已经知道可以取哪两个值
(2)、PTHREAD_INHERIT_SCHED OR PTHREAD_EXPLICIT_SCHED:继承属性,前者表示新线
程的调度策略和相连的
调度参数从创建线程继承,后者则表示自己设定
(3)、SCHED_FIFO OR SCHED_RR OR SCHED_OTHER
(4)、调度参数属性
int
pthread_attr_getscope(const pthread_attr_t* restrict attr,int
*restrict contentionscope)
int
pthread_attr_setscope(pthread_attr_t* attr,int
contentionscope)
47int
*inherit)
int
inherit)
int
int
pthread_attr_getinheritsched(pthread_attr_t* attr,int
pthread_attr_setinheritsched(pthread_attr_t* attr,int
pthread_attr_getschedpolicy(pthread_attr_t* attr,int* policy)
pthread_attr_setschedpolicy(pthread_attr_t* attr,int policy)
int
pthread_attr_getschedparam(const pthread_attr_t *restrict
attr,struct sched_param* restrict param)
int
pthread_attr_setschedparam(pthread_attr_t* attr,const struct
sched_param* restrict param)
----------------------------------------------------------------------
动态改变线程的调度策略和优先级:
int
pthread_getschedparam(pthread_t thread,int* restrict
policy,struct sched_param* restrict param)
int
pthread_setschedparam(pthread_t thread,int policy,const struct
sched_param* param)
int
pthread_setschedprio(pthread_t thread,int prio);
110、线程与信号
在多线程模式下,任然通过函数 sigaction 函数来建立,并且被进程中的所有线程共享,即对于每
一种信号,所有线程
的信号动作都相同,也就是说,信号动作是进程范文内的。
在多线程模式下,进程内的每一个线程都有一个信号屏蔽,这样,尽管所有线程共享相同的信号动
作,但每一个线程可以阻塞
自己不想处理的信号。
int
pthread_sigmask(int how,const sigset_t* restrict set,sigset_t*
restrict oset)
我们可以回想函数:sigprocmask。
如果 set 不是 NULL 的话,它指向用于改变信号屏蔽的信号集合。此时参数 how 指明任何改变信号
(1)、SIG_BLOCK:将 set 所指的信号集合添加到现有的信号集合中
(2)、SIG_UNBLOCK:将 set 所指的信号集合从现有的信号集合中移除
(3)、SIG_SETMASK:将 set 所指的信号集合设置为新的信号屏蔽集合
oset 将返回原来的信号屏蔽。
--------------------------------------------------------
向线程发送信号:
线程可以向进程发送信号,也可以向进程内的线程发送信号,但是不能向其他进程的线程发送
信号,因为线程 ID
是进程范围内有效的。
线程向进程发送信号任然使用 kill 函数,如果要向其他线程发送信号,则需要下面的函数来
实现:
int
pthread_kill(pthread_t thread,int sig):
该函数向 thread 发送一个 sig 信号,如果 sig 为 0,则同 kill 一样。
我们可以通过下面的函数调用来实现向线程向自己发送信号:
int
pthread_kill(pthread_self(),sig)
int
raise(sig)
----------------------------------------------------------------
等待信号:
使用下面这个函数,在收到信号后直接对信号进行处理而不再需要信号句柄:
int
sigwait(const sigset_t *restrict set,int *restrict sig)
48111、一种新的事件通知方法:SIGEV_THREAD
让我们再次看看一个结构:
struct sigevent
{
union sigval sigev_value;
int
sigev_signo;
int
sigev_notify;
void(*sigev_notify_function)(union sigval);
pthread_attr_t* sigev_notify_attributes;
}
如果指定 sigev_notify 为 SIGEV_THREAD,系统将调用 sigev_notify_attributes 指定的
属性创建一个新的线程。该
线程将执行 sigev_notify_function 指定的函数,sigev_value 则将作为该函数的参数。
--------end 2016/3/22-----------------hujian--------
nankai-------------------
49
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章