目录

Linux系统编程

目录

linux系统编程

1 文件I/O

  • 文件描述符,文件描述符是一个非负整数。
  • 0标准输入,1标准输出,2标准错误
  • STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO

普通文件的文件描述符,在内核空间是这样的: /计算机基础/filestruct.png

1.1 函数open和openat

1
2
3
4
#include <fcntl.h>
int open(const char *path, int oflag,.../* mode_t mode */);
int openat(int fd,const char *path, int oflag,.../* mode_t mode*/);
										//成功返回文件描述符,出错返回-1
  • fd参数把open和openat区分开

    • 1 path参数指定的是绝对路径名,fd参数被忽略,两函数没有区别
    • 2 path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd是通过打开相对路径名所在的目录来获取的
    • 2 path参数指定的是相对路径名,fd参数具有特殊值AT_FDCWD,在这种情况下路径名在当前工作目录中获取
  • oflag选项可用选项

作用 备注
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开 大多数实现将前三种定义成0,1,2
O_EXEC 只执行打开
O_APPEND 追加模式 在每次写入操作执行之前,自动将文件指针定位到文件末尾
O_CLOEXEC FD_CLOEXEC常量设置为文件描述符标志
O_CREAT 若文件不存在则创建 需要给定第三个参数,设置文件权限,最终权限受umask影响
O_DIRECTORY 如果path不是目录则出错
O_EXCL 如果同时指定了O_CREAT,而文件已存在,则出错
O_NOCTTY 如果path引用的是终端设备,则不将该终端分配作为此进程的控制终端
O_NOFOLLOW 如果path是一个符号链接,则出错
O_NONBLOCK 如果path引用的是个FIFO,一个块或字符文件特殊文件,设置I/O操作为非阻塞
O_SYNC 使每次write等待物理I/O操作完成,包括write操作引起的文件属性更新所需的I/O
O_TRUNC 如果此文件存在,且以写属性打开,将其长度截断为0 对FIFO和终端文件不管用
O_TTY_INIT 打开一个未打开的终端设备,设置非标准termios参数,使其符合singel unix specification

1.2 函数creat

1
2
3
4
5
#include <fcntl.h>
int creat(const char *path, mode_t mode);
									//成功返回只写打开的文件描述符,出错返回-1
//等效于open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
//不怎么用了,用open都可以实现,且更灵活

1.3 函数lseek

  • 每打开一个文件都有一个与其关联的“当前文件偏移量”(current file offset)
1
2
3
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
									//成功返回新的文件偏移量,出错返回-1
  • 对管道,FIFO或网络套接字使用,返回-1,设置errnoESPIPE
  • whence 参数取值 /wens/ 根源
    • SEEK_SET ,将该文件偏移量设置为距文件开始处offset个字节
    • SEEK_CUR ,将该文件偏移量设置为当前值+offsetoffset可正可负
    • SEEK_END ,将该文件偏移量设置为文件长度+offsetoffset可正可负

1.4 函数read

1
2
3
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
										//返回实际读到的字节数,出错返回-1
  • 当文件指针已位于文件结尾,返回0,在套接字上读到0说明另一端关闭

  • 返回-1时会设置errno

错误值 含义
EAGAIN 使用O_NONBLOCK标志 指定了非阻塞输入输出,但当前没有数据可读。
EBADF fd不是一个合法的文件描述符,或者不是为了读操作而打开。
EINTR 在读取到数据以前调用被信号中断。
EINVAL fd所指向的对象不合适读,或者是文件打开时指定了O_DIRECT标志。
EISDIR fd指向一个目录。

1.5 函数write

1
2
3
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
											//返回写入字节数,出错返回-1
  • 错误值
错误值 含义
EBADF fd不是一个合法的文件描述符,或者不是为写 操作而打开。
EINTR 系统调用在写入任何数据之前调用被信号所中断。
EINVAL fd所有指向的对象不合适写,或者是文件打开时指定了O_DIRECT标志。
ENOSPC fd指向的文件所在的设备无可用空间。
EPIPE fd连接到一个管道,或者套接字的读方向一端已经关闭。

1.6 函数dup和dup2

  • 两函数都复制一个现有的文件描述符
1
2
3
4
#include <unistd.h>
int dup(int fd);
int dup2(int fd, ing fd2);
							//成功返回新的文件描述符,出错返回-1
  • dup返回到新描述符一定是当前可用的描述符中最小值
  • dup2可以用fd2指定新描述符的值,如果fd2已打开,则先将其关闭。
  • 若fd=fd2,返回fd2但不执行关闭动作。否则fd2的FD_CLOEXEC标志就被清除,这样fd2在进程调用exec时是打开状态

1.7 函数close

  • 可调用close函数关闭一个打开文件
1
2
#include <unistd.h>
int close(int fd);		//成功返回0,否则返回-1
  • 当关闭一个文件还会释放该进程加在该文件上的所有记录锁。

  • 当进程终止时,内核自动关闭它打开的所有文件,很多程序利用这一点不显式调用。

1.8 函数 sync fsync fdatasync

  • 将缓冲区内容同步到磁盘
1
2
3
4
5
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
						//成功返回0,否则返回-1
void sync(void);
  • sync 只是将所有修改过的块缓冲区写入队列,就返回,不等待实际磁盘操作

  • fsync只对文件描述符指定的一个文件起作用,并等待磁盘操作结束才返回

  • fdatasync类似发fsync,但它只影响文件的数据部分,fsync同时更新属性部分

1.9 函数 fcntl

  • fcntl 函数可以改变已经打开文件的属性
1
2
3
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*int arg*/);
								//返回值:成功则依赖于cmd,出错返回-1
  • cmd
参数 功能
F_DUPFD 复制文件描述符fd,新文件描述符作为返回值,它是尚未打开的各描述符中大于等于第三个参数值中最小的值。新描述符有他自己的一套标志,其中FD_CLOEXEC被清除,
F_DUPFD_CLOEXEC 同上,但D_CLOEXEC被设置
F_GETFD 对应于fd的文件描述符标志作为函数返回值。
F_SETFD 对于fd设置文件描述符标志,新标志按第三个参数传递(如FD_CLOSEXEC)
F_GETFL 对应于fd的文件状态标志作为函数返回值,open函数下面有列出
F_SETFL 设置状态标志,可更改的有:O_APPEND,O_NONBLOCK,O_SYNC
F_GETOWN 获取当前SIGIOSIGURG信号(两种异步io信号)的进程id或进程组id
F_SETOWN 设置接收当前SIGIOSIGURG信号的进程id或组id,正的arg指定进程id,负的arg指定组id

1.10 函数 ioctl

  • 不能用其他函数表示的i/o操作通常都可以用这个
  • 每个设备驱动文件可以定义他自己专用的一组ioctl命令,系统则为不同驱动提供通用ioctl命令
1
2
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...); //出错返回-1,成功返回其他值

1.11 /dev/fd

  • 较新的系统都有名为/dev/fd的目录,目录下是名为0,1,2的文件

  • 打开/dev/fd/n等效于复制描述符n,如fd = open("/dev/fd/0",mode)等效fd = dup(0)

2 C 标准I/O

2.1 文件指针

  • 标准I/O并不直接操作文件描述符,而是通过文件指针(file pointer)。文件指针映射到一个文件描述符。文件指针类型为FILE,定义在头文件<stdio.h>
  • 在标准I/O中,一个打开的文件叫做“流”(stream)。
  • 标准输入 输出 错误的流通过预定义文件指针 stdinstdoutstderr加以引用

2.2 打开文件

  • 文件通过 fopen()打开以提供读写操作:打开文件并为它关联一个新的流
1
2
3
#include <stdio.h>
FILE* fopen(const char * path, const char * mode);
									//成功返回文件指针,否则返回NULL
  • mode
mode 说明
r 打开文件用来读,流定位在文件开始处
r+ 打开文件用来读写,流定位在文件开始处
w 打开文件用来写,如果文件存在,文件会被清空。如果不存在,会被创建。流定位在文件开始。
a d打开文件用来追加模式的写入。如果文件不存在它会被创建。流被设置在文件的末尾,所有写入都会接在文件后。
a+ 打开文件用来追加模式的读写。如果文件不存在它会被创建。流被设置在文件的末尾,所有写入都会接在文件后。
b 表示二进制形式,这个值在linux下被忽略,因为文本文件和二进制文件都一视同仁
  • 通过文件描述符打开文件
  • 函数fdopen()将一个已经打开的文件描述符转换成流
1
2
#include <stdio.h>
FILE* fdopen(int fd, const char * mode);
  • 可能模式与上面一样,但必须和原来打开文件描述符的模式匹配。可以指定w,w+,但是他们不会截断文件。流的位置设置在文件描述符指向的文件位置。可以但不要在打开流的情况下直接操作描述符。关闭流也会关闭描述符。

2.3 关闭流

  • fclose()函数关闭一个给定的流
1
2
3
#include <stdio.h>
int fclose(FILE * stream);
						//成功返回0,失败返回EOF(-1),设置errno
  • fcloseall()关闭所有和当前程序关联的流,包括标准输入,输出,错误
1
2
3
#include <stdio.h>
int fcloseall(void);
				//该函数始终返回0;该函数是linux特有的

2.3 从流中读取数据

  • 单字节读取
1
2
3
#include <stdio.h>
int fgetc(FILE * stream);
	//读取一个字符,并把该无符号字符强转成int返回。文件结尾,错误都返回EOF(-1)
  • 把字符放回流中
  • 允许你偷窥流,如果不需要,可以把它放回
1
2
3
4
#include <stdio.h>
int ungetc(int c,FILE * stream);
	//把c强转为无符号字符,放回流中,成功返回c,否则返回EOF
	//如果使用了一次定位函数,所有推回的字符被丢弃
  • 按行读取
1
2
3
4
#include <stdio.h>
char * fgets(char *str, int size, FILE * stream);
	//从流中读取最多size-1个字节数据,遇到文件尾或换行时读入结束,如果读到‘\n’
	//也放入str,最后把空字符'\0'放到str结尾。成功返回str,失败返回NULL
  • 读取二进制文件
1
2
3
4
5
#include <stdio.h>
size_t fread(void *buf, size_t size, size_t nr, FILE* stream);
	//从流中读取nr个数据,每个数据大小为size,放入buf中
	//实际读入的个数被返回(注意不是字节数)
	//返回小于nr的值时读取失败或文件结束,用ferror()和feof()判断

2.4 向流中写数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>
//写入单个字符
int fput(int c, FILE * stream);
	//成功返回c,否则返回EOF
//写入字符串
int fputs(const char *str, FILE *stream);
	//将str中'\0'前部分写入stream指向的流中,成功返回非负整数,失败EOF
//写入二进制数据
size_t fwrite(void *buf, size_t size, size_t nr, FILE *stream);
	//把buf指向的nr个元素(每个元素大小为size)写入流,返回实际写入个数,小于nr出错

2.5 定位/清洗流

1
2
3
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
	//成功返回0,清空文件结束标志,取消ungetc()操作,错误返回-1
  • SEEK_SET ,将该文件偏移量设置为距文件开始处offset个字节
  • SEEK_CUR ,将该文件偏移量设置为当前值+offsetoffset可正可负
  • SEEK_END ,将该文件偏移量设置为文件长度+offsetoffset可正可负
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//将流位置设置到pos处
int fsetpos(FILE *stream, const fpos_t *pos);
	//成功返回0,否则返回-1
//将当前位置填入pos
int fegtpos(FILE *stream, fpos_t *pos);

void rewind(FILE *stream);
	//等于 fseek(stream,0,SEEK_SET); 位置重置为初始位置

//返回当前流位置
long fetll(FILE *stream);
	//错误返回-1

//清洗一个流,流中数据立即刷新到内核中
int fflush(FILE *stream);

2.6 错误和文件结束

1
2
3
4
5
6
7
8
9
#include <stdio.h>
//测试流上是否设置了错误标志
int ferror(FILE *stream);
	//如果有错误标记,返回非0,否则返回0
//测试文件结尾标记是否被设置
int feof(FILE *stream);
	//如果有文件结束标记,返回非0,否则返回0
//清空错误和结尾标记
void cleareer(FILE *stream);

2.7 控制缓冲和关联描述符

  • 提供三种用户缓冲,不缓冲,行缓冲(标准输出默认),块缓冲(文件操作默认)
1
2
3
4
5
6
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
	//buf为NULL是关闭缓冲,否则大小由常量BUFSIZ决定
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
	//必须在流打开后,任何操作之前调用
	//成功返回0,否则返回非0
  • mode

    • _IONBF 无缓冲
    • _IOLBF 行缓冲
    • _IOFBUF 块缓冲
  • _IONBF情况下buf和size被忽略

  • buf可以指向一个size字节大小的缓冲空间,如果buf为NULL,则由glibc自动分配

  • 获得关联文件的描述符

1
2
3
#include <stdio.h>
int fileno(FILE *stream);
	//成功返回和流关联的文件描述符,否则返回-1

2.8 线程安全

  • 单个I/O操作是原子的,但有时需要加锁一组操作

  • 又时有不需要单个I/O的锁机制

  • 手动文件加锁

1
2
3
4
5
6
7
8
#include <stdio.h>
void flockfile(FILE *stream);
	//等待流解锁 并 获得锁 然后返回
vodi funlockfile(FILE *stream); //解锁

//非阻塞加锁版本
int ftrylockfile(FILE *stream);
	//没有拿到锁返回非零,拿到了返回0
  • 不加锁的流操作,linux提供一系列函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int getc_unlocked(FILE *stream);
int getchar_unlocked(void);
int putc_unlocked(int c, FILE *stream);
int putchar_unlocked(int c);

void clearerr_unlocked(FILE *stream);
int feof_unlocked(FILE *stream);
int ferror_unlocked(FILE *stream);
int fileno_unlocked(FILE *stream);
int fflush_unlocked(FILE *stream);
int fgetc_unlocked(FILE *stream);
int fputc_unlocked(int c, FILE *stream);
size_t fread_unlocked(void *ptr, size_t size, size_t n,
                      FILE *stream);
size_t fwrite_unlocked(const void *ptr, size_t size, size_t n,
                      FILE *stream);

char *fgets_unlocked(char *s, int n, FILE *stream);
int fputs_unlocked(const char *s, FILE *stream);
...

3 高级IO

3.1 非阻塞IO

两种指定非阻塞io的方法:

  • 如果调用open获得描述符,可指定O_NONBLCK标志
  • 对于一个已经打开的描述符,调用fcntl,由该函数打开O_NONBLCK标志
1
2
3
4
5
6
//1
if(open("path/a.txt",O_RDWR|O_NONBLOCK) == -1 ) return -1;
//2
int flags;
if((flags = fcntl(fd, F_GETFL, NULL)) < 0) return -1;
if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) return -1;
  • 如果调用read/write时暂时没有数据可读,返回-1,设置errnoEAGAIN

3.2 fcntl记录锁

1
2
3
4
5
6
7
8
9
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /*struct flock * flockptr*/);
struct flock {
    short l_type;	/*F_RDLCK,F_WRLCK,or F_UNLCK*/
    short l_whence;	/*SEEK_SET,SEEK_CUR or SEEK_END*/
    off_t l_start; 	/*offset in bytes,relative to l_whence*/
    off_t l_len;	/*length,in bytes, 0 mens lock to EOF*/
    pid_t l_pid;	/*returned with F_GETLK*/
}

对flock结构说明如下

  • 所希望的锁的类型:F_RDLCK共享读锁,F_WRLCK独占性写锁,or F_UNLCK解锁一个区域

  • 要加锁或解锁的区域的起始位置 l_whence + l_start

  • 区域的字节长度

  • 进程的ID持有的锁能阻塞当前进程(仅由F_GETLK返回)

  • 锁可以在当前文件尾部开始,或越过尾部开始,但不能在文件起始位置之前开始

  • l_len为0,表示锁的范围无限大

  • 为了对整个文件加锁,设置l_start和l_whence指向文件起始位置,l_len为0

俩个锁实现:可以同时读,读时不能写。只能一个写,写时不能读。

cmd

  • F_GETLK:如果锁能被设置,该命令并不真的设置锁,而是只修改lock的l_type为F_UNLCK,然后返回该结构体。如果存在一个或多个锁与希望设置的锁相互冲突,则fcntl返回其中的一个锁的flock结构。出错返回-1
  • F_SETLK:加锁或解锁,如果锁被其他进程占用,则返回-1,这种情况设的锁遇到锁被其他进程占用时,会立刻停止进程。;
  • F_SETLKWF_SETLK的阻塞版本

3.3 IO多路转接

如果我们的程序要处理多个输入输出,我们不能阻塞在任意一个上面,也不知道何时有哪个fd上的数据可读。

使用多进程,增加复杂性,父子进程同步

使用多线程,增加复杂性,线程同步

一个程序,非阻塞IO,轮询,长时间空转,浪费性能

异步IO(asynchronous IO),当描述符准备好可以IO时,内核用一个信号通知进程。缺点标准不统一,信号有限(SIGPOLL或SIGIO,各系统不一致),难以管理多个描述符。

IO多路转接(IO multiplexing),先构造一张我们感兴趣的描述符的列表,然后调用一个函数,知道这些描述符中的一个已准备好IO时,该函数才返回。poll,pselect,select这三个函数使我们能够进行IO多路转接。

  • posix指定,为了使用select,必须包含<sys/select.h>。

3.3.1 select和pselect

在所有posix兼容的平台上,select函数使我们可以执行IO多路转接,传给select的参数告诉内核:

  • 我们关心的描述符
  • 对于每个描述符我们所关心的条件(是否想读,是否想写,是否关心异常条件)
  • 愿意等待多长时间(可以永远等待,等待一个固定的时间或根本不等待)

从select返回时,内核告诉我们:

  • 已准备好的描述符的总量
  • 对于读,写,异常这3个条件中的每一个,哪些描述符已准备好

使用这种返回信息,就可以调用IO函数(一般是read或write),并确知不会阻塞。

1
2
3
4
5
#include <sys/select.h>
int select(int maxfdp1, fd_set * readfds,
          fd_set * writefds, fd_set * exceptfds,
          struct timeval *restrict tvptr);
				//返回准备就绪的描述符数量;若超时返回0,出错返回-1
  • maxfdp1是最大文件描述符编号值加1,找出三个描述符集中最大的描述符编号值,然后加1。也可以设置为FD_SETSIZE常量,经常是1024。对于大多数程序,此值过大。

  • 中间三个参数readfdswritefdsexceptfds是指向描述符集的指针,既是输入又是输出,每个描述符集存储在一个FD_SET数据类型中。描述符集可以看做一个大数组,关心哪个描述符就在数组中置1,select将更新这个集合,把其中不可读的套节字去掉。如果三个参数都是NULL,可以 提供比sleep()更精确的睡眠

    1
    2
    3
    4
    5
    6
    
    #include <sys/select.h>
    int FD_ISSET(int fd, fd_set *fdset);
                                  //若fd在描述符集中,返回非0,否则返回0
    void FD_CLR(int fd, fd_set *fdset); //开启描述符中的一位
    void FD_SET(int fd, fd_set *fdset);	//清除一位
    void FD_ZERO(fd_set * fdset);		//将所有位清0
    
  • tvptr,等待时间,单位秒和微秒

    1
    2
    3
    
    tvptr == NULL   永远等待,知道知道描述符中一个已经准备好。如果捕捉到一个信号则返回-1errno=EINTR
    tvpty->tv_sec == 0 && tvptr->tv_usec == 0  不等待
    tvpty->tv_sec != 0 || tvptr->tv_usec != 0  等待指定时间,也可被信号打断
    

返回-1,表示出错,如被信号打断

返回0,表示没有描述符准备好,所有描述符集都会被置0

返回正值,说明已经准备好的描述符数。该值是3个描述符集中已准备好的个数之和。同一个描述符准备 好读和写,那么返回值会计数两次。

当一个描述符达到文件尾端,select仍认为是可读的,这时候read返回0

  • POSIX也定义了一个select的变体pselect
1
2
3
4
5
6
#include <sys/select.h>
int pselect(int maxfdp1, fd_set * readfds,
          fd_set * writefds, fd_set * exceptfds,
          const struct timespec *restrict tsptr,
          const sigset_t * sigmask);
				//返回准备就绪的描述符数量;若超时返回0,出错返回-1

不同点:

  • 使用timespec提供纳秒级别控制
  • 超时值被声明为const,保证pselect不会改变此值
  • 可使用可选信号屏蔽字。原子操作安装屏蔽字,返回时恢复以前的信号屏蔽字

3.3.2 poll

poll函数类似select,但是程序员接口有所不用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
							//返回就绪的描述符数目,若超时返回0,出错返回-1

//与select不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构的数组
struct pollfd {
    int fd;			/*file descriptor to check, or < 0 to ignore*/
    short events;	/*events of interest on fd*/
    short revents;	/*events that occured on fd 由内核设置,告诉我们发生了什么*/
}
  • events应设置:

    POLLIN普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND POLLRDNORM‐普通数据可读 POLLRDBAND‐优先级带数据可读 POLLPRI 高优先级可读数据 POLLOUT普通或带外数据可写 POLLWRNORM‐数据可写 POLLWRBAND‐优先级带数据可写 POLLERR 发生错误 POLLHUP 发生挂起 POLLNVAL 描述字不是一个打开的文件

  • nfds指定数组中的元素数

  • timeout毫秒级等待,-1永远等待,0不等待

3.4 epoll

由于poll和select的局限性,linux引入了event poll机制。虽然比前两个复杂了很多,但解决了他们共有的性能问题。它能显著提高程序在大量文件描述符中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

  • 创建epoll实例:
1
2
3
4
#include <sys/epoll.h>
int epoll_create(int size);
					//创建一个实例,返回一个epoll fd,出错返回-1
int epoll_create1(int flag); // EPOLL_CLOEXEC设置描述符标志  为0和epoll_create一样

出错设置errno为下列值之一:

EINVALsize不是正数

ENFILE系统达到打开文件数的上限

ENOMEN没有足够内存完成此次操作

1
2
3
4
5
int epfd;
epfd = epoll_create(100);/*plan to watch ~100 fds*/
if(epfd<0){
    perror("epoll_create");
}

epoll返回的文件描述符需要用close()关闭。

  • 控制epoll
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
						//成功返回0,失败返回-1
struct epoll_event {
	__u32 events; /* events */
	union {
		void *ptr;
		int fd;
		__u32 u32;
		__u64 u64;
	} data;
};

参数op指定要对fd进行的操作:增删改

EPOLL_CTL_ADD把fd指定文件添加到epfd指定的epoll实例监听集中,监听event中定义的事件

EPOLL_CTL_DEL从监听集合中删除

EPOLL_CTL_MOD使用event改变在已有fd上的监听行为

epoll events结构体中的events参数列出了在给定文件描述符上监听的事件。可以用位运算同时指定

EPOLLIN 文件描述符可读(包括对端SOCKET正常关闭) EPOLLOUT文件描述符可写 EPOLLPRI高优先级的带外数据可读 EPOLLERR文件出错,即使没有设置这个事件也是被监听的 EPOLLHUP文件被挂起,即使没有设置这个事件也是被监听的 EPOLLET将EPOLL设为边缘触发(Edge Triggered)模式,默认是水平触发(Level Triggered) EPOLLONESHOT只监听一次事件,当监听完这次事件之后,必须使用EPOLL_CTL_MOD指定新事件,以便重新监听文件

event_poll中的data字段由用户使用。确认事件监听后,data会被返回给用户,通常将event.data.fd设定为fd

失败后errno为下列值:

EBADF epfd不是一个有效地epoll实例,或者fd不是有效地文件描述符

EEXIST op为 EPOLL_CTL_ADD,但是fd已经关联过,不可重复关联

EINVAL epfd不是一个epoll实例,epfd和fd相同,或者op无效

ENOENT op为 EPOLL_CTL_MODEPOLL_CTL_DEL ,但fd没有与epfd关联

ENOMEN 内存不足

EPERM fd不支持epoll

  • 等待epoll事件
1
2
3
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
							//返回事件数,出错返回-1

出错设置errno :

EBADF epfd无效

EFAULT 进程对events指向的内存无写权限

EINTR 被信号打断

EINVAL epfd不是有效epoll实例,或者maxevents大于创建时的size

timeout为-1,永远等待,为0不等待,如果超时函数返回0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define MAX_EVENTS 64
struct epoll_event * events;
int nr_events, i, epfd;
events = malloc(sizeof(struct epoll_event) * MAX_EVENTS); /*创建接收结果的数组*/
if(!events){
    perror("malloc");
    return 1;
}
nr_events = epoll_wait(epfd, events, MAX_EVENTS, -1);
if(nr_events < 0){
    perror("epoll_wait");
    free(events);
    return 1;
}
for(i=0; i < nr_events; ++i){
    printf("event=%ld on fd=%d\n",events[i].events,events[i].data.fd);
    /*do something IO*/
}
free(events);

3.5 异步IO

4 文件和目录

5 进程控制

5.1 进程环境

  • main函数

c程序总是从main函数开始执行,main函数的原型是:

1
int main(int argc, char *argv[]);

内核执行c程序时(使用exec函数),调用main前先调用一个特殊的启动例程,启动例程从内核取得命令行参数和环境变量值。此启动例程被设置为程序起始地址,这是由连接编辑器设置的。

/计算机基础/linux程序内存布局.png

  • 进程终止

5中正常终止:

  1. 从main返回
  2. 调用exit
  3. 调用 _exit 或 _Exit
  4. 最后一个线程从其启动例程返回
  5. 最后一个线程调用 pthread_exit

3中异常终止:

  1. 调用abort
  2. 接到一个信号
  3. 最后一个线程对取消请求作出相应
  • 退出函数
1
2
3
4
5
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);

3个函数用于正常终止一个程序,_exit 和 _Exit 立即进入内核,exit则先执行一些清理处理,然后返回内核

  • 函数 atexit
1
2
3
#include <stdlib.h>
int atexit(void (*func)(void));
					//成返回0,失败返回非0

一个进程可以登记至多32个函数,这些函数由exit自动调用。调用顺序与登记顺序相反

  • 环境表

每个程序都接收到一张环境表。

1
extern char **environ;

通常使用 getenv 和 putenv 函数来访问特定的环境变量,但如果要查看整个环境,必须使用environ

1
2
3
4
5
6
7
8
#include <stdlib.h>
char *getenv(const char *name);
				//返回与name关联的value指针,找不到返回NULL
int putenv(char *str);
				//成功返回0,出错非0
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
				//成功返回0,失败返回-1

putenv去形式为name=value的字符串,name已存在则先删除原来的

setenv将name设置为value,若name已存在,rewirte 取1/0控制是否删除原来或放弃操作,放弃不算出错

unsetenv删除name的定义

5.2 进程标识符

每个进程都有一个非负整形表示的唯一进程ID

1
2
3
4
5
6
7
8
9
#include <unistd.h>
pid_t getpid(void);		//返回进程ID
pid_t getppid(void);	//返回父进程ID

uid_t getuid(void);		//返回调用程序的实际用户ID
uid_t geteudi(void);	//返回有效用户ID

gid_t getgid(void);		//返回实际组ID
gid_t getegid(void);	//返回有效组ID

这些函数都没有出错返回。

5.3 fork

一个现有的进程可以调用fork函数创建一个新进程

1
2
3
#include <unistd.h>
pid_t fork(void);
			//子进程返回0,父进程返回子进程ID,出错返回-1

子进程获得父进程数据空间,堆栈的副本。并不共享(写时复制Copy-On-Write)

文件共享:

/计算机基础/twouseone.png

除了打开文件之外,父进程很多其他属性也被继承:

  1. 实际用户ID,实际组ID,有效用户ID,有效组ID
  2. 附属组ID
  3. 进程组ID
  4. 会话ID
  5. 控制终端
  6. 设置用户ID标志和设置组ID标志
  7. 当前工作目录
  8. 根目录
  9. 文件模式创建屏蔽字
  10. 信号屏蔽和安排
  11. 对任一打开文件描述符的执行时关闭(close-on-exec)标志
  12. 环境
  13. 连接的共享存储段
  14. 存储映像
  15. 资源限制

父进程和子进程之间的区别如下:

  1. fork返回值不同
  2. 进程ID不同
  3. 父进程ID不同
  4. 子进程的tms_utime、tmd_stime、tms_cutime 和 tms_ustime 的值设置为0
  5. 子进程不继承父进程设置的文件锁
  6. 子进程的未处理闹钟被清除
  7. 子进程的未处理信号集设置为空集

fork失败有两个主要原因:系统有太多进程,用户进程数超过限制

5.4 函数wait和waitpid

当一个进程正常或异常结束时,内核就向其父进程发送SIGCHLD信号。

调用wait和waitpid可能会发生:

  1. 如果其所有子进程都还在运行,则阻塞
  2. 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程终止状态立即返回
  3. 如果他没有任何子进程,立即出错返回
1
2
3
4
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
								//俩函数成功返回进程id,出错返回0或-1

两函数区别:

  1. 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项可以不阻塞
  2. waitpid不等待在其调用之后的第一个 终止子进程,他有若干个选项,可以控制他所等待的进程

参数statloc是一个整型指针。如果不是空指针,则终止进程的状态就放在里面,如果不关心终止状态,可以指定为NULL

有4个互斥的宏可以用来取得进程终止的原因

说明
WIFEXITED(status) 若为正常终止的状态,返回真。可以执行WEXITSTATUS(status)获取子进程传送给exit的低8位
WIFSIGNALED(status) 若为异常常终止的状态,返回真(接到一个不捕捉的信号),可执行WTERMSIG(status)获取使进程终止的信号编号。有些实现定义宏WCOREDUMP(status)返回是否已产生终止进程的core文件
WIFSTOPPED(status) 若为当前暂停子进程的返回的状态,则为真,可执行WSTOPSIG(status),获取使进程暂停的信号编号
WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回了状态,则为真。(仅用于waitpid)

对与waitpid的参数pid

1
2
3
4
pid == -1;  /*等待任一子进程,和wait没区别*/
pid > 0;	/*等待进程id与pid相等的子进程*/
pid == 0;	/*等待组id等于调用进程组id的任一子程序*/
pid < -1;	/*等待组id等于pid绝对值的任一子程序*/

options参数进一步控制waitpid的操作:

1
2
3
WCONTINUED /*若实现支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态*/
WNOHANG 	/*若没有等待的子进程,waitpid不阻塞 此时返回值为0*/
WUNTRACED	/*若实现支持作业控制,那么由pid指定的任一子进程停止,但其状态紫停止以来尚未报告,则返回其状态*/

waitpid提供了wait函数没有的三个功能

  1. waitpid可以等待特定进程
  2. waitpid提供了一个wait的非阻塞版本
  3. waitpid支持作业控制

5.5 函数exec

fork函数创建子进程后,子进程可以调用exec函数以执行另一个程序。exec只是用磁盘上的一个程序替换当前程序的正文段,数据段,堆段和栈段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <unistd.h>
int execl(const char *pathname, const char * arg0, ... /* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... 
           /* (char *)0 , char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[])

int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);
int execvp(const char *filename, char *const argv[]);

int fexecve(int fd, char *const argv[], char *const envp[]);
									//7个函数失败返回-1,若成功不返回

这些函数的第一个区别是前4个函数去路径名作为参数,后两个取文件名,最后一个取文件描述符

当指定filename作为参数时:

  1. 如果filename中包含/,就将其视为路径名
  2. 否则就按PATH环境变量搜寻

如果execlp或execvp使用路径前缀中的一个找到了可执行文件,但是不是二进制文件,就认为时shell脚本,调用/bin/sh执行

1
2
3
4
5
6
7
8
execl(/bin/ls,ls,-l,NULL); //ls -l  第一个参数描述命令path 
char* av[]={"ls","-l",NULL};
execv("/bin/ls",av); //ls -l

execlp(ls,ls,-l,NULL);
char* argin[]={"ls", "-l", NULL};
execvp(argin[0],argin); // ls -l;
//execlp 和 execvp 的区别是不用完整path,会在系统path环境变量中搜索

5.6 进程时间

我们可以度量的时间有三个,墙上的时钟,用户CPU时间,系统CPU时间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <sys/times.h>
clock_t times(struct tms *buf);
				//成功返回流逝的墙上的时间,出错返回-1
				//1970 年 1 月 1 日 00:00:00 到当前时间的秒数  64bit

struct tms {
    clock_t tms_utime;	/* user CPU time */
    clock_t tms_stime;	/* system CPU time */
    clock_t tms_cutime;	/* user CPU time,terminated children */
    clock_t tms_cstime;	/* system cpu time, terminated children */
};

6 进程关系

POSIX.1要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着发送继续信号(SIGCONT)

SIGHUB的默认动作是终止进程

7 信号

7.1 信号概念

信号是软件中断。每个信号都有一个名字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1) SIGHUP: #当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
2)SIGINT:#当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止里程。
3)SIGQUIT:#当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
4)SIGILL:#CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
5)SIGTRAP:#该信号由断点指令或其他trap指令产生。默认动作为终止里程并产生core文件。
6 ) SIGABRT:#调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
7)SIGBUS:#非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
8)SIGFPE:#在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
9)SIGKILL:#无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
10)SIGUSE1:#用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
11)SIGSEGV:#指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
12)SIGUSR2:#这是另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。1
13)SIGPIPE:#Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
14) SIGALRM:#定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
15)SIGTERM:#程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
16)SIGCHLD:#子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
17)SIGCONT:#停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为终止进程。
18)SIGTTIN:#后台进程读终端控制台。默认动作为暂停进程。
19)SIGTSTP:#停止进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
21)SIGTTOU:#该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
22)SIGURG:#套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
23)SIGXFSZ:#进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。
24)SIGXFSZ:#超过文件的最大长度设置。默认动作为终止进程。
25)SIGVTALRM:#虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
26)SGIPROF:#类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
27)SIGWINCH:#窗口变化大小时发出。默认动作为忽略该信号。
28)SIGIO:#此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
29)SIGPWR:#关机。默认动作为终止进程。
30)SIGSYS:#无效的系统调用。默认动作为终止进程并产生core文件。
31)SIGRTMIN~(64)SIGRTMAX:#LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程

7.2 函数signal

安装信号处理函数

1
2
3
4
5
6
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
					//成功返回以前的信号配置,出错返回SIG_ERR
//有的实现分开定义,但意思一样
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);

signo可以用上面的信号宏,也可以用 SIG_DFL, SIG_IGN ,表示默认动作和忽略

SIGCHLD信号处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void handle_sig_child()
{
	int status;
	pid_t pid;
	while(1){
		pid = waitpid(-1, &status, WUNTRACED | WCONTINUED | WNOHANG);
        if(pid <= 0) break;
		printf("recv child pid %d \n", pid);
		if(WIFEXITED(status))
			printf("child process exited with %d\n", WEXITSTATUS(status));
		else if(WIFSIGNALED(status))
			printf("child process signaled with %d\n", WTERMSIG(status));
		else if(WIFSTOPPED(status))
			printf("child process stoped\n");
		else if(WIFCONTINUED(status))
			printf("child process continued\n");
	}
}

7.3 函数kill和raise发送信号

kill函数将信号发送给进程或进程组。raise函数向自身发送信号

1
2
3
4
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
				//成功返回0 失败返回-1  raise(signo)等价于kill(getpid(),signo)

kill的pid参数有4种不同信号

1
2
3
4
pid > 0 	/*将信号发送给进程*/
pid == 0	/*将信号发送给同一进程组的所有进程*/ 
pid < 0		/*发送给其他进程组,id为pid绝对值*/
pid == -1 	/*发送给所有进程(前提有权限)*/

信号编号0定义以为空信号,如果signo参数是0,kill仍执行错误检查,但不发送信号,这个特性常用来检查一个特定的进程是否仍然存在。如果向不存在的进程发送信号,返回-1,errno=ESRCH

7.4 alarm和pause

1
2
3
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
					//返回值 0或以前设置的闹钟余留秒数

一个进程只能有一个闹钟时间,如果在调用alarm时,已有闹钟,返回余留时间,替换新的秒数

如果有以前注册的尚未超过的闹钟时间,seconds参数为0,则取消以前的闹钟时间,返回余留秒数

SIGALRM默认动作是终止进程

pause函数使调用进程挂起直到捕捉到一个信号

1
2
3
#include <unistd.h>
int pause(void);
			//返回-1,errno设置为EINTR,只有执行了一个信号处理程序并从其返回,pause才返回

7.5 信号集

需要一个能表示多个信号的数据类型——信号集(signal set)

POSIX.1定义数据类型sigset_t以包含一个信号集,并定义5个信号集处理函数

1
2
3
4
5
6
7
8
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
						//4个函数成功返回0,出错返回-1
int sigismember(const sigset_t *set, int signo);
						//若真,返回1,假返回0

7.5.1 函数sigprocmask

检测或更改,或同时检测和更改进程的信号屏蔽字

信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集

1
2
3
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
											//成功返回0,出错返回-1

若oset是非空指针,当前的信号屏蔽字通过oset返回。

若set是非空指针,参数how指示如何修改当前屏蔽字,否则how无效

1
2
3
SIG_BLOCK 	#新屏蔽字取set和老屏蔽字的并集
SIG_UNBLOCK	#新屏蔽字取set和老屏蔽字的交集
SIG_SETMASK	#新屏蔽字取set

不能阻塞SIGKILL和SIGSTOP信号

sigprocmask是仅为单线程定义的,多线程中的信号屏蔽使用另一个

7.5.2 函数sigpending

返回以信号集,对于调用进程而言,其中的信号是阻塞不能递送的,因而也是当前未决的。通过set返回

1
2
3
#include <signal.h>
int sigpending(sigset_t *set);
				//成功返回0,出错返回-1

7.5.3 函数sigaction

检查或(并)修改与指定信号相关联的处理动作

1
2
3
4
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act,
             struct sigaction *restrict oact);
					//成功返回0,失败返回-1

signo是要检测或修改其具体动作的信号编号。

若act非空,则要修改其动作。

若oact非空,则返回该信号的上一个动作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct sigaction
{
	void (*sa_handler)(int);
	void (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t sa_mask;
	int sa_flags;
};
/*
sa_handler : 早期的捕捉函数

sa_sigaction : 新添加的捕捉函数,可以传参, 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数

sa_mask : 在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的
阻塞信号集

sa_flags : SA_SIGINFO 或者 0 用来指定调用sa_handler还是sa_sigaction ,SA_SIGINFO 时为调用sa_sigaction,
SA_RESTART 让被打断的系统调用重新开始
*/
//如果设置了SA_SIGINFO,则按下列方式调用信号处理程序
void handler(int signo, siginfo_t *info, void *context);



//一个栗子
struct sigaction act;
act.sa_handler = sig_handle;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); //当进入信号处理函数的时候,屏蔽掉SIGQUIT的递达
sigaction(SIGINT, &act, NULL);

7.6 信号名和编号

数组下标是信号编号,数组元素是指向信号名字符串的指针

1
extern char *sys_siglist[];

可以使用psignal函数打印信号对应的字符串

1
2
3
4
5
#include <signal.h>
void psignal(int signo, const char *msg); //到标准错误

#include <string.h>
char *strsignal(int signo);		//返回字符串指针

8 线程

8.1 线程标识

每个线程有一个线程ID,线程ID只有在它所属的进程上下文中才有意义

线程ID使用pthread_t数据类型来表示的,不同实现可能定义不一样,linux用无符号长整形表示

1
2
3
4
5
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
						//比较tid,相等返回非0,否则返回0
pthread_t pthread_self(void);
						//返回调用线程的tid

8.2 线程创建

1
2
3
4
5
6
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
                  const pthread_attr_t *restrict attr,
                  void *(*start_rtn)(void *),
                  void *restrict arg);
//若成功返回0,否则返回错误编号

返回成功时,新建进程的ID会通过tidp传回

attr参数用于定制各种不同的线程属性,也可以填NULL,使用默认

新建进程从start_rtn函数的地址开始运行

arg是穿给start_rtn的参数,可以直接当值传也可当指针传一个结构体携带更多信息

新建线程继承调用线程的浮点环境和信号屏蔽字,但是挂起信号集会被清除

8.3 线程终止

如果进程中任意线程调用了exit,_Exit,_exit,那么整个进程就会终止

如果默认动作是终止进程,发送给线程的信号会终止整个进程

单个线程可以通过3种方式退出

  1. 线程可以简单地从启动例程中返回,返回值是线程退出码
  2. 被同一进程中的其他线程取消
  3. 线程调用pthread_exit

8.3.1 pthread_exit

1
2
#include <pthread.h>
void pthread_exit(void *rval_ptr)

rval_ptr参数是一个无类型指针。其他线程也可以通过pthread_join函数访问到这个指针

8.3.2 pthread_join

1
2
3
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
						//成功返回0,否则返回错误编号

调用线程将一直阻塞,直到指定的线程调用pthread_exit,从启动例程返回或被取消。

如果线程简单地从启动例程返回,rval_ptr就包含返回码。不关心可以穿NULL

如果线程被取消,有rval_ptr指定的内存单元就设置为PTHREAD_CANCELED

调用函数可以回收线程的资源,前提线程不能是分离态,否则返回EINVAL

8.3.3 pthread_cancel

线程可以调用pthread_cancel函数来请求取消同一进程中其他线程

1
2
3
#include <pthread.h>
int pthread_cancel(pthread_t tid);
				//成功返回0,否则返回错误码

默认情况下pthread_cancel函数会使得有tid表示的线程行为表现为如同调用了参数为PTHREAD_CANCELEDpthread_exit 函数

但是线程可以选择忽略取消或控制如何被取消,pthread_cancel并不等待线程终止,只提出请求

8.3.4 安装清理函数

线程可以安排他退出时需要调用的函数,与进程的atexit函数类似

这样的函数叫做线程清理处理程序,可以建立多个,记录在栈中

1
2
3
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

只有当线程执行以下动作时,清理函数是由pthread_clean_push函数调度的,调用时只有一个参数arg

  1. 调用pthread_exit时
  2. 相应取消请求时
  3. 用非零execute参数调用pthread_cleanup_pop时

如果execute参数设置为0,清理函数不被调用,并且删除上次安装的处理程序

8.3.5 pthread_detach

默认情况下,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被回收。在线程处于分离态调用pthread_join会产生未定义行为

可以调用pthread_detach分离线程

1
2
3
#include <pthread.h>
int pthread_detach(pthread_t tid);
					//成功返回0,失败返回错误编号

这个方法是在线程创建后分离,还可以在创建线程时候设置属性

8.4 线程属性

pthread_create的第二个参数不为NULL时,为指定线程的属性

这个参数的初始化和释放由下面函数控制

1
2
3
4
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destory(pthread_attr_t *attr);
						//成功返回0,失败返回错误编号

POSIX.1定义了线程属性

1
2
3
4
detachstate #线程分离状态属性
guardsize	#线程栈末尾的警戒缓冲区大小
stackaddr	#线程栈的最低地址
stacksize	#线程栈的最小长度

如果要线程在创建时就处于分离态,下面函数设置detachstate属性

1
2
3
4
5
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr,
                               int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int *detachstate);
										//成功返回0,失败返回错误编号

detachstate参数取PTHREAD_CREATE_DETACHEDPTHREAD_CREATE_JOINABLE

8.5 线程同步

当多个控制线程共享相同的内存时,要保证每个线程看到一致的数据视图。如果每个进程使用的变量都是其它线程不会读取的,那么就不存在一致性问题。同样如果变量是只读的,多个线程同时读取也没有问题。但是当一个线程可以修改的变量,其他线程也可以读取或修改,就需要对这些线程进行同步。

8.5.1 互斥量

可以使用pthread的互斥接口来保护数据,确保同一时间只有一个线程访问数据。

互斥量(mutex)本质是一把锁。

互斥变量用pthread_mutex_t数据类型表示。使用前必须初始化。

可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于与静态分配的互斥量)。

也可以通过调用函数初始化。在释放内存前要调用pthread_mutex_destroy(动态分配)

1
2
3
4
5
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restirct mutex,
                      const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
								//成功返回0,否则返回错误编号

要使用默认属性初始化互斥量,attr参数用NULL

互斥量的使用

1
2
3
4
5
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
								//成功返回0,否则返回错误编号

pthread_mutex_lock对互斥量加锁,如果互斥量已经上锁,调用线程阻塞直到互斥量被解锁

pthread_mutex_trylock加锁的非阻塞版本,不能加锁返回EBUSY

注意避免死锁

1
2
3
4
5
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict tsptr);
								//成功返回0,否则返回错误编号	

设置超时的方式加锁 超时返回ETIMEDOUT

8.5.2 读写锁

使用之前初始化,底层内存释放前必须销毁

1
2
3
4
5
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                       const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
								//成功返回0,否则返回错误编号

使用默认属性初始化attr传NULL

操作读写锁

1
2
3
4
5
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
								//成功返回0,否则返回错误编号

非阻塞方式

1
2
3
4
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
								//成功返回0,否则返回错误编号

不能得到锁返回EBUSY

带有超时的方式

1
2
3
4
5
6
7
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
                              const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
                              const struct timespec *restrict tsptr);
								//成功返回0,否则返回错误编号

超时返回ETIMEDOUT

8.5.3 条件变量

条件变量与互斥量一起使用时,运行进程以无竞争的方式等待特定条件发生。

初始化

静态初始化可以把常量PTHREAD_COND_INITIALIZER付给条件变量

动态分配使用函数初始化,在底层内存释放前destroy

1
2
3
4
5
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
                     const pthread_condattr_t *restrict attr);
int phtread_cond_destroy(pthread_cond_t *cond);
								//成功返回0,否则返回错误编号

使用默认属性创建,addr参数穿NULL。

等待条件

1
2
3
4
5
6
7
8
#include <pthread.h>
#include <time.h>
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 tsptr);
										//成功返回0,否则返回错误编号

把锁住的互斥量传给函数,函数把线程放到等待条件变量的列表上,然后解锁互斥量。休眠等待条件

返回时互斥量再次被锁住。

通知条件满足

1
2
3
4
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond); 
int pthread_cond_broadcast(pthread_cond_t *cond);
										//成功返回0,否则返回错误编号

通知等待条件的线程,signal唤醒至少一个,broadcast唤醒所有(顺序唤醒,因为等待线程返回时要给互斥量重新上锁)

一个栗子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <pthread.h>

struct msg {
  	struct msg *m_next;
    /* ... more stuff here ... */
};

struct msg *workq;

pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void
process_msg(void)
{
	struct msg *mp;
    
	while (1) {
		pthread_mutex_lock(&qlock);
        while (workq == NULL)
            pthread_cond_wait(&qready, &qlock);	//消息队列没有消息就一直等待
        
        mp = workq;
        workq = mp->m_next;	//取消息
        pthread_mutex_unlock(&qlock);
	}
}

void
enqueue_msg(struct msg *mp){
	pthread_mutex_lock(&qlock);
	mp->m_next = workq;
	workq = mp; //发消息
	pthread_mutex_unlock(&qlock);
	pthread_cond_signal(&qready); //通知有新消息
}

8.5.4 自旋锁

自旋锁与互斥量类似,但他不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。

自旋锁可用于以下情况:锁被持有的时间段,而且线程并不希望在重新调度上花费太多成本

自旋锁通常作为底层原语用于实现其他类型的锁

有些互斥锁实现很高效,在获取锁时先自旋一段时间,等时间到达某一阀值才会休眠。

1
2
3
4
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
										//成功返回0,否则返回错误编号

只有一个属性,pshared取PTHREAD_PROCESS_SHARED则自旋锁能被可以访问锁底层内存的线程锁获取,即便那些线程属于不同进程

PTHREAD_PROCESS_PRIVATE则自旋锁只能被初始化该锁的进程内部的线程所访问

使用

1
2
3
4
5
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
										//成功返回0,否则返回错误编号

8.5.5 屏障

屏障(barrier)是协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合并线程都到达某一点,然后从该点继续执行。

可以看到,pthread_join函数就是一种屏障,线程等待另一线程结束

但屏障的概念更加广泛,其允许任意数量的线程等待,直到所有线程完成处理工作阻塞在预先设置的点(也就是调用pthread_barrier_wait函数的地方),线程不需要退出。所有线程到达barrier后可以接着工作。

初始化

1
2
3
4
5
6
#include<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
                         const pthread_barrierattr_ty *restrict attr,
                         unsigned int count); 
int pthread_barrier_destroy(pthread_barrier_t *barrier);
										//成功返回0,否则返回错误编号

初始化屏障时,可以使用count参数指定,在所有线程继续运行之前,必须达到屏障的线程数目。

使用attr参数设置属性,默认可以用NULL

等待其他线程

1
2
3
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
						//成功返回0或PTHREAD_BARRIER_SERIAL_THREAD,失败返回错误编码

调用此函数的线程在之前设置的屏障计数(count)未满足时,进入休眠状态,

如果是最后一个调用此函数的线程,就满足了屏障计数,所有线程都会被唤醒。

对于一个任意线程,pthread_barrier_wait返回PTHREAD_BARRIER_SERIAL_THREAD

剩下的其他线程看到的返回值是0,这使得一个线程可看作为“主线程”(不是执行main那个主线程),可以工作在其他所有线程已完成的工作结果上

一旦达到屏障计数值,屏障就可以被重用

如果要改变屏障计数,则需要destroy之后再init

不一定要用PTHREAD_BARRIER_SERIAL_THREAD来决定用哪个线程做剩余工作,

可以让主线程(main那个线程)一起pthread_barrier_wait,让主线程 做 ”主线程“(从等待中返回一定是所有任务都完成了)

8.5.6 pthread_once

第一次是在在陈硕书中见到

一次性初始化

有时候我们需要对一些posix变量只进行一次初始化,如线程键。如果我们进行多次初始化程序就会出现错误。

在传统的顺序编程中,一次性初始化经常通过使用布尔变量来管理。控制变量被静态初始化为0,而任何依赖于初始化的代码都能测试该变量。如果变量值仍然为0,则它能实行初始化,然后将变量置为1。以后检查的代码将跳过初始化。

但是在多线程程序设计中,事情就变的复杂的多。如果多个线程并发地执行初始化序列代码,可能有2个线程发现控制变量为0,并且都实行初始化,而该过程本该仅仅执行一次。

如果我们需要对一个posix变量静态的初始化,可使用的方法是用一个互斥量对该变量的初始话进行控制。但有时候我们需要对该变量进行动态初始化,pthread_once就会方便的多。

函数原形:

1
2
3
pthread_once_t once_control = PTHREAD_ONCE_INIT;

int pthread_once(pthread_once_t *once_control,void(*init_routine)(void));

参数:

once_control 控制变量

init_routine 初始化函数

返回值:

若成功返回0,若失败返回错误编号。

类型为pthread_once_t的变量是一个控制变量。控制变量必须使用PTHREAD_ONCE_INIT宏静态地初始化。

pthread_once函数首先检查控制变量,判断是否已经完成初始化,如果完成就简单地返回;否则,pthread_once调用初始化函数,并且记录下初始化被完成。如果在一个线程初始时,另外的线程调用pthread_once,则调用线程等待,直到那个现成完成初始话返回。

9 网络IPC:套接字

9.1 套接字描述符socket

套接字是通信端点的抽象。类UNIX系统中套接字被当做一种文件描述符

许多处理文件的函数(如read/wirte)也可以用于处理套接字描述符。

  • 套接字的创建
1
2
3
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
							//成功返回文件(套接字)描述符,出错返回-1

参数domain(域)确定通信的特性,包括地址格式

下面总结了POSIX.1指定的域,都已AF_开头,意为地址族(address family)

1
2
3
4
AF_INET		# IPv4因特网域
AF_INET6	# IPv6因特网域
AF_UNIX		# UNIX域
AF_UPSPEC	# 未指定

参数type确定套接字的类型,进一步确定通信特征,下面是POSIX.1定义的类型,在实现中可以自由增加其他类型的支持

1
2
3
4
SOCK_DGRAM		# 固定长度的,无连接的,不可靠的报文传递
SOCK_STREAM		# 有序的,可靠地,双向的,面向连接的字节流
SOCK_RAW		# IP协议的数据报接口(在POSIX.1中为可选)
SOCK_SEQPACKET	# 固定长度的,有序的,可靠的,面向连接的报文传递

参数protocol通常是0,表示为给定的域和套接字类型选择默认协议。

AF_INETSOCK_STREAM默认TCPSOCK_DGRAM默认UDP

1
2
3
4
5
6
IPPROTO_IP		# IPv4网际协议
IPPROTO_IPV6	# IPv6网际协议(在POSIX.1中为可选)	
IPPROTO_ICMP	# 因特网控制报文协议
IPPROTO_RAW		# 原始ip数据包协议(在POSIX.1中为可选)
IPPROTO_TCP		# 传输控制协议
IPPROTO_UDP		# 用户数据报协议
  • 套接字通信是双向的,可以用shutdown函数来禁止一个套接字的I/O
1
2
3
#include <sys/socket.h>
int shutdown(int sockfd, int how);	
					//成功返回0,出错返回-1

参数how:

1
2
3
SHUT_RD		# 关闭读端
SHUT_WR		# 关闭写端
SHUT_RDWR	# 同时关闭读端写端

能够关闭(close)一个套接字,为何还要使用shutdown。

只有最后一个活动引用关闭时,close才释放网络端点。这意味着复制一个套接字(如dup),要直到关闭了最后 一个引用它的文件描述符才会释放这个套接字。而shutdown允许一个套接字处于不活动状态,和引用它的文件描述符数目无关。其次可以关闭套接字双向传输的一个方向。

9.2 寻址

9.2.1 字节序

字节序分为大端字节序小端字节序 大端字节序: 是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。 小端字节序: 是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。 x86框架采用小端字节序,ARM架构启动时可以 设置大小端字节序。 网络字节序采用大端字节序。 字节序的转换:

1
2
3
4
5
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h表示host,n表示network,l表示32位长整数,s表示16位短整数。

9.2.2 地址格式与IP转换

很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

在linux中的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <sys/socket.h>
struct sockaddr {
sa_family_t sa_family; /* 地址族 */    
char sa_data[14]; /*地址值,实际可能更长*/    
};
/*IPV4*/
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* AF_INET4 */    
__be16 sin_port; /*端头号*/    
struct in_addr sin_addr; /* IPv4 地址 */  
/* 8字节填充 */    
unsigned char __pad[__SOCK_SIZE__  sizeof(short int)     
sizeof(unsigned short int)  sizeof(struct in_addr)];    
};
struct in_addr {
__be32 s_addr;    
};
/*IPV6*/
struct sockaddr_in6 {
unsigned short int sin6_family; /*AF_INET6*/    
__be16 sin6_port; /* 端口 # */    
__be32 sin6_flowinfo; /* IPv6 flow information */    
struct in6_addr sin6_addr; /* IPv6 地址 128bit*/    
__u32 sin6_scope_id; /* scope id (new in RFC2553) */    
};
struct in6_addr {
union {
__u8 u6_addr8[16];    
__be16 u6_addr16[8];    
__be32 u6_addr32[4];    
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
/*本地socket通信*/
struct sockaddr_un {
__kernel_sa_family_t sun_family; /* AF_UNIX */    
char sun_path[UNIX_PATH_MAX]; /* pathname */    
};

ipv4以及的ipv6点分十进制表达法与二进制整数之间的互转。

1
2
3
4
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
							//成功返回1,格式无效返回0,出错返回-1

inet_pton把字符串src转换成ip地址保存在 dst 中。该函数调用成功返回大于0的整数。

inet_ntop() 把网络字节序的ip地址 src 转换成字符串保存在 dst 中作为返回值返回,参数 sizedst所包含的字节数。

af 用来指定要转换的ip属于什么协议族

栗子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
int main(int argc, char** argv)
{
   struct sockaddr_in sin;  
   char buf[16];    
   memset(&sin, 0, sizeof(sin));    
   sin.sin_family=AF_INET;  
   sin.sin_port=htons(3001);  
   inet_pton(AF_INET, "192.168.1.111", &sin.sin_addr.s_addr);  
   printf("%s\n", inet_ntop(AF_INET, &sin.sin_addr, buf, sizeof(buf)));  
   return 0;
}

9.2.3 地址查询

gethostbyname和gethostbyaddr被认为是过时的。

gethostbyname, gethostbyaddr是不可重入函数;已经被getaddrinfo, getnameinfo替代

1
2
3
4
5
6
7
8
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host,
               	const char *service,
               	const struct addrinfo *hint,
               	struct addrinfo	**res);
						//成功返回0,出错返回非0错误码
void freeaddrinfo(struct addrinfo *ai);

hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串) service:服务名可以是十进制的端口号(字符串),也可以是已定义的服务名称,如ftp、http等 hints:可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。 result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。必须用freeaddrinfo释放

hostname和service不能同时为NULL

出错不能使用perror或strerror来生成错误信息,而是调用gai_strerror将返回的错误码转换成错误消息

1
2
#include <netdb.h>
const char *gai_strerror(int error);	//返回错误信息字符串

addrinfo结构体定义至少包含以下成员

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct addrinfo {
    int				ai_flags;
    int 			ai_family; 		/* AF_INET,AF_INET6或者AF_UNSPEC */
    int				ai_socktype;	/* SOCK_STREAM SOCK_DGRAM ... */
    int				ai_protocol;	/* 一般填0 使用默认 */
    socklen_t 		ai_addrlen;		/* length in bytes of address*/
    struct sockaddr	*ai_addr;		/* address */
    char			*ai_canonname;	/* canonical name of host */
    struct addrinfo	*ai_next;		/* next in list */
};

可以提供一个可选的hint来选择符合条件的地址,包括ai_family,ai_flags,ai_protocol,ai_socktype,其余字段必须为0,ai_flags可以用

1
2
3
4
5
6
7
AI_ADDRCONFIG	#查询配置的地址类型(ipv4或ipv6)
AI_ALL			#查找IPv4和IPv6地址(仅用于AI_V4MAPPED)
AI_CANONNAME	#需要一个规范的名字(与别名相对)
AI_NUMERICHOST	#以数字格式指定主机地址,不翻译
AI_NUMERICSERV	#将服务指定为数字端口号,不翻译
AI_PASSIVE		#套接字地址用于监听绑定
AI_V4MAPPED		#如果没有找到IPv6地址,返回映射到IPv6格式的IPv4地址

getnameinfo函数将一个地址转换成一个主机名和一个服务名

1
2
3
4
5
6
7
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *addr, socklen_t alen,
               	char *host, socklen_t hostlen,
               	char *service, socklen_t servlen,
               	int flags);
								//成功返回0,出错返回非0错误码,用gai_strerror打印

输入addr ,主机名和服务返回到host和service中(不关心填NULL)

getaddrinfo栗子: 输入网址,输出ip

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    if (argc != 2) {
        printf("Usag...%s abc.com\n",argv[0]);
        exit(1);
    }
    struct addrinfo hints;
    struct addrinfo *res, *cur;
    int ret;
    struct sockaddr_in *addr;
    char ipbuf[16];

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET; /* Allow IPv4 */
    hints.ai_flags = AI_PASSIVE; /* For wildcard IP address */
    hints.ai_protocol = 0; /* Any protocol */
    hints.ai_socktype = SOCK_STREAM;
       
    ret = getaddrinfo(argv[1], NULL,&hints,&res);
    
    if (ret != 0) {
        printf("getaddrinfo:%s\n",gai_strerror(ret));
        exit(1);
    }
    for (cur = res; cur != NULL; cur = cur->ai_next) {
        addr = (struct sockaddr_in *)cur->ai_addr;
        printf("%s\n",inet_ntop(AF_INET, 
            &addr->sin_addr, ipbuf, 16));
    }
    freeaddrinfo(res);
    exit(0);
}

9.3 将套接字与地址关联bind

将一个客户端的套接字关联上一个地址没有多大意义,可以让系统选一个默认地址。

然而对于服务器,需要一个众所周知的地址。

  • bind函数来关联地址和套接字
1
2
3
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
						//成功返回0,出错返回-1

使用的地址的一些限制:

  1. 在进程正在运行的计算机上,指定地址必须有效,必须是本机地址
  2. 地址必须和创建套接字时支持的地址族格式匹配
  3. 地址端口号不小于1024,0-1023只能root权限使用
  4. 一般只能将一个套接字端点绑定到一个给定地址上,尽管有些协议允许多重绑定

对于因特网域,如果指定IP地址为INADDR_ANY(<netinet/in.h>),这意味着这可以接收任意网卡数据

如果调用connect或 listen,但没有将地址绑定到套接字上,系统会自动分配地址。

  • getsockname函数来发现绑定到套接字上的地址
1
2
3
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);
								//成功返回0,出错返回-1

addr和alenp既做传入参数,也做传出参数。

  • getpeername函数来找到对方的地址
1
2
3
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp)
    								//成功返回0,出错返回-1

除了返回的是对方的地址,其他和getsockname一样

9.4 建立连接connect

如果要处理一个面向连接的网络服务(SOCK_STREAMSOCK_SEQPACKET),那么交换数据前要与服务器(套接字)建立连接。

  • connect
1
2
3
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklent_t len);
    								//成功返回0,出错返回-1

如果sockfd没有绑定地址,connect会给调用者绑定一个默认地址

如果connect失败,可迁移的应用程序需要关闭套接字,如果想重试必须新打开一个套接字

如果套接字描述符处于非阻塞模式,那么连接不能马上建立时connect返回-1,errno设为EINPROGRESS

可以poll或select来判断文件描述符何时可写,如果可写,连接完成

connect还可以用于无连接的网络服务(SOCK_DGRAM),这看起来有点矛盾,实际是个不错的选择,如果SOCK_DGRAM套接字调用connect,传送的报文地址会设置成connect调用指定的地址,这样每次传送报文时就不需要再提供地址,另外仅能接收来自指定地址的报文。

9.5 接受连接请求listen accept

  • listen函数宣告愿意接收连接请求
1
2
3
#include <sys/socket.h>
int listen(int sockfd, int backlog);
    								//成功返回0,出错返回-1

backlog提供了一个提示,提示系统该进所要入队的未完成连接数量。其实际值由系统决定,上限由<sys/socket.h>中的SOMAXCONN指定,队列满后,系统会拒绝多余连接请求。一般SOMAXCONN

一旦服务器调用listen,所用的套接字就能接收连接请求

  • accept函数获得连接请求并建立连接
1
2
3
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *len);
	    								//成功返回套接字描述符,出错返回-1

accept返回的描述符是套接字描述符,这个新的描述符和原始套接字sockfd具有相同的类型和地址族,

传给accept的原始描述符没有关联到这个连接,而是继续保持可以状态并接收其他连接请求

如果不关心客户端标识,可将参数addr和len设为NULL,否则accept会吧客户端地址填充到缓冲区

如果没有连接请求在等待,accept会阻塞直到一个请求到来

如果sockfd处于非阻塞模式,accept会返回-1,errno设置为EAGAIN

9.6 数据传输

可以通过read和write交换数据

但如果想指定选项,从多个客户端接收数据包,或者发送带外数据,需要使用6个专门为数据传递而设计的函数

3个用来发送数据,3个用来接收数据

9.6.1 send sendto sendmsg

  • send,和write很像,但是可以指定标志来改变处理传输数据的方式
1
2
3
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbyges, int flags);
    								//成功返回发送的字节数,出错返回-1

类似write,多个flags

flags:

1
2
3
4
5
6
7
8
MSG_CONFIRM 	# 提供链路层反馈以保持地址映射有效
MSG_DONTROUTE	# 勿将数据包路由出本地网络
MSG_DONTWAIT	# 运行非阻塞操作(等价于使用O_NONBLOCK)
MSG_EOF			# 发送数据后关闭套接字发送端
MSG_EOR			# 如果协议支持,标记记录结束
MSG_MORE		# 延迟发送数据包允许写更多数据
MSG_NOSIGNAL	# 在写无连接的套接字时不产生SIGPIPE信号
MSG_OOB			# 如果协议支持,发送带外数据

对支持报文边界的协议,报文长度唱过协议支持,send失败,errno为EMSGSIZE

  • sendto,和send类似,区别是可以在无连接的套接字上指定一个目标地址
1
2
3
4
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
               const struct sockaddr *destaddr, socklen_t destlen);
    								//成功返回发送的字节数,出错返回-1

对于面向连接的套接字,目标地址destaddr是被忽略的

对于无连接的套接字,除非先调用了connect设置了目标地址,否则不能用send。要有sendto

  • sendmsg 略

9.6.2 recv recvfrom recvmsg

  • recv,和read相似,但是可以指定标志来控制如何接收数据
1
2
3
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
					//返回读到数据的字节长度,若无可以数据或对方已经按序结束,返回0,出错返回-1

flags:

1
2
3
4
5
6
7
MSG_CMSG_CLOEXEC	# 为unix域套接字上接收的文件描述符设置执行时关闭标志
MSG_DONTWAIT		# 启用非阻塞操作(相当于使用O_NONVLOCK)
MSG_ERRQUEUE		# 接收错误信息作为辅助数据
MSG_OOB				# 如果协议支持,获取带外数据
MSG_PEEK			# 返回数据包内容而不真正取走数据包,偷窥
MSG_TRUNC			# 使数据包被截断,也返回数据包的实际长度
MSG_WAITALL			# 等待直到所有的数据可用(仅SOCK_STREAM),buf填满nbytes才返回
  • recvfrom,可以得到数据发送者的源地址
1
2
3
4
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t nbytes, int flags,
                 struct sockaddr *addr, socklen_t *addrlen);
					//返回读到数据的字节长度,若无可以数据或对方已经按序结束,返回0,出错返回-1

flags与recv一样,addr不为NULL,会填充另一端的地址

  • recvmsg 略

9.7 套接字选项

命令行命令

  • od(1)命令观察该文件的实际内容,-c参数表示以字符方式打印内容,括号里数字是查看man页码
  • size(1)命令报告正文段,数据段和bss段长度