文件与I/O函数
- C标准I/O库函数与Unbuffered I/O函数
- C标准I/O库函数:fwrite, fgetc, fopen, ...
- 对于C标准I/O库来说,打开的文件由FILE *指针标识,而对于内核来说,打开的文件由文件描述符标识,文件描述符从open系统调用获得,在使用read、write、close系统调用时都需要传文件描述符
- open、read、write、close等系统函数称为无缓冲I/O(Unbuffered I/O)函数, 在头文件unistd.h中声明
- 网络编程通常直接调用Unbuffered I/O函数
- 每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的信息,称为进程描述符, task_struct中有一个指针指向files_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开的文件的指针
- 用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符
- open函数与fopen函数的轻微区别:
- 以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
- 以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写
- open函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
// 返回值:成功返回新分配的文件描述符,出错返回-1并设置errno
- 阻塞
- 阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行
- 程序启动时会自动打开三个文件:标准输入、标准输出和标准错误输出。在C标准库中分别用FILE *指针stdin、stdout和stderr表示
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
- 非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
int main(void)
{
char buf[10];
int fd, n, i;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);// 为什么不直接对STDIN_FILENO做非阻塞read? 因为STDIN_FILENO在程序启动时已经被自动打开了,而我们需要在调用open时指定O_NONBLOCK标志
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
for(i=0; i<5; i++) {
n = read(fd, buf, 10);
if(n>=0)
break;
if(errno!=EAGAIN) {
perror("read /dev/tty");
exit(1);
}
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
}
if(i==5)
write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
else
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
- fcntl
- fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件
- 文件描述符和shell重定向
./a.out 5 5<>temp.foo
# Shell在执行a.out时在它的文件描述符5上打开文件temp.foo,并且是可读可写的
# 如果在<、>、>>、<>前面添一个数字,该数字就表示在哪个文件描述符上打开文件,例如2>>temp.foo表示将标准错误输出重定向到文件temp.foo并且以追加方式写入文件,注意2和>>之间不能有空格,否则2就被解释成命令行参数了。文件描述符数字还可以出现在重定向符号右边, 文件描述符数字写在重定向符号右边需要加&号,否则就被解释成文件名了,2>&1其中的>左右两边都不能有空格
# 例如: command > /dev/null 2>&1 标准输出为/dev/null, 2>&1 , 表示把标准错误输出也重定向到标准输出即/dev/null
int flags;
flags = fcntl(STDIN_FILENO, F_GETFL);
flags |= O_NONBLOCK;
if(fcntl(STDIN_FILENO, F_SETFL, flags)==-1) {
perror("fcntl");
exit(1);
}
int val;
val = fcntl(STDIN_FILENO, F_GETFL);
switch(val & O_ACCMODE) {// 用掩码O_ACCMODE取出它的读写位
case O_RDONLY:
break;
case O_WRONLY:
break;
case O_RDWR:
break;
default:
// invalid access mode
}
if (val & O_APPEND)
printf("append");
if (val & O_NONBLOCK)
printf("nonblocking");
VFS 虚拟文件系统

dup和dup2函数
- 复制一个现存的文件描述符, 使两个文件描述符指向同一个file结构体
- 成功返回新分配或指定的文件描述符, 如果出错则返回-1
- dup2可以用newfd指定新描述符的数值
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd,int newfd);
进程
打印环境变量
#include <stdio.h>
int main(void)
{
extern char **environ;
// libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明
int i;
for(i=0; environ[i]!=NULL; i++)
printf("%s\n", environ[i]);
return 0;
}
进程控制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main (int argc , char * argv[]) {
// fork
pid_t pid;
char * message;
int n;
pid = fork();
if ( pid < 0 ) {
perror("fork failed");
exit(1);
}
if ( pid == 0 ) {
message = "This is the child\n";
n = 6;
} else {
message = "This is the parent\n";
n = 3;
}
for (; n > 0; n--) {
printf("%s",message);
sleep(1);
}
return 0;
}
/*
1.fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id
2.fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法
*/
- wait和waitpid函数
- 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程
- 如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了
- 如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它
#include <unistd.h>
#include <stdlib.h>
// 僵尸进程例子, 子进程结束后, 父进程既没有结束没有调用wait或waitpid清理子进程, 子进程此时就是僵尸进程
int main(void)
{
pid_t pid=fork();
if(pid<0) {
perror("fork");
exit(1);
}
if(pid>0) { /* parent */
while(1);
}
/* child */
return 0;
}
- wait和waitpid函数解析
- 若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会以下几种情况
- 阻塞(如果它的所有子进程都还在运行)
- 带子进程的终止信息立即返回(如果一个子进程已终止,等待父进程读取它的终止信息)
- 出错返回-1(没有子进程)
- 区别:
- 如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0
- wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程
- 通过宏定义读取status
- WIFEXITED(stat_val), 子进程正常退出返回非0
- WEXITSTATUS(stat_val), 获得子进程exit()返回的结束代码,一般先要用WIFEXITED判断
- WIFSIGNALED(stat_val), 如果子进程是因为信号而结束则此宏值返回非0
- WTERMSIG(stat_val), 取出的字段值就是信号的编号, 一般先要用WIFSIGNALED判断
- WIFSTOPPED(stat_val), 子进程接收到停止信号时返回非0
- WSTOPSIG(stat_val), 返回导致子进程停止的信号类型, 一般先要用WIFSTOPPED判断
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); // 如果参数status不是空指针,则子进程的终止信息通过这个参数传出
pid_t waitpid(pid_t pid, int *status, int options);
进程间通信(IPC)
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核, 在内核中开辟一块缓冲区, 两个进程在这个缓存区上交换数据, 内核提供的这种机制称为进程间通信
管道(pipe)
- 4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
- 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样
- 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回
- 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止
- 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回
#include <unistd.h>
int pipe(int filedes[2]);
/*
调用pipe函数成功即在内核中开辟一块缓存区, 失败返回-1
通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端
*/
#include <stdlib.h>
#include <unistd.h>
#define MAXLINE 80
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0) {
perror("pipe");
exit(1);
}
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid > 0) { /* parent */
close(fd[0]);// 不使用的读端或写端必须关闭,如果不关闭会有什么问题???
write(fd[1], "hello world\n", 12);
wait(NULL);
} else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
return 0;
}
- 文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标识一个IPC通道
- FIFO(有名管道)
- 有名管道(FIFO)的创建可以使用 mkfifo() 函数,该函数类似文件中的open() 操作,可以指定管道的路径和访问权限 (用户也可以在命令行使用 “mknod <管道名>”来创建有名管道)
- 在创建管道成功以后,就可以使用open()、read() 和 write() 这些函数了。与普通文件一样,对于为读而打开的管道可在 open() 中设置 O_RDONLY,对于为写而打开的管道可在 open() 中设置O_WRONLY。
- 不支持lseek
- Unix Domain Socket
- 几个进程可以在文件系统中读写某个共享文件,也可以通过给文件加锁来实现进程间同步
信号
#include <signal.h>
#include <stdlib.h>
// 成功返回0,错误返回-1
int kill(pid_t pid, int signo);
int raise(int signo);
void abort(void);
- 由软件条件产生信号 alarm函数和SIGALRM信号
- 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
阻塞信号
#include <signal.h>
int sigemptyset(sigset_t *set); // 初始化set所指向的信号集, 使其中所有信号的对应bit清零
int sigfillset(sigset_t *set);// 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位
int sigaddset(sigset_t *set, int signo);// 在该信号集中添加某种有效信号
int sigdelset(sigset_t *set, int signo);// 在该信号集中删除某种有效信号
int sigismember(const sigset_t *set, int signo);// 判断一个信号集的有效信号中是否包含某种信号
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);// 读取或更改进程的信号屏蔽字
int sigpending(sigset_t *set);// 读取当前进程的未决信号集,通过set参数传出
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void printsigset(const sigset_t *set) {
int i;
for (i = 1;i < 32;i++) {
// 判断一个信号集的有效信号中是否包含某种信号
if (sigismember(set,i) == 1) {
// putchar('1');
write(STDOUT_FILENO,"1",1);
} else {
// putchar('0');
write(STDOUT_FILENO,"0",1);
}
}
puts("");
}
int main(void) {
sigset_t s,p;// 信号集
sigemptyset(&s); // sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零
sigaddset(&s,SIGINT);// 向信号集中添加某种有效信号
sigprocmask(SIG_BLOCK,&s,NULL);// 把set中的信号添加到信号屏蔽字(阻塞信号集)
while (1) {
sigpending(&p); // 读取当前进程的未决信号集
printsigset(&p);
sleep(1);
}
return 0;
}
信号捕捉
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
/*
act, oact指向的sigaction结构体
struct sigaction {
void (*sa_handler)(int); /* addr of signal handler, */
/* or SIG_IGN, or SIG_DFL */
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options, Figure 10.16 */
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *);
};
*/
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm(int signo)
{
/* nothing to do */
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
unsigned int unslept;
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
alarm(nsecs);
pause();
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
return unslept;
}
int main(void)
{
while(1){
mysleep(2);
printf("Two seconds passed\n");
}
return 0;
}
终端
每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件
通过Ctrl-Alt-F1~Ctrl-Alt-F6切换到6个字符终端,相当于有6套虚拟的终端设备,它们共用同一套物理终端设备,对应的设备文件分别是/dev/tty1~/dev/tty6,所以称为虚拟终端
内核中处理终端设备的模块包括硬件驱动程序和线路规程
- 硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理
伪终端(Pseudo TTY):一套伪终端由一个主设备(PTY Master)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程; 从设备和/dev/tty1这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件而是访问主设备
作业控制:
- 个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制
- Session与进程组
- stty tostop:stty命令设置终端选项,禁止后台进程写,然后启动一个后台进程准备往终端写,这时进程收到一个SIGTTOU信号,默认处理动作也是停止进程
守护进程
// 创建守护进程, daemonize(3)函数同样实现
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
void daemonize(void)
{
pid_t pid;
/*
* Become a session leader to lose controlling TTY.
*/
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
} else if (pid != 0) /* parent */
exit(0);
setsid();// 关键!!
/*
* Change the current working directory to the root.
*/
if (chdir("/") < 0) {
perror("chdir");
exit(1);
}
/*
* Attach file descriptors 0, 1, and 2 to /dev/null.
*/
close(0);
open("/dev/null", O_RDWR);
dup2(0, 1);
dup2(0, 2);
}
int main(void)
{
daemonize();
while(1);
}
线程
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);
// 成功返回0, 失败返回错误号
// 在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定,start_routine返回时,这个线程就退出了
// pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元
// start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *
// 其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
(unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg)
{
printids(arg);
return NULL;
}
int main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
if (err != 0) {
fprintf(stderr, "can't create thread: %s\n", strerror(err));// 由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印
exit(1);
}
printids("main thread:");
sleep(1);
// 如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒
return 0;
}
- 终止线程:
- 线程函数中return(主线程不行,相当于调用了exit)
- 一个线程可以调用pthread_cancel终止同一进程中的另一个线程
- 线程可以调用pthread_exit终止自己
#include <pthread.h>
void pthread_exit(void *value_ptr);
// value_ptr是void *类型,其它线程可以调用pthread_join获得这个指针
// pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
// 调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下
// 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
// 如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
// 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void * thr_fn1(void * arg) {
printf("thread 1 returning\n");
return (void *)1;// 线程结束方式1:非主线程返回
}
void * thr_fn2(void * arg) {
printf("thread 2 exiting\n");
pthread_exit((void *)2); // 线程结束方式2:pthread_exit
}
void * thr_fn3(void * arg) {
while (1) {
printf("thread 3 running\n");// 线程结束方式3:pthread_cancel
sleep(1);
}
}
int main(void) {
pthread_t tid;
void *tret;
pthread_create(&tid,NULL,thr_fn1,NULL);
pthread_join(tid,&tret);
printf("thread 1 exit code %d\n",(int)tret);
pthread_create(&tid,NULL,thr_fn2,NULL);
pthread_join(tid,&tret);
printf("thread 2 exit code %d\n",(int)tret);
pthread_create(&tid,NULL,thr_fn3,NULL);
pthread_cancel(tid);
// 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止
// 但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态
// 调用pthread_detach之后不能调用pthread_join,反之亦然
pthread_join(tid,&tret);
printf("thread 3 cancel code %d\n",(int)tret);
return 0;
}
线程间同步
- 对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock)
- Mutex用pthread_mutex_t类型的变量表示
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex); // 销毁锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr); // 初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL
#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);// 释放锁
- Condition Variable:阻塞等待条件
- 创建和销毁:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);// 阻塞直到条件成立
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);// 阻塞直到条件成立或超时
int pthread_cond_broadcast(pthread_cond_t *cond);// 唤醒所有等待的进程
int pthread_cond_signal(pthread_cond_t *cond);// 唤醒某个等待的进程
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
// 生产者-消费者的例子,生产者生产一个结构体串在链表的表头上,消费者从表头取走结构体
struct msg {
struct msg * next;
int num;
};
struct msg * head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void * consumer (void * arg) {
struct msg * mp;
while (1) {
pthread_mutex_lock(&lock);
while (head == NULL) {
pthread_cond_wait(&has_product,&lock);
}
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n",mp->num);
free(mp);
sleep(rand() % 5);
}
}
void * producer (void * arg) {
struct msg * mp;
while (1) {
mp = malloc(sizeof(struct msg));
pthread_mutex_lock(&lock);
mp->next = head;
mp->num = rand() % 1000;
head = mp;
printf("Product %d\n",mp->num);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
int main (int argc,char * argv[]) {
pthread_t pid,cid;
srand(time(NULL));
pthread_create(&pid,NULL,producer,NULL);
pthread_create(&cid,NULL,consumer,NULL);
pthread_join(pid,NULL);
pthread_join(cid,NULL);
return 0;
}
~
- Semaphore信号量
- 详见sem_overview(7),这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
// semaphore变量的类型为sem_t, pshared参数为0表示信号量用于同一进程的线程间同步, value值表示可以资源的个数
int sem_wait(sem_t *sem);
// 调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
// sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程
int sem_destroy(sem_t * sem);
// 用完semaphore变量之后应该调用sem_destroy()释放与semaphore相关的资源
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];
sem_t blank_number,product_number;
void * producter (void * arg) {
static int p = 0;
while (1) {
sem_wait(&blank_number);// 5个空位置, 用完就阻塞
queue[p] = rand() % 1000;
printf("Produce %d \n",queue[p]);
p = (p+1) % NUM;
sleep(rand()%5);
sem_post(&product_number);
}
}
void * consumer (void * arg) {
static int c = 0;
while (1) {
sem_wait(&product_number);
printf("Consume %d\n",queue[c]);
c = (c+1) % NUM;
sleep(rand()%5);
sem_post(&blank_number);// 腾出空位置
}
}
int main (int argc,char * argv[]) {
pthread_t pid,cid;
sem_init(&blank_number,0,NUM);
sem_init(&product_number,0,0);
pthread_create(&pid,NULL,producter,NULL);
pthread_create(&cid,NULL,consumer,NULL);
pthread_join(pid,NULL);
pthread_join(cid,NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
TCP/IP
TCP/IP数据包的封装

以太网帧格式

ARP数据报格式

IP数据报格式

UDP段格式

TCP段格式

TCP连接建立断开

- TCP建立链接过程:
- 客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
- 服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
- 客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。
- 数据传输过程:
- 客户端发出段4,包含从序号1001开始的20个字节数据。
- 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。
- 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
- 关闭链接过程:
- 客户端发出段7,FIN位表示关闭连接的请求。
- 服务器发出段8,应答客户端的关闭连接请求。
- 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
- 客户端发出段10,应答服务器的关闭连接请求。
Socket编程
预备知识
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 比如发送数据1000(0x3e8):先发0x03,再发0xe8
- 如果发送主机是小端字节序, 把1000填到发送缓冲区之前需要做字节序的转换
- 如果接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换
- 相关字节序转换的函数:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);// 将32位的长整数从主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- sockaddr数据结构(结构体):

- sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换
// 字符串转in_addr的函数
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);
// in_addr转字符串的函数:
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
// 例子:
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char * argv[]) {
struct in_addr addr;
if ( argc != 2 ) {
fprintf(stderr, "%s <dotted-address>\n", argv[0]);
exit(EXIT_FAILURE);
}
if (inet_aton(argv[1],&addr) == 0) {
perror("inet_aton");
exit(EXIT_FAILURE);
}
printf("%s\n",inet_ntoa(addr));
exit(EXIT_SUCCESS);
}
基于TCP协议的网络程序
- TCP协议通讯流程

- socket简单通信例子:
// server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main () {
struct sockaddr_in servaddr,cliaddr;// socket ipv4 地址结构体
socklen_t cliaddr_len; // 客户端地址结构体长度
char buf[MAXLINE];// 数据
char str[INET_ADDRSTRLEN];// ip地址字符串
int listenfd,connfd;// 文件描述符
int i,n;
listenfd = socket(AF_INET,SOCK_STREAM,0);// socket()打开一个网络通讯端口
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;// ipv4网络协议
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// ip地址字节序转换,INADDR_ANY表示0.0.0.0
servaddr.sin_port = htonl(SERV_PORT);// 端口字节序转换
bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));// 将参数sockfd和myaddr绑定在一起
listen(listenfd,20);// listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);// cliaddr传出参数, cliaddr_len传入传出参数
n = read(connfd,buf,MAXLINE);
printf("receivd from %s at PORT %d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++) {
buf[i] = toupper(buf[i]);
}
write(connfd,buf,n);
close(connfd);
}
}
// client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
char *str;
if (argc != 2) {
fputs("usage: ./client message\n", stderr);
exit(1);
}
str = argv[1];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));
n = read(sockfd, buf, MAXLINE);// 调用read从网络读就会阻塞
printf("Response from server:\n");
write(STDOUT_FILENO, buf, n);// 写常规文件是不会阻塞的,而向终端设备或网络写则不一定
close(sockfd);
return 0;
}
select
- select是网络程序中很常用的一个系统调用,它可以同时监听多个阻塞的文件描述符(例如多个网络连接),哪个有数据到达就处理哪个,这样,不需要fork和多进程就可以实现并发服务的server