文件IO//

240212 IO进程

大纲
IO: input、output
标准IO
文件IO
文件属性获取
目录操作

进程:process
进程基础
线程(thread)、同步、互斥、条件变量
进程间通信:6种(一共有7种)
无名管道(pipe)、有名管道(fifo)、信号(signal)、共享内存(shared memory)、信号灯集(semphore set)、消息队列(message queue)

标准IO
1.什么是标准IO
1.1 概念
标准IO:是在C库中定义的一组专门用于输入输出的函数。

1.2 特点
(1)通过缓冲机制减少系统调用,提高效率。
系统调用:内核向上提供的一组接口
例如:从硬盘读1KB文件里的数据,每次只读1B

(2)围绕流进行操作,流用FILE*来描述。

(3)标准IO默认打开三个流,stdin(标准输入)、stdout(标准输出)、stderr(标准错误)

注意: vi用ctags索引使用:
1)vi -t 查找名称
输入前面序号,回车。
2)继续追踪:
将光标定位到要追踪的内容,ctrl+]
回退:ctrl+t
3)跳转到上次位置:ctrl+o
跳转到下次位置:ctrl+i

1.3 操作
开打文件:fopen
关闭文件:fclose
读写操作:fgetc、fputc、fgets、fputs、fread、fwrite
其他操作: freopen、rewind、fseek

2.缓存区
(1)全缓存:和文件相关的
(2)行缓存: 和终端相关的
刷新缓存的条件:
●\n
●程序正常退出
●缓存区满
●强制刷新:fflush
#include <stdio.h>

int main(int argc, char const *argv[])
{
printf(“hello world”);
fflush(NULL); //强制刷新缓存区
// printf(“hello world\n”); //\n不光换行还可以刷新刷新缓存区,将缓存区内容刷新到终端。

while(1);
return 0;

}

(3)不缓存:没有缓存,标准错误。

综上:当我们每次要打印数据时,并不是将数据直接发送给标准输出设备,也就是并直接发送给显示器,而是将要打印的数据先存放到缓存区,当缓冲存数据满时,或者遇到\n,或者程序结束时,或者手动刷新缓存区时,缓冲区才会把数据传输到标准输出设备中,也就是显示器中进行输出。

练习:计算标准输出缓存区的大小 KB
方法一:利用循环打印展示

计算得出结果:1KB

方法二: 利用结构体指针stdout
#include <stdio.h>

int main(int argc, char const *argv[])
{
printf(“buf:”);
printf(“%d\n”,stdout->_IO_buf_end -stdout->_IO_buf_base );
return 0;
}

得到:1024B

3.函数接口
3.1 打开文件 fopen
man 3 fopen
FILE *fopen(const char *path, const char *mode);
功能:打开文件
参数:
path:打开的文件路径
mode:打开的方式
r:只读,当文件不存在时报错,文件流定位到文件开头
r+:可读可写,当文件不存在时报错,文件流定位到文件开头
w:只写,文件不存在创建,存在则清空
w+:可读可写,文件不存在创建,存在则清空
a:追加(在末尾写),文件不存在创建,存在追加,文件流定位到文件末尾
a+:读和追加,文件不存在创建,存在追加,读文件流定位到文件开头,写文件流定位到文件末尾
注:当a+的方式打开文件时,写只能在末尾进行追加,定位操作是无法改变写的位置,但是可以改变读的位置
(当a+的方式打开文件时,写如果进行了写操作,读的位置会回到开头)

返回值:
成功:文件流
失败:NULL,并且会设置错误码

3.2 关闭文件
int fclose(FILE* stream);
功能:关闭文件
参数:stream:文件流

#include <stdio.h>

int main(int argc, char const *argv[])
{
FILE *fp;
//打开文件
//fp = fopen(“test.txt”, “r”);
fp = fopen(“test.txt”, “w”);
if (NULL == fp)
{
perror(“fopen err”);
// while(1); //可以验证标准错误不缓存,错误内容直接显示到终端了
return -1;
}
printf(“fopen success\n”);

//关闭文件
fclose(fp);
return 0;

}

3.3 文件读写操作
3.1.1 每次读写一个字符: fgetc()、fputc()
每次读一个字符fgetc()
int fgetc(FILE * stream);
功能:从文件中读取一个字符,并将当前文件指针位置向后移动一个字符。
参数:stream:文件流
返回值:成功:读到的字符
失败或读到文件末尾:EOF(-1)

每次写一个字符fputc()
int fputc(int c, FILE * stream);
功能:向文件中写入一个字符, 成功写入后文件指针会自动向后移动一个字节位置。
参数:c:要写的字符
stream:文件流
返回值:成功:写的字符的ASCII
失败:EOF(-1)

a.针对文件
#include <stdio.h>

int main(int argc, char const *argv[])
{
FILE *fp;
//打开文件
//fp = fopen(“test.txt”, “r”);
fp = fopen(“test.txt”, “r+”);
if (NULL == fp)
{
perror(“fopen err”);
// while(1); //可以验证标准错误不缓存,错误内容直接显示到终端了
return -1;
}
printf(“fopen success\n”);

//针对文件读写操作
char ch = fgetc(fp);
printf("%c %d\n", ch, ch); //h 104

ch = fgetc(fp);
printf("%c %d\n", ch, ch); //i 105

ch = fgetc(fp);
printf("%c %d\n", ch, ch); //97 a

fputc('a', fp); //向文件写字符a
fputc(98, fp);  //向文件写字符b

ch = fgetc(fp);
printf("%c %d\n", ch, ch); //EOF -1

//关闭文件
fclose(fp);
return 0;

}

注意:写操作看文件打开方式是否可写

b.针对终端
#include <stdio.h>

int main(int argc, char const *argv[])
{
char ch = fgetc(stdin);
printf(“%c %d\n”, ch, ch);

ch = fgetc(stdin);
printf("%c %d\n", ch, ch);

fputc('h',stdout);
fputc(10,stdout);
return 0;

}

补充:feof和ferror
int feof(FILE * stream);
功能:判断文件有没有到结尾,也就是当前所在位置后面还有没有字符。
返回:如果到达文件末尾,则为真,如果没到末尾则为假。

接着上面的例子:
#include <stdio.h>

int main(int argc, char const *argv[])
{
FILE *fp;
//打开文件
//fp = fopen(“test.txt”, “r”);
fp = fopen(“test.txt”, “r+”);
if (NULL == fp)
{
perror(“fopen err”);
return -1;
}
printf(“fopen success\n”);

//针对文件读写操作
char ch = fgetc(fp);
printf("%c %d\n", ch, ch); //h 104

ch = fgetc(fp);
printf("%c %d\n", ch, ch); //i 105

ch = fgetc(fp);
printf("%c %d\n", ch, ch); //97 a

fputc('a', fp); //向文件写字符a
fputc(98, fp);  //向文件写字符b

if(feof(fp))
{
    printf("end1\n");  //打印不出来,因为后面还有个EOF。
    return -1;
}

ch = fgetc(fp);
printf("%c %d\n", ch, ch); //EOF -1

if(feof(fp))
{
    printf("end2\n");  //EOF已经被读出所以到末尾了,判断成功。
    return -1;
}

//关闭文件
fclose(fp);
return 0;

}

int ferror(FILE * stream);
功能:检测文件有没有出错
返回:文件出错,返回非零值。如果没有出错,返回0。

练习:实现cat 文件名:查看文件内容,显示到终端。(用命令行参数的方式)
步骤:1.打开文件
2.循环用fgetc获取文件内容
3.当读到文件末尾标志EOF时结束
4.将读取文件内容用fputc打印到终端
5.关闭文件
#include <stdio.h>
int main(int argc, char const *argv[]) //指针数组
{
FILE *fp;
char ch;
if (argc != 2)
{
printf(“format:%s \n”, argv[0]);
return -1;
}

//1.打开文件
fp = fopen(argv[1], "r+");
if (NULL == fp)
{
    perror("fopen err");
    return -1;
}
printf("open success\n");
//2.循环用fgetc读文件内容,只要读到就打印到终端
//3.当到末尾标志EOF时结束

// while ((ch = fgetc(fp)) != EOF)
// {
//     //4.将读到的文件内容用fputc打印到终端
//     fputc(ch, stdout);
// }

while (1)
{
    ch = fgetc(fp);
    if (feof(fp))
        break;
    fputc(ch, stdout);
}

//5.关闭文件
fclose(fp);
return 0;

}

3.1.2 每次一个字符串的读写 fgets()和fputs()
char * fgets(char *s, int size, FILE * stream);
功能:从文件中每次读取一行字符串
参数: s:存放字符串的地址
size:期望一次读取的字符个数
stream:文件流
返回值:成功:s的地址
失败或读到文件末尾:NULL
特性: 每次实际读取的字符个数为size-1个,会在末尾自动添加\0
每次读一行,遇到\n或者到达文件末尾后不再继续读下一行
并把它存储在s所指向的字符串内。

int fputs(const char *s, FILE * stream);
功能:向文件中写字符串
参数:s:要写的内容
stream:文件流
返回值:成功:非负整数
失败:EOF

a. 针对终端
#include <stdio.h>

int main(int argc, char const *argv[]) //指针数组
{
char buf[32]=“”;
//从终端读字符串
fgets(buf,32,stdin); //从终端读到的内容为:hello\n
printf(“%s\n”,buf); //此时buf里面的内容为:hello\n\0

//往终端输出
fputs(buf,stdout);
fputs("world",stdout);
return 0;

}

注意:一定会留一个位置给\0

b. 针对文件

#include <stdio.h>

int main(int argc, char const *argv[]) //指针数组
{
char buf[32] = “”;
//打开文件
FILE *fp = fopen(“test.txt”, “r+”);
if (NULL == fp)
{
perror(“fopen err”);
return -1;
}

//读写操作
fgets(buf, 32, fp); //buf: hello\n\0
fputs(buf, stdout);

fgets(buf, 32, fp); //buf: world\n\0
fputs(buf, stdout);

fgets(buf, 32, fp); //buf:6\0rld\n\0
fputs(buf, stdout);

fgets(buf, 32, fp); //buf:6\0rld\n\0
fputs(buf, stdout);

//关闭文件
fclose(fp);

return 0;

}

注意:如果到达末尾fgets则返回NULL,继续输出则是上一次buf中保留的值因为buf没更新。

练习:通过fgets实现"wc -l 文件名"命令功能(计算文件行数)
思路:打开文件,用fgets循环读文件,读一行n++
#include <stdio.h>
#include <string.h>

int main(int argc, char const *argv[])
{
FILE *fp;
char buf[32] = “”;
int n = 0;
if (argc != 2)
{
printf(“format: %s \n”, argv[0]);
return -1;
}

fp = fopen(argv[1], "r");
if (fp == NULL)
{
    perror("fopen err");
    return -1;
}

//循环用fgets读,只要读到内容也就是没到末尾就进循环
while (fgets(buf, 32, fp) != NULL)
{
    //判断是否有\n,如果有就n++
    if (buf[strlen(buf) - 1] == '\n')
        n++;
}

printf("%d %s\n", n, argv[1]); //因为最后一行最后没有换行,对标wc -l

return 0;

}

练习:使用C语言编写一段程序,实现从1开始以每秒累加1的方式向文件中写入数字,写到100后停止。要求代码格式规范,输出结果清晰易懂。(北京凝思软件股份有限公司笔试题)
#include <unistd.h>
sleep(1);
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
FILE *fp;
char buf[32] = “”;
int n = 0;
if (argc != 2)
{
printf(“format: %s \n”, argv[0]);
return -1;
}

fp = fopen(argv[1], "a+");
if (fp == NULL)
{
    perror("fopen err");
    return -1;
}

//计算文件行数
while (fgets(buf, 32, fp) != NULL)
{
    if (buf[strlen(buf) - 1] == '\n')
        n++;
}

//每隔一秒往文件里写一个数字然后换行
while (1)
{
    fprintf(fp, "%d\n", ++n);
    fflush(NULL);
    if (n == 100)
        break;
    sleep(1);
}

return 0;

}

3.1.3 二进制读写fread()和fwrite()
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:从文件流读取多个元素(将二进制数据从文件读出)
参数: ptr :是一个指针,是存放数据的存储空间的起始地址,用来存放读取元素
size :元素大小 sizeof(元素数据类型)
nmemb :读取元素的个数
stream :要读取的文件流
返回值:成功:读取的元素的个数
读到文件尾或失败: 0

size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);
功能:将二进制数据写入文件
参数: ptr :是一个指针,保存要输出数据的空间的地址。
size :要写入的字节数 sizeof(数据类型)
nmemb : 要进行写入元素的个数
strem: 目标文件流指针
返回值:成功:写的元素个数
失败 :-1

a. 针对终端
#include <stdio.h>

int main(int argc, char const *argv[])
{
char buf[32] = “”;
fread(buf, sizeof(char), 10, stdin);
printf(“buf:%s\n”, buf);

fwrite(buf,1,10,stdout);
return 0;

}

b. 针对文件
#include <stdio.h>

int main(int argc, char const *argv[])
{
FILE *fp;
float arr[3] = {1.2, 3.4, 5.6};
float data[3] = {};
fp = fopen(argv[1], “r+”);
if (NULL == fp)
{
perror(“fopen err”);
return -1;
}

fwrite(arr, sizeof(float), 3, fp); //将3个元素写到文件,因为是float类型所以在文件里是乱码

//需要将文件指针定位到文件开头,不然上一步是写操作,文件指针到末尾,读的话会从末尾继续读从而什么都读不到。
rewind(fp); //定位到文件头
fread(data, 4, 3, fp);

printf("%f %f %f\n", data[0], data[1], data[2]);

return 0;

}

文件定位操作:rewind(FILE *fp);

编程读写一个文件test.txt,每隔1秒向文件中写入一行录入时间的数据
作业:题目要求:编程读写一个文件test.txt,每隔1秒向文件中写入一行录入时间的数据,类似这样:
1,  2007-7-30 15:16:42  
2,  2007-7-30 15:16:43
该程序应该无限循环,直到按Ctrl-C中断程序。
再次启动程序写文件时可以追加到原文件之后,并且序号能够接续上次的序号,比如:
1,  2007-7-30 15:16:42
2,  2007-7-30 15:16:43
3,  2007-7-30 15:19:02
4,  2007-7-30 15:19:03
5,  2007-7-30 15:19:04

思路:
1.打开文件fopen, 循环往文件里写内容
2.每隔1s写入一行,sleep(1);
3.计算行数wc -l
4.计算当前时间,转换成年月日时分秒的格式: time localtime

man 2 time
time_t time(time_t *t);
如果t是空指针,直接返回当前时间。如果t不是空指针,返回当前时间的同时,将返回值赋予t指向的内存空间。
1.字符串拼接函数:strcpy/strcat(dest, src)、sprintf、fprintf
fprintf:
格式化输出到流(stream)文件中,返回值是输出的字符数,发生错误时返回一个负值.
int fprintf( FILE *stream, const char *format, … );
sprintf:
格式化输出发送到buffer(缓冲区)中.返回值是写入的字符数量.
int sprintf( char *buffer, const char *format, … );
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

int main(int argc, char const *argv[])
{
FILE *fp;
char buf[32] = “”;
int n = 0;
time_t t;
struct tm *tm;

fp = fopen(argv[1], "a+");
if (fp == NULL)
{
    perror("fopen err");
    return -1;
}

while (fgets(buf, 32, fp) != NULL)
{
    if (buf[strlen(buf) - 1] == '\n')
        n++;
}

while (1)
{
    time(&t); //t=time(NULL);
    tm = localtime(&t);

    fprintf(fp, "%d,%d-%d-%d %d:%d:%d\n", ++n, tm->tm_year + 1900,
            tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);

//月份从一月开始所以+1,年要+1900
fflush(NULL); //全缓存,要强制刷新。
sleep(1);
}
fclose(fp);

return 0;

}

3.4 其他操作
3.4.1 重定向流到文件 freopen
FILE * freopen(const char *pathname, const char mode, FILE fp);
功能:将指定的文件流重定向到打开的文件中
参数:path:文件路径
mode:打开文件的方式(同fopen)
fp:文件流指针
返回值:成功:返回文件流指针
失败:NULL

例子:
#include<stdio.h>

int main(int argc, char const *argv[])
{
printf(“hello world\n”);

//将标准输出流重定向到打开的文件
freopen("test.txt","r+",stdout);
printf("66666\n");

//将标准输出流重定向到终端文件
freopen("/dev/tty","r+",stdout);
printf("7777\n");

return 0;

}

3.4.2 文件定位操作
void rewind(FILE *stream);
功能:将文件位置指针定位到起始位置

int fseek(FILE *stream, long offset, int whence);
功能:文件的定位操作
参数:stream:文件流
offset:偏移量:正数表示向后文件尾部偏移,负数表示向文件开头偏移
whence:相对位置:
SEEK_SET:相对于文件开头
SEEK_CUR:相对于文件当前位置
SEEK_END:相对于文件末尾
返回值:成功:0
失败:-1
注:当打开文件的方式为a或a+时,fseek不起作用
补充:其中SEEK_SET,SEEK_CUR和SEEK_END和依次为0,1和2.

简言之:
fseek(fp,100,0); //把fp指针移动到离文件开头100字节处.
fseek(fp,100,1); //把fp指针移动到离文件当前位置100字节处;
fseek(fp,-100,2); //把fp指针退回到离文件结尾100字节处。

long ftell(FILE *stream);
功能:获取当前的文件位置
参数:要检测的文件流
返回值:成功:当前的文件位置,出错:-1

#include <stdio.h>

int main(int argc, char const *argv[])
{
FILE *fp;
fp = fopen(“test.txt”, “w+”);

fseek(fp, 10, SEEK_SET);
fputc('a', fp);

// fseek(fp,-15,SEEK_END);
// fputc('b',fp); //还是在结尾写,范围超了没意义

fseek(fp, 5, SEEK_CUR);
fputs("hello", fp);

fseek(fp, -1, SEEK_END);
fputc('c', fp);

long l = ftell(fp); //计算文件位置指针所在位置
printf("%ld\n", l);

return 0;

}

笔试题:
1.想对一个文本文件的尾部追加写入,应当在fopen何中使用的文件操作方式指示符号为 ()(杭州快越科技笔试题)
A.r B.wb C. a D.w+

2.fseek(1, 2, 3); 这个函数是什么作用,三个参数分别是什么意思?(深圳元征信息科技)

3.函数调用语句:fseek (fp,-10L,2);的含义是
A 将文件位置指针从文件未尾处向文件头的方向移动10个字节
B 将文件位置指针从当前位置向文件头的方向移动10个字节
C 将文件位置指针从当前位置向文件未尾方向移动10个字节

总结:
为什么用标准IO?
1.因为读写文件通常是大量的数据(相对于底层驱动的系统调用所实现的数据操作单位),这时,使用库函数可以大大减少系统调用的次数
2.为了保证可移植性

文件IO
1.什么是文件IO
1.1 概念
又称系统IO,是系统调用,是操作系统提供的接口函数。
posix中定义的一组用于输出输出的函数。
POSIX接口 (英语:Portable Operating System Interface)可移植操作系统接口(了解)

1.2 特点
(1)没有缓冲机制,每次调用都会引起系统调用。
(2)围绕着文件描述符进行操作,非负整数(>=0),依次分配的。
(3)文件IO默认打开三个文件描述符,分别是0(标准输入),1(标准输出),2(标准错误)
(4)操作除了目录以外的任意类型的文件: b c - l s p(除了d目录文件)

问题:打开三个文件,描述符分别是:3 4 5
关闭4以后,重新打开这个文件,描述符是几?
答: 还是4

问题:一个进程的文件描述符最大到几?最多能打开多少个文件描述符?最多能打开多少个文件
答: 一个进程的文件描述符最大到1023,最多能打开1024个文件描述符(因为还有0),最多能打开1024-3个文件

1.3 操作
打开文件:open
关闭文件:close
读写操作:read、write
定位操作:lseek

2.接口函数
2.1 打开文件open()
int open(const char *pathname, int flags);
功能:打开文件
参数:pathname:文件路径名
flags:打开文件的方式
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
O_CREAT:不存在创建
O_TRUNC:存在清空
O_APPEND:追加
返回值:成功:文件描述符
失败:-1

当第二个参数中有O_CREAT选项时,需要给open函数传递第三个参数,指定创建文件的权限
int open(const char *pathname, int flags, mode_t mode);
最后权限=创建出来的文件指定权限值&(~umask)

例如:指定权限位0666(8进制)
最终权限:0666&(umask)=0666&(0002)

例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
int fd;
// fd=open(“a.c”,O_RDONLY); //没有这个文件会报错,因为是只读方式,没有不存在创建
fd = open(“a.c”, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror(“open err”);
return -1;
}
printf(“fd: %d\n”, fd);

return 0;

}

思考:文件IO和标准IO的打开方式的对应关系

标准IO 文件IO
r O_RDONLY
只读
r+ O_RDWR
可读可写
w O_WRONLY|O_CREAT|O_TRUNC,0777
只写,不存在创建,存在清空
w+ O_RDWR|O_CREAT|O_TRUNC,0777
可读可写,不存在创建,存在清空
a O_WRONLY|O_CREAT|O_APPEND,0777
只写,不能存在创建,存在追加
a+ O_RDWR|O_CREAT|O_APPEND,0666
可读可写,不存在创建,存在追加
注意:有O_CREAT需要加第三个参数表示权限

2.2 关闭文件
#include <unistd.h>
int close(int fd);
功能:关闭文件
参数:fd:文件描述符

2.3 读写文件
2.3.1 读文件
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:从一个已打开的可读文件中读取数据
参数: fd 文件描述符
buf 存放位置
count 期望的个数
返回值:成功:实际读到的个数(小于期望值说明实际没这么多)
返回0:表示读到文件结尾
返回-1:表示出错,并设置errno号

fgetc : 失败或末尾返回EOF
fgets: 失败或末尾返回NULL
fread:失败或末尾返回0
read:失败返回-1,末尾返回0

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
int fd;
char buf[32] = “”;

fd=open("a.c",O_RDONLY);  //没有这个文件会报错,因为是只读方式,没有不存在创建
//fd = open("a.c", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
    perror("open err");
    return -1;
}
printf("fd: %d\n", fd);

//从文件中读
read(fd, buf, 10);
printf("buf:%s\n", buf);

close(fd);

return 0;

}

2.3.2 写文件
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
功能:向指定文件描述符中,写入 count个字节的数据。
参数:fd 文件描述符
buf 要写的内容
count 期望写入字节数
返回值:成功:实际写入数据的个数
失败 : -1

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
int fd;
char buf[32] = “”;

fd=open("a.c",O_RDWR);  //没有这个文件会报错,因为是只读方式,没有不存在创建
//fd = open("a.c", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
    perror("open err");
    return -1;
}
printf("fd: %d\n", fd);

read(fd, buf, 10);
printf("buf:%s\n", buf);

//向文件中写
write(fd,"world",5);
write(fd,"world",10);//文件中写入world?????

close(fd);

return 0;

}

练习:文件IO实现cp功能。cp 源文件 新文件名
./a.out src dest
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
char buf[32]=“”;
ssize_t s;
int fd_src, fd_dest;
if (argc != 3)
{
printf(“usage: %s \n”, argv[0]);
return -1;
}

//打开源文件,以只读的方式
fd_src = open(argv[1], O_RDONLY);
if (fd_src < 0)
{
    perror("fd_src open err");
    return -1;
}

//打开目标文件以可写的方式
fd_dest = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0777);
if (fd_dest < 0)
{
    perror("fd_dest open err");
    return -1;
}

//循环读源文件,只要读到内容就把读到的内容写到目标文件中
while ((s=read(fd_src,buf,32))>0)
    write(fd_dest,buf,s); //每次读多少用s接收,读多少就写多少到目标文件  
close(fd_src);
close(fd_dest);
return 0;

}

2.4 文件定位操作
off_t lseek(int fd, off_t offset, int whence);
功能:设定文件的偏移位置
参数:fd:文件描述符
offset: 偏移量
正数:向文件结尾位置移动
负数:向文件开始位置
whence: 相对位置
SEEK_SET 开始位置
SEEK_CUR 当前位置
SEEK_END 结尾位置
补充:和fseek一样其中SEEK_SET,SEEK_CUR和SEEK_END和依次为0,1和2.

返回值:成功:文件的当前位置
失败:-1

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
int fd;
char buf[32] = “”;

fd = open("a.c", O_RDWR); 
if (fd < 0)
{
    perror("open err");
    return -1;
}
printf("fd: %d\n", fd);

lseek(fd,10,SEEK_END);
write(fd,"k",1);

//利用结尾偏移求出文件长度
off_t off=lseek(fd,0,2); //定位到文件结尾,也就是距离结尾偏移0,然后接收返回值从而计算出文件长度
printf("off=%ld\n",off);

close(fd);

return 0;

}

练习:向文件中第 10 位置后面写一个字符,在文件此时的位置,后第 20个位置处,写一行字符串hello进去,求此时文件的长度。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
int fd;
char buf[32] = “”;

fd = open("a.c", O_RDWR); 
if (fd < 0)
{
    perror("open err");
    return -1;
}
printf("fd: %d\n", fd);

lseek(fd,10,SEEK_SET);
write(fd,"u",1);

lseek(fd,20,SEEK_CUR);
write(fd,"hello",5);

off_t off=lseek(fd,0,2);
printf("off=%ld\n",off);

close(fd);

return 0;

}

总结标准IO和文件IO
标准IO 文件IO
概念 C库中定义的一组用于输入输出的函数 posix中定义的一组输入输出的函数
特点 1.有管冲机制减少系统调用提高效率
2.围绕流进行操作:FILE *
3.默认打开三个流:stdout/stdin/stderr
4.只能操作普通文件
5.可移植性好 1.无缓冲机制,每次都能引起系统调用
2.围绕文件描述符操作,非负整数
3.默认打开三个文件描述符:0/1/2
4.可以操作除了目录以外的所有文件
5.可移植性相对较弱
函数接口 打开文件:fopen、freopen
关闭文件:fclose
读文件:fgetc/fgets/fread
写文件:fputc/fputs/fwrite
定位操作:fseek/rewind/ftell 打开文件:open
关闭文件:close
读文件:read
写文件:write
定位操作:lseek

练习: 实现“head -n 文件名”命令的功能
实现“head -n 文件名”命令的功能
例:head -3 test.c -> ./a.out -3 test.c
atoi : “1234” – 1234
n = atoi(argv[1]+1) ==> 3
思想:循环读并且打印,读到一行就行数+1,然后判断是否达到打印最后一行,如果达到就退出。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char const *argv[])
{
FILE *fp;
char buf[32];
int num = 0;
int n = 0;
if (argc != 3)
{
printf(“usage:%s -n \n”, argv[0]);
return -1;
}

fp = fopen(argv[2], "r");
if (NULL == fp)
{
    perror("fopen err");
    return -1;
}

num = atoi(argv[1] + 1); //./a.out -3 a.c 去掉-
if (num == 0)    //判断-0的情况
    return 0;

//循环读,读到内容就打印到终端,直到达到需要打印的行就终止
while (fgets(buf, 32, fp))
{
    if (buf[strlen(buf) - 1] == '\n') //计算行数,如果遇见\n就累加
        n++;
    printf("%s", buf);
    if (num == n) //达到需要打印的行数就终止
        break;
}

fclose(fp);
return 0;

}

文件属性获取
1.stat函数: man 2 stat
int stat(const char *path, struct stat buf);
功能:获取文件属性
参数: path:文件路径名
buf:保存文件属性信息的结构体
返回值:成功:0
失败:-1
struct stat {
ino_t st_ino; /
inode号 ls -il /
mode_t st_mode; /
文件类型和权限 /
nlink_t st_nlink; /
硬链接数 /
uid_t st_uid; /
用户ID /
gid_t st_gid; /
组ID /
off_t st_size; /
大小 /
time_t st_atime; /
最后访问时间 /
time_t st_mtime; /
最后修改时间 /
time_t st_ctime; /
最后状态改变时间 */
};

打印inode号,链接数,大小:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
struct stat st;
if (stat(“stat.c”, &st) < 0)
{
perror(“stat err”);
return -1;
}
printf(“inode:%lu nlink:%d size:%ld\n”, st.st_ino,
st.st_nlink, st.st_size);

return 0;

}

st_mode 主要包含了 3 部分信息:
a. 15bit ~ 12bit 保存文件类型
b. 11bit ~ 9bit 保存执行文件时设置的信息(不用管)
c. 8bit ~ 0bit 保存文件访问权限

2.获取文件类型
S_IFMT是一个掩码,它的值是0170000(注意这里用的是八进制前缀为0,二进制为0b1111000000000000), 可以用来过滤提取出表示的文件类型的那四位(15bit~12bit位)。

这四位可以表示0b00000b1111(八进制表示:001014)七个值,每个值分别对应不同的文件类型:套接字文件、符号链接文件、普通文件、块设备、目录、字符设备、管道。

通过man手册可以看出,判断一个文件是不是普通文件,首先通过掩码S_IFMT把其他无关的部分置0,再与表示普通文件的数值比较,从而判断这是否是一个普通文件:

位操作公式解释:

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
struct stat st;
if (stat(“stat.c”, &st) < 0)
{
perror(“stat err”);
return -1;
}
printf(“st_mode:%#o\n”, st.st_mode);

//判断文件类型
if ((st.st_mode & S_IFMT) == S_IFREG)
    printf("-");
else if ((st.st_mode & S_IFMT) == S_IFDIR)
    printf("d");

//或者用宏函数
if (S_ISREG(st.st_mode))
    printf("-");
else if (S_ISDIR(st.st_mode))
    printf("d");
    
return 0;

}

作业:
1.完善整理所学内容,整理笔记,代码敲两遍截图发群里
2.用标准IO实现cp功能
#include <stdio.h>

int main(int argc, char const *argv[])
{
FILE *fp_src, *fp_dest;
char buf[32] = “”;
if (argc != 3)
{
printf(“format err\n”);
return -1;
}

fp_src = fopen(argv[1], "r");
if (NULL == fp_src)
{
    perror("open src err");
    return -1;
}
fp_dest = fopen(argv[2], "w");
if (NULL == fp_dest)
{
    perror("open dest err");
    return -1;
}

while (fgets(buf, 32, fp_src) != NULL)
    fputs(buf, fp_dest);

fclose(fp_src);
fclose(fp_dest);

return 0;

}

3.获取文件权限
0-8bit位每一位表示一个权限,所以只需要把这一位位与出来就可以判断是否有这个权限,为1说明有,为0说明没有。

比如判断个人权限是否有可读: st.st_mode&0b0000100000000000(八进制:00400)
也就是利用宏: st.st_mode&S_IRUSR

解释:

代码如下:
//判断文件权限
//判断个人权限知否读写执行
if (st.st_mode & S_IRUSR)
putchar(‘r’);
else
putchar(‘-’);

if (st.st_mode & S_IWUSR)
    putchar('w');
else
    putchar('-');

if (st.st_mode & S_IXUSR)
    putchar('x');
else
    putchar('-');

练习:编程实现“ls -l 文件名”功能
getpwuid
getgrgid
localtime或ctime

ctime函数在C库中,头文件为<time.h>
函数原型:
char *ctime (const time_t *__timer)
作用:返回一个表示当地时间的字符串,当地时间是基于参数 timer
格式例如: Wed Aug 29 19:48:54 2018

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>

int main(int argc, char const *argv[])
{
struct stat st;
if (stat(argv[1], &st) < 0)
{
perror(“stat err”);
return -1;
}

//判断文件类型
switch (st.st_mode & S_IFMT)
{
case S_IFREG:
    printf("-");
    break;
case S_IFDIR:
    printf("d");
    break;
case S_IFCHR:
    printf("c");
    break;
case S_IFIFO:
    printf("f");
    break;
case S_IFLNK:
    printf("l");
    break;
case S_IFBLK:
    printf("b");
    break;
case S_IFSOCK:
    printf("s");
    break;
default:
    printf("mode err\n");
    break;
}

//判断文件权限
//个人权限
if (st.st_mode & S_IRUSR)
    printf("r");
else
    printf("-");
if (st.st_mode & S_IWUSR)
    printf("w");
else
    printf("-");
if ((st.st_mode & S_IXUSR))
    printf("x");
else
    printf("-");

//小组权限
if (st.st_mode & S_IRGRP)
    printf("r");
else
    printf("-");
if (st.st_mode & S_IWGRP)
    printf("w");
else
    printf("-");
if ((st.st_mode & S_IXGRP))
    printf("x");
else
    printf("-");
//其他人权限
if (st.st_mode & S_IROTH)
    printf("r");
else
    printf("-");
if (st.st_mode & S_IWOTH)
    printf("w");
else
    printf("-");
if ((st.st_mode & S_IXOTH))
    printf("x");
else
    printf("-");

//链接数
printf(" %d", st.st_nlink);

//用户名 用到getpwuid函数
printf(" %s", getpwuid(st.st_uid)->pw_name);

//组名 用到getgrgid函数
printf(" %s", getgrgid(st.st_gid)->gr_name);

//文件大小
printf(" %ld", st.st_size);

//最后修改时间
printf(" %.12s", ctime(&st.st_mtime) + 4); 
//+4代表往后偏移4个地址也就是跳过前4位,%.12s代表只打印12个字符

//名字
printf(" %s\n", argv[1]);

return 0;

}

stat/fstat/lstat的区别?
stat函数返回一个与此命名文件有关的信息结构
fstat函数获得已在描述符filedes上打开的文件的有关信息,也就是参数是文件描述符,其他与stat相同。
lstat函数类似于stat,但是当命名的文件是一个符号连接时,lstat返回该符号连接的有关信息,而不是由该符号连接引用的文件的信息.

目录操作
围绕目录流进行操作:DIR *
opendir
closedir
readdir
chdir
DIR *opendir(const char *name);
功能:获得目录流
参数:要打开的目录
返回值:成功:目录流
失败:NULL

struct dirent readdir(DIR dirp);
功能:读目录
参数:要读的目录流
返回值:成功:读到的信息
失败:NULL
返回值为结构体,该结构体成员为描述该目录下的文件信息
struct dirent {
ino_t d_ino; /
索引节点号
/
off_t d_off; /在目录文件中的偏移/
unsigned short d_reclen; /* 文件名长度*/
unsigned char d_type; /* 文件类型 /
char d_name[256]; /
文件名 */
};

int closedir(DIR *dirp);
功能:关闭目录
参数:dirp:目录流

#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
DIR *dir;
struct dirent *d;

dir = opendir(".");
if (NULL == dir)
{
    perror("opendir err");
    return -1;
}

d = readdir(dir);
printf("%s\n", d->d_name);

d = readdir(dir);
printf("%s\n", d->d_name);

return 0;

}

练习:实现 ls -a
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
DIR *dir;
struct dirent *d;

dir = opendir(".");
if (NULL == dir)
{
    perror("opendir err");
    return -1;
}

while ((d=readdir(dir))!=NULL)
{
    printf("%s\n",d->d_name);
}
//这里顺序不是按照ls排序的顺序,因为ls自身有个排序算法。这里是按照文件在磁盘的顺序读取的。

return 0;

}


头文件:#include<stdio.h>
<>代表从系统路径下查找头文件/usr/include。
#include “head.h”
""代表先去当前路径下查找头文件,找不到再去系统路径下查找。

头文件也就是.h结尾的文件,包含:其他头文件、宏定义、重命名、结构体、共用体、枚举的定义、函数声明、条件编译、外部引用

源文件:包含main函数的xx.c
包含子函数的xx.c, 封装的函数需要再头文件里声明

库文件(不能包含main函数)

1.库的定义
当使用别人的函数时除了包含头文件以外还需要有库
头文件:其他头文件、宏定义、重命名、结构体、共用体、枚举的定义、函数声明、条件编译、外部引用
库:把一些常用的函数的目标文件打包在一起,提供相应的函数接口,便于程序员使用。本质上来说库是一种可执行代码的二进制形式文件。
由于windows和linux的本质不同,因此而这库的二进制是不兼容的。(Linux中的C运行库是glibc, 由GUN发布。)

2.库的分类
静态库和动态库,本质区别是代码被载入的时刻不同。
2.1 静态库
静态库是在程序编译的时候被复制到目标代码中的,以.a结尾。
优点: 程序运行时不需要该静态库;运行时无需加载库,运行速度快;可移植性好。
缺点:静态库中的代码复制到了程序中,因此程序体积较大;静态库升级后,程序需要重新编译链接。

2.2 动态库
动态库是在程序运行时才被载入代码中。也叫共享库,以.so结尾。
优点:程序在执行时加载动态库,代码体积小;程序升级更简单;
不同应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。
缺点:运行时还需要动态库的存在,运行时还需要加载库所以速度慢,可移植性较差。

3.静态库的制作
(1)将源文件编译生成目标文件
gcc -c fun.c -o fun.o
注意: 不要编译有main函数的文件
(2)创建静态库用ar命令,将很多.o转换成.a
ar crs libmyfun.a fun.o
(3)测试使用静态库:
gcc main.c -L. -lmyfun
注意:-L加路径:指定库的路径; -l库名:指定链接的库
执行:./a.out

4.动态库制作
(1)用gcc来创建共享库
gcc -fPIC -c fun.c -o fun.o //-fPIC创建与地址无关的编译程序
gcc -shared -o libfun.so fun.o
(2)测试动态库并使用
gcc main.c -lfun
执行:./a.out 可以正常编译通过,但是运行时会报错
./a.out: error while loading shared libraries: libfun.so: cannot open shared object file: No such file or directory
原因:当加载动态库时,系统会自动从/lib或/usr/lib路径下查找库文件,所以不用-L加路径,直接: gcc main.c -lfun就可以了

解决方法(有三种):
1)把库拷贝到/usr/lib和/lib目录下。(此方法编译时不需要指定库的路径)
2)在LD_LIBRARY_PATH环境变量中加上库所在路径。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
(终端关闭,环境变量就没在了)
3)添加/etc/ld.so.conf.d/*.conf文件。把库所在的路径加到文件末尾,并执行ldconfig刷新
sudo vi xx.conf
添加动态库存在的路径,如:
/home/hq/work/lib

注意:
同名的静态库和动态库:默认优先使用动态库,如果想使用静态库需要在后面加 -static,这是内核规定的。

-L路径:指定库的路径
-l库名:指定链接的库
-I路径(大写i):指定头文件的路径 默认的查找头文件的路径/usr/include
#include<stdio.h> //从系统路径下查找头文件
#include"head.h" //从当前路径下查找头文件,找不到再去系统路径下查找

ldd 可执行文件名:查看链接的动态库

5.总结静态库和动态库
静态库:编译阶段,以.a结尾,体积大,移植性好,升级麻烦。
动态库:运行阶段,以.so结尾,体积小,移植性差,升级简单。

可以看出静态库编译出来的程序体积大:

升级演示:改变源文件
静态库需要重新编译

动态库只需要重新生成库,不需要重新编译

进程 Process
1.什么是进程
进程和程序的区别
1.1 概念
程序:编译好的可执行文件
存放在磁盘上的指令和数据的有序集合(文件)
程序是静态的,没有任何执行的概念

进程:一个独立的可调度的任务
执行一个程序所分配的资源的总成
进程是程序的一次执行过程
进程是动态的,包括创建、调度、执行和消亡

1.2 特点
(1)系统会位每一个进程分配0-4g的虚拟空间,0-3(用户空间)是每个进程所独有的,3-4g(内核空间)是所有进程共有的。

(2)CPU调度进程时会给进程分配时间片(几毫米~十几毫秒),当时间片用完后,CPU再进行其他进程的调度,实现进程的轮转,从而实现多任务操作。(没有外界干预的情况下怎么调度进程是CPU随机分配的 )

进程控制块task_struct
●进程控制块pcb:包含描述进程的相关信息
●进程标识PID:唯一的标识一个进程
主要进程标识:
进程号(PID: Process Identity Number)
父进程号:(Parent Process ID: PPID)
●进程用户
●进程状态、优先级
●文件描述符(记录当前进程打开的文件)

1.3 进程段
Linux中的进程大致包含三个段:
数据段:存放的是全局变量、常数以及动态数据分配的数据空间(如malloc函数取得的空间)等。
正文段:存放的是程序中的代码
堆栈段:存放的是函数的返回地址、函数的参数以及程序中的局部变量 (类比内存的栈区)

1.4 进程分类
交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行。该类进程经常与用户进行交互,需要等待用户的输入,当接收到用户的输入后,该类进程会立刻响应,典型的交互式进程有:shell命令进程、文本编辑器等。
批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行。(目前接触不到)
守护进程:该类进程在后台运行。它一般在Linux启动时开始执行,系统关闭时才结束。

1.5 进程状态
D uninterruptible sleep (usually IO) 不可中断的睡眠态
R running or runnable (on run queue) 运行态
S interruptible sleep (waiting for an event to complete) 可中断的睡眠态
T stopped by job control signal 暂停态
t stopped by debugger during the tracing 因为调试而暂停
X dead (should never be seen) 死亡态
Z defunct (“zombie”) process, terminated but not reaped by its parent 僵尸态
< high-priority (not nice to other users) 高优先级
N low-priority (nice to other users) 低优先级
L has pages locked into memory (for real-time and custom IO) 锁在内存中
s is a session leader 会话组组长
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)多线程

  • is in the foreground process group 前台进程
    没有+时,默认是后台进程
    I (大写i)空闲状态进程

1.6 进程状态切换图

进程创建后,进程进入就绪态,当CPU调度到此进程时进入运行态,当时间片用完时,此进程会进入就绪态,如果此进程正在执行一些IO操作(阻塞操作)会进入阻塞态,完成IO操作(阻塞结束)后又可进入就绪态,等待CPU的调度,当进程运行结束即进入结束态。
1.7 进程相关命令
ps 查看系统中运行的进程 -aux -ef
top 动态显示系统中的进程
renice 改变正在运行的进程的优先级
nice 按用户指定的优先级启动进程
kill 给进程发送信号
bg 将进程切换到后台执行
fg 将进程切换到前台执行
jobs 查看当前终端的进程

补充:优先级调度(面试可能遇见)
根据进程的优先级进行调度,优先级高的进程先执行。
两种类型:
1.非剥夺式(非抢占式)优先级调度算法。当一个进程正在处理上运行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在进行的进程继续运行,直到由于其自身原因而主动让出处理机(任务完成或等待事件),才把处理机分配给更为重要或紧迫的进程。
2.剥夺式(抢占式)优先级调度算法。当一个进程正在处理机上运行时,若有某个更为重要或紧迫的进程进入就绪队列,则立即暂停正在运行的进程,将处理机分配给更重要或紧迫的进程。

面试题:
1.下列关于轮询任务调度和可抢占式调度区别描述错误的是?
A. 抢占式调度实现相对较复杂且可能出现低优先级的任务长期得不到调度
B. 轮询调度不利于后面的请求及时得到响应
C. 抢占式调度有利于后面的高优先级的任务也能及时得到响应
D. 抢占式调度优点是其简洁性,它无需记录当前所有连接的状态
错误原因:抢占式调度相对于轮询调度来说实现相对复杂,需要记录和管理任务的优先级、状态等信息。

2.会导致进程从执行态变为就绪态的事件是( )。(大明科技)
A. 执行P(wait)操作
B. 申请内存失败
C. 启动I/O设备
D.被高优先级进程抢占

3.分配到必要的资源并获得处理机时的进程状态是( )。(大明科技)
A. 就绪状态
B. 执行状态
C.阻塞状态

2.进程函数接口
2.1 创建进程fork()
pid_t fork(void);
功能:创建子进程
返回值:
成功:在父进程中:返回子进程的进程号 >0
在子进程中:返回值为0
失败:-1并设置errno

例子:
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
pid_t pid;
pid = fork();
if(pid<0)
{
perror(“fork err”);
return -1;
}
else if(pid==0)
{
printf(“child\n”);
while(1); //让子进程不要结束
}
else
{
printf(“parent\n”);
while(1); //让父进程不要结束
}

return 0;

}

解释:fork()函数之前只有一个进程,fork()函数相当于在这个进程中又开启的一个进程并且把代码以及数据信息拷贝了一份,这两个进程只有个别数据例如进程号不一样。

特点:
1)子进程几乎拷贝了父进程的全部内容。包括代码、数据、系统数据段中的pc值、栈中的数据、父进程中打开的文件等;但它们的PID、PPID是不同的。
2)父子进程有独立的地址空间,互不影响;当在相应的进程中改变全局变量、静态变量,都互不影响。
3)若父进程先结束,子进程成为孤儿进程,被init进程收养,子进程变成后台进程。
4)若子进程先结束,父进程如果没有及时回收,子进程变成僵尸进程(要避免僵尸进程产生)

验证:
(1)子拷贝父资源,但是PID和PPID各不相同

(2)父子进程有独立的地址空间,互不影响

结论:改变一个进程中的变量是不会影响另一个进程的,因为进程有独立的空间,互不影响。

(3)孤儿进程的产生

(4)僵尸进程的产生

2.2 回收资源
pid_t wait(int *status);
功能:回收子进程资源(阻塞)
参数:status:子进程退出状态,不接受子进程状态设为NULL
返回值:成功:回收的子进程的进程号
失败:-1

pid_t waitpid(pid_t pid, int *status, int options);
功能:回收子进程资源
参数:
pid:>0 指定子进程进程号
=-1 任意子进程
=0 等待其组ID等于调用进程的组ID的任一子进程
<-1 等待其组ID等于pid的绝对值的任一子进程
status:子进程退出状态
options:0:阻塞 WNOHANG:非阻塞
返回值:正常:结束的子进程的进程号
当使用选项WNOHANG且没有子进程结束时:0
出错:-1

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror(“fork err”);
return -1;
}
else if (pid == 0)
{
printf(“child\n”);
sleep(3);
}
else
{
printf(“parent\n”);
//wait(NULL);
//waitpid(-1,NULL,0); //0代表阻塞,此时和wait(NULL)是一样的
//waitpid(-1,NULL,WNOHANG); //WNOHANG代表非阻塞,此时有可能接收不到子进程资源从而产生僵尸进程

    //所以要循环调用waitpid,直到接收到了子进程资源才结束循环
    while (1)
    {
        if (waitpid(-1, NULL, WNOHANG) > 0)
            break;
    }
    while (1)
        ;
}
return 0;

}

作业:
1.吸收今天所学内容,整理笔记,代码敲两遍以上截图发群里。
2.自己尝试waitpid函数用法
3.9到9点半,组长发起互相提问目前所学知识点。反馈到群里。

2.3 结束进程
void exit(int status);
功能:结束进程,刷新缓存
void _exit(int status);
功能:结束进程,不刷新缓存
参数:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。

通常0表示正常结束;
其他的数值表示出现了错误,进程非正常结束

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
printf(“hello”);
//exit(0); //让进程结束,刷新缓存
_exit(0); //让进程结束,不刷新缓存
while(1);

return 0;

}

思考: exit和return的区别
exit结束的是进程而return结束的是当前函数
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int fun()
{
printf(“in fun\n”);
//return 0; //结束的是当前函数,进程还会继续执行,后面主函数的打印语句还能执行
exit(0); //整个进程结束,所以后面主函数的打印语句执行不到了
}

int main(int argc, char const *argv[])
{
fun();
printf(“in main\n”);

return 0;

}

2.4 获取进程号
pid_t getpid(void);
功能:获取当前进程的进程号
pid_t getppid(void);
功能:获取当前进程的父进程号

例如: 在父子进程中分别打印这两个进程的进程号
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
pid_t pid = fork();
if (pid < 0)
{
perror(“fork err”);
return -1;
}
else if (pid == 0)
{
printf(“child:%d %d\n”,getppid(),getpid()); //PPID和PID
while(1);
}
else
{
printf(“parent:%d %d\n”,getpid(),pid); //PID和子进程的PID
while(1);
}
return 0;
}

3.exec函数族(了解)
在进程中执行另一个进程:
system(“clear”);
system(“ls -l”);

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
printf(“hello\n”);
//system(“pwd”); //原进程不会被替换,执行完召唤进程后还会继续执行原来的进程,所以后面的66666666666666666还会打印
execl(“/bin/ls”,“ls”,“-l”,NULL); //原进程会被完全替换,后面不打印了
printf(“66666666666666666\n”);

return 0;

}

原型:
#include <unistd.h>
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg,…, char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

4.守护进程 Daemon
Linux以会话(session)、进程组的方式管理进程,每个进程属于一个进程组,也就是多个进程组成一个进程组。会话是一个或多个进程组的集合,通常用户打开一个终端时,系统会创建一个会话。所有通过该终端运行的进程都属于这个会话。终端关闭时,所有相关进程会被结束。但是守护进程却能突破这种限制,不受终端关闭的影响。

4.1 特点
守护进程是后台进程;
生命周期比较长,一般从系统启动时开启,系统关闭时结束;
它是脱离控制终端且周期执行的进程。

4.2 创建步骤
(1)创建子进程,父进程退出让子进程成为孤儿进程,成为后台进程;fork()
(2)在子进程中创建新会话
让子进程成为会话组组长并且脱离终端:为了让子进程完全脱离终端;setsid()
(3)改变进程运行路径为根目录
原因: 进程运行的路径不能被删除或卸载;chdir(“/”)
函数说明:chdir() 将进程当前的工作目录改变成以参数路径所指的目录
(4)重设文件权限掩码
目的:增大进程创建文件时权限,提高灵活性;umask(0)
子进程继承了父进程的文件权限掩码,给该子进程使用文件带来一定的影响,因此把文件 权限掩码设置为0,可以增强该守护进程的灵活性。
(5)关闭文件描述符
原因:子进程继承了父进程的一些已经打开了的文件,这些被打开的文件可能永远不会被 守护进程访问,但它们一样占用系统资源,而且还可能导致所在的文件系统无法被卸载。
将不需要的文件关闭:close()

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>

int main(int argc, char const *argv[])
{
pid_t pid = fork();
if (pid < 0)
{
perror(“fork err”);
return -1;
}
else if (pid == 0)
{
setsid(); //创建新会话,成为会话组组长脱离终端。
chdir(“/”); //改变进程所属路径为根目录
umask(0); //重设文件权限掩码为0
for (int i = 0; i < 3; i++) //关闭默认打开的0 1 2文件描述符
close(i);
while (1)
;
}
else
{
exit(0);
}
return 0;
}

练习:创建一个守护进程,循环间隔1s向文件中写入一串字符“hello”
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
pid_t pid = fork();
if (pid < 0)
{
perror(“fork err”);
return -1;
}
else if (pid == 0)
{
setsid();
chdir(“/”);
umask(0);
for (int i = 0; i < 3; i++) //关闭默认打开的0 1 2
close(i);
int fd = open(“/home/hq/work/24021/IO/process/test.txt”,
O_RDWR | O_CREAT | O_APPEND, 0777); //a+
while (1)
{
sleep(1);
write(fd, “hello”, 5);
}
}
else
{
exit(0);
}
return 0;
}

总结守护进程:
●守护进程是一个生存周期较长的进程,通常独立于控制终端并且周期性的执行某种任务或者等待处理某些待发生的事件
●大多数服务都是通过守护进程实现的
●关闭终端,相应的进程都会被关闭,而守护进程却能够突破这种限制

线程 Thread
1.什么是线程
1.1 概念
线程是一个轻量级的进程,为了提高系统的性能引入线程。
线程和进程都参与统一的调度。
在同一进程中可以创建多个线程,这多个线程共享同一进程的资源。

(Linux里同样用task_struct来描述一个线程)

1.2 进程和线程的区别
相同点:
都为操作系统提供了并发执行的能力
不同点:
(1)调度和资源:线程是调度的最小单位;进程是资源分配的最小单位。
(2)地址空间方面:同一进程中的多线程共享该进程的资源;进程的地址空间相互独立。
(3)通信方面:线程通信相对简单,只需要通过全局变量可以实现,但是需要考虑临界资源访问的问题;进程通信比较复杂,需要借助进程间的通信机制(借助3g-4g内核空间)
(4)安全性方面:线程安全性差一些,当进程结束时会导致所有线程退出;进程相对安全

面试题:程序什么时候该使用线程?什么时候用进程?(深圳棱镜空间智能科技有限公司)(北京明朝万达)
简而言之:对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高、速度快的高并发环境时,需要频繁创建、销毁或切换时,资源的保护管理要求不是很高时,使用多线程。

1.3 线程资源
共享的资源:可执行的指令、静态数据、进程中打开的文件描述符、信号处理函数、当前工作目录、用户ID、用户组ID
私有的资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈(局部变量, 返回地址)、错误号 (errno)、信号掩码和优先级、执行状态和属性

2.函数接口
2.1 创建线程:pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
功能:创建线程
参数: thread:线程标识(线程号)
attr:线程属性, NULL:代表设置默认属性
start_routine:函数名:代表线程函数(自己写的)
arg:用来给前面函数传参
返回值:成功:0
失败:错误码

编译的时候需要加 -pthread 链接动态库

函数指针格式:数据类型 (*指针名)(参数列表);
#include <stdio.h>
#include <stdlib.h>
int test(int (*p)(int, int), int a, int b)//p=fun,a=3,b=4
{
return p(a,b); //fun(3,4)
}
int fun(int m, int n)
{
return m * n;
}

int main(int argc, char const *argv[])
{
printf(“%d\n”, test(fun, 3, 4)); //12
return 0;
}

创建线程:
#include <pthread.h>
#include <stdio.h>

//线程函数
void *handler_tread(void *arg)
{
printf(“in handler_tread\n”);
while (1); //让线程不退出,进程状态为l,也就是多线程

return NULL;

}

int main(int argc, char const *argv[])
{
pthread_t tid;
if (pthread_create(&tid, NULL, handler_tread, NULL) != 0)
{
perror(“pthread create err”);
return -1;
}
printf(“in main\n”);
while (1); //让主线程也不要结束
return 0;
}

2.2 退出线程:pthread_exit
void pthread_exit(void *value_ptr)
功能:用于退出线程的执行
参数:value_ptr:线程退出时返回的值

#include <pthread.h>
#include <stdio.h>

//线程函数
void *handler_tread(void *arg)
{
printf(“in handler_tread\n”);
pthread_exit(NULL); //让线程退出
while (1);

return NULL;

}

int main(int argc, char const *argv[])
{
pthread_t tid;
if (pthread_create(&tid, NULL, handler_tread, NULL) != 0)
{
perror(“pthread create err”);
return -1;
}
printf(“in main\n”);
while (1); //让主线程不要结束
return 0;
}

2.3 回收线程资源
int pthread_join(pthread_t thread, void **value_ptr)
功能:用于等待一个指定的线程结束,阻塞函数
参数:thread:创建的线程对象,线程ID
value_ptr:指针*value_ptr指向线程返回的参数, 一般为NULL
返回值:成功 : 0
失败:errno

int pthread_detach(pthread_t thread);
功能:让线程结束时自动回收线程资源,让线程和主线程分离,非阻塞函数
参数:thread:线程ID
非阻塞式的,例如主线程分离(detach)了线程T2,那么主线程不会阻塞在pthread_detach(),pthread_detach()会直接返回,线程T2终止后会被操作系统自动回收资源

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

//线程函数
void *handler_tread(void *arg)
{
printf(“in handler_tread\n”);
sleep(1);
pthread_exit(NULL); //让线程退出

return NULL;

}

int main(int argc, char const *argv[])
{
pthread_t tid;
if (pthread_create(&tid, NULL, handler_tread, NULL) != 0)
{
perror(“pthread create err”);
return -1;
}
printf(“in main\n”);
//pthread_join(tid, NULL); //阻塞函数,阻塞等待指定线程退出以后回收其资源
pthread_detach(tid); //非阻塞函数,让指定线程结束后自己回收自己资源
printf(“after\n”);

while (1); //让主线程也不要结束
return 0;

}

练习: 通过父子进程完成对文件的拷贝(cp)
1.通过父子进程完成对文件的拷贝(cp),父进程从文件开始到文件的一半开始拷贝,子进程从文件的一半到文件末尾。要求:文件IO cp src dest
1)文件长度获取?lseek
2)子进程定位到文件一半的位置 lseek
3)父进程怎么能准确读到文件一半的位置
4)fork之前打开文件,父子进程中读写文件时,位置指针是同一个
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
int fd_src, fd_dest;
pid_t pid;
ssize_t s;
char buf[32];

if (argc != 3)
{
    printf("format err\n");
    return -1;
}
fd_src = open(argv[1], O_RDONLY);
if (fd_src < 0)
{
    perror("open src err");
    return -1;
}

fd_dest = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd_dest < 0)
{
    perror("open dest err");
    return -1;
}

off_t len = lseek(fd_src, 0, SEEK_END) / 2; //计算出一半长度

pid = fork();
if (pid < 0)
{
    perror("fork err");
    return -1;
}
else if (pid == 0) //子从一半到结尾
{
    //定位到一半位置
    lseek(fd_src, len, 0);  //从一半位置开始读
    lseek(fd_dest, len, 0); //从一半位置开始写
    //进行读写操作
    while ((s = read(fd_src, buf, 32)) > 0)
    {
        write(fd_dest, buf, s);
        sleep(1);
    }
}
else //从开头到一半
{
    wait(NULL); //等子结束以后父再开始读写
    //定位到开头
    lseek(fd_src, 0, 0);
    lseek(fd_dest, 0, 0);
    //进行读写操作
    while (len > 0) //利用读了多少让len减去多少,也就是len表示剩下要读的数量
    {
        if (len > 32)
            s = read(fd_src, buf, 32);
        else
            s = read(fd_src, buf, len);
        write(fd_dest, buf, s);
        len -= s; //剩下要读的字符数量
        sleep(1);
    }
}
close(fd_src);
close(fd_dest);
return 0;

}

练习:输入输出,quit结束
通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。
1)全局变量进行通信
2)加上标志位(flag),实现主线程输入一次,线程函数打印一次, int flag = 0;
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

char buf[32] = “”;
int flag = 0; //为了进行线程间通讯,保证主线程先执行输入操作,然后从线程再输出。

//线程函数
void *handler_tread(void *arg)
{
while (1) //从线程打印完了将flag置0
{
if (flag == 1)
{
if (strcmp(buf, “quit”) == 0)
break;
printf(“%s\n”, buf);
flag = 0;
}
}

return NULL;

}

int main(int argc, char const *argv[])
{
pthread_t tid;
if (pthread_create(&tid, NULL, handler_tread, NULL) != 0)
{
perror(“pthread create err”);
return -1;
}

while (1) //主线程输入完了将flag置1
{

// if (flag == 0) // 因为scanf会阻塞一下,不加判断也可以
// {
scanf(“%s”, buf);
if (strcmp(buf, “quit”) == 0)
break;
flag = 1;
// }
}

return 0;

}

作业:
1.完善今天所学代码,整理笔记,进程和线程的代码敲两遍以上截图发群里。
2.做一套基础练习题,答案截图发群里
3.做之前的函数练习题,答案截图发群里。
4.预习一下线程间同步,信号量
练习题答案最后发,有问题的可以自己敲一敲或者网上查一查,实在不明白可以发信息问我。
注意:以上作业截图发群里以后,也把截图提交到在线表格。组长最后统计组员作业情况以及掌握情况。

3.同步
3.1 什么是同步
同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情
(异步:异步则反之,并非一定需要一件事做完再做另一件事。)
3.2 同步机制
通过信号量实现线程间同步
信号量:通过信号量实现同步操作;由信号量来决定线程是继续运行还是阻塞等待.
信号量代表某一类资源,其值表示系统中该资源的数量:
信号量的值>0,表示有资源可以用, 可以申请到资源,
信号量的值<=0, 表示没有资源可以通用, 无法申请到资源, 阻塞.
信号量还是一个受保护的变量,只能通过三种操作来访问:初始化、P操作(申请资源)、V操作(释放资源)
sem_init(): 信号量初始化
sem_wait(): 申请资源,P操作,如果没有资源可用则阻塞,否则资源-1
sem_post(): 释放资源,V操作,非阻塞+1
3.3 函数接口
int sem_init(sem_t *sem, int pshared, unsigned int value)
功能:初始化信号量
参数:sem:初始化的信号量对象
pshared:信号量共享的范围(0: 线程间使用 非0:1进程间使用)
value:信号量初值
返回值:成功 0
失败 -1

int sem_wait(sem_t *sem)
功能:申请资源 P操作
参数:sem:信号量对象
返回值:成功 0
失败 -1
注:此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞

int sem_post(sem_t *sem)
功能:释放资源 V操作
参数:sem:信号量对象
返回值:成功 0
失败 -1
注:释放一次信号量的值加1,函数不阻塞

例如: 主线程输入,从线程输出
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

sem_t sem1;
char buf[32];

void *handler_thread(void *arg)
{
while (1)
{
//申请资源
sem_wait(&sem1); //P操作 -1
if (strcmp(buf, “quit”) == 0)
break;
printf(“%s\n”, buf);
}
}

int main(int argc, char const *argv[])
{
pthread_t tid;
if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
{
perror(“pthread create err”);
return -1;
}

//信号量初始化
if (sem_init(&sem1, 0, 0) != 0)
{
    perror("sem1 init err");
    return -1;
}

while (1)
{
    scanf("%s", buf);
    //释放资源
    sem_post(&sem1);
    if (strcmp(buf, "quit") == 0)
        break;
}
pthread_detach(tid);

return 0;

}

两个信号量实现:
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

sem_t sem1;
sem_t sem2;
char buf[32];

void *handler_thread(void *arg)
{
while (1)
{
//申请资源
sem_wait(&sem1); //P操作 -1
if (strcmp(buf, “quit”) == 0)
break;
printf(“%s\n”, buf);
sem_post(&sem2);
}
}

int main(int argc, char const *argv[])
{
pthread_t tid;
if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
{
perror(“pthread create err”);
return -1;
}

//信号量初始化
if (sem_init(&sem1, 0, 0) != 0)
{
    perror("sem1 init err");
    return -1;
}

if (sem_init(&sem2, 0, 1) != 0)
{
    perror("sem2 init err");
    return -1;
}

while (1)
{
    sem_wait(&sem2);  //输入之前等待从线程输出完成后释放资源
    scanf("%s", buf);
    //释放资源
    sem_post(&sem1);
    if (strcmp(buf, "quit") == 0)
        break;
}
pthread_detach(tid);

return 0;

}

4.互斥
4.1 概念
互斥:多个线程在访问临界资源时,同一时间只能一个线程访问
临界资源:一次仅允许一个线所使用的资源
临界区:指的是一个访问共享资源的程序片段
互斥锁:通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_unlock

4.2 函数接口
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
功能:初始化互斥锁
参数:mutex:互斥锁
attr: 互斥锁属性 // NULL表示缺省属性
返回值:成功 0
失败 -1

int pthread_mutex_lock(pthread_mutex_t *mutex)
功能:申请互斥锁
参数:mutex:互斥锁
返回值:成功 0
失败 -1
注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回

int pthread_mutex_unlock(pthread_mutex_t *mutex)
功能:释放互斥锁
参数:mutex:互斥锁
返回值:成功 0
失败 -1

int pthread_mutex_destroy(pthread_mutex_t *mutex)
功能:销毁互斥锁
参数:mutex:互斥锁

例如:打印导致数组功能
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t lock;
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

void *handler_swap(void *arg)
{
int t;
while (1)
{
pthread_mutex_lock(&lock);
for (int i = 0; i < 5; i++)
{
t = a[i];
a[i] = a[9 - i];
a[9 - i] = t;
}
pthread_mutex_unlock(&lock);
}
return NULL;
}

void *handler_print(void *arg)
{
while (1)
{
pthread_mutex_lock(&lock);
for (int i = 0; i < 10; i++)
printf(“%d “, a[i]);
printf(”\n”);
pthread_mutex_unlock(&lock);
sleep(1); //锁里面尽量减少耗时大的操作
}
return NULL;
}

int main(int argc, char const *argv[])
{
pthread_t t1, t2;
pthread_create(&t1, NULL, handler_swap, NULL);
pthread_create(&t2, NULL, handler_print, NULL);

//初始化互斥锁
if (pthread_mutex_init(&lock, NULL) != 0)
{
    perror("mutex init err");
    return -1;
}

pthread_mutex_destroy(&lock);

pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;

}

补充:死锁
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

死锁产生的四个必要条件
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

5.条件变量
条件变量用于在线程之间传递信号,以便某些线程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行。

一般和互斥锁搭配使用,实现同步机制:
pthread_cond_init(&cond,NULL); //初始化条件变量

使用前需要上锁:
pthread_mutex_lock(&lock); //上锁
if判断条件
pthread_cond_wait(&cond, &lock); //阻塞等待条件产生,没有条件产生时阻塞,同时解锁,当条件产生时结束阻塞,再次上锁

pthread_cond_signal(&cond); //产生条件,不阻塞

pthread_cond_destroy(&cond); //销毁条件变量

注意: 必须保证让pthread_cond_wait先执行,pthread_cond_signal再产生条件

为什么pthread_cond_wait前要加互斥锁?
防止调用 pthread_cond_wait之前其他线程改变条件,从而发生错乱。

例子:赚钱和花钱
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

pthread_cond_t cond;
pthread_cond_t cond1;
pthread_mutex_t lock;
int money = 0; //保存手里剩余的钱数

//花钱,手里的钱达到100的时候才开始花钱,花到不到100再通知主线程赚钱
void *spend_money(void *arg)
{
while (1)
{
pthread_mutex_lock(&lock);

    if (money < 100)
        pthread_cond_wait(&cond, &lock);

    while (money >= 100) //手里的钱达到100的时候让他花钱
    {
        money -= 100;
        printf("money: %d billion\n", money);
    }

    if (money < 100)
        pthread_cond_signal(&cond1);

    pthread_mutex_unlock(&lock);
}

return NULL;

}

//手里的钱不到100的时候继续赚钱,赚够达到100的钱才通知从线程开始花钱
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_create(&tid, NULL, spend_money, NULL);
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);
pthread_cond_init(&cond1, NULL);

//赚钱
while (1)
{
    //sleep(1);
    pthread_mutex_lock(&lock);

    if (money >= 100)
        pthread_cond_wait(&cond1, &lock);
    scanf("%d", &money);
    if (money >= 100)
        pthread_cond_signal(&cond);
    pthread_mutex_unlock(&lock);
}

pthread_join(tid, NULL);

return 0;

}

面试题:linux线程间同步有几种方式?(中维世纪)
1.互斥锁(Mutex): 互斥算是一种特殊的同步。使用互斥锁可以确保同一时间只有一个线程可以访问共享资源。当一个线程进入临界区时,会锁定互斥锁,阻止其他线程进入该区域,直到该线程完成操作并释放锁。
2.信号量(Semaphore): 信号量用于控制对有限数量的资源的访问。它们是一种计数器,可以对其进行递增或递减操作。当信号量值为零时,线程必须等待,直到另一个线程递增该信号量。
3.条件变量(Condition Variable) : 条件变量用于在线程之间传递信号,以便某些线程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行

进程间通信IPC
InterProcess Communication

1.进程间通信方式
1). 早期的进程间通信:
无名管道(pipe)、有名管道(fifo)、信号(signal)
2).system V IPC:
共享内存(shared memory)、消息队列(message queue)、信号灯集(semaphore set)
3).BSD:
套接字(socket)

2.无名管道

2.1 特点
(1)只能用于具有亲缘关系的进程之间的通信
(2)半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。
(3)管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
(4)管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

2.2 函数接口
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端 fd[1]:写端
返回值:成功 0
失败 -1

#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
int fd[2];
char buf[65536] = “555555555”;

if (pipe(fd) < 0)
{
    perror("pipe err");
    return -1;
}
printf("%d %d\n", fd[0], fd[1]);

//读写,结构类似队列,先进先出
// write(fd[1], "hello", 5);
// read(fd[0], buf, 5);
// printf("%s\n", buf);

//1.当管道中无数据时,读阻塞
// read(fd[0],buf,32);
// printf("%s\n",buf);

//但是关闭写端就不一样了
//当管道中有数据,关闭写端可以读出数据。但是管道中无数据时,关闭写端,读操作会立即返回(0)。
//write(fd[1],"hello",5);
// close(fd[1]);
// read(fd[0],buf,5);
// printf("%s\n",buf);

//2.当管道中写满数据的时候,写阻塞,管道大小为64K
// write(fd[1], buf, 65536);
// printf("full!\n");
// write(fd[1], "6", 1);
// printf("after write!\n");

//3. 写满一次之后,当管道中至少有4K空间时(也就是再读4K)才可以继续写,否则写阻塞。
//先写满
// read(fd[0], buf, 4096); //换成4095就阻塞了,因为不到4K空间了
// write(fd[1], "h", 1);
// printf("after write!\n");

//4.当读端关闭的时候,向管道中写无意义,会导致管道破裂,进程会收到内核发送的SIGPIPE信号
close(fd[0]);
write(fd[1], "a", 1);
printf("read close!\n");

return 0;

}

用gdb调试r一下可以看到管道破裂信号:
gcc -g pipe.c
gdb a.out
r

2.3 注意事项
(1)当管道中无数据时,读操作会阻塞。
管道中有数据,将写端关闭,可以将数据读出。
管道中无数据,将写端关闭,读操作会立即返回。
(2)管道中装满(管道大小64K)数据写阻塞,一旦有4k空间,写继续
(3)只有在管道的读端存在时,向管道中写入数据才有意义。否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号 (通常Broken pipe错误)。

练习:父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入quit结束。
提示:不需要加同步机制, 因为pipe无数据时读会阻塞。
考虑:创建管道是在fork之前还是之后? 先pipe()再fork()
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc, char const *argv[])
{
pid_t pid;
int fd[2];
char buf[32] = “”;

if (pipe(fd) < 0)
{
    perror("pipe err");
    return -1;
}
printf("%d %d\n", fd[0], fd[1]);

if ((pid = fork()) < 0)
{
    perror("fork err");
    return -1;
}
else if (pid == 0)
{
    //循环输出
    while (1)
    {
        read(fd[0], buf, 32);
        if (strcmp(buf, "quit") == 0)
            break;
        printf("%s\n", buf);
    }
}
else
{
    //循环输入
    while (1)
    {
        scanf("%s", buf);
        write(fd[1], buf, 32); //把scanf写进buf的内容write进管道
        //或者write(fd[1], buf, strlen(buf)+1); //+1是为了最后多写一个'\0'
        if (strcmp(buf, "quit") == 0)
            break;
    }
}
wait(NULL);
return 0;

}

作业
1.吸收完善今天所学内容整理笔记,代码敲两遍以上截图发群里。
2.完善今天父子进程无名管道通信代码
3.练习:请在linux 利用c语言编程实现两个线程按照顺序依次输出”ABABABAB…" (信雅达)
例如a线程输出”A”之后b线程输出”B”,然后a线程输出“A”,再b线程输出”B”,之后往复循环。

信号量:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t sem;
sem_t sem1;

void *producer_thread(void *arg)
{
sleep(1); //让主线程先调度,申请信号量
while (1)
{
sem_wait(&sem1); //1
printf(“A”);
fflush(NULL);
sem_post(&sem);
sleep(1);
}
}

void *consumer_thread(void *arg)
{
sleep(1); //让主线程先调度,申请信号量
while (1)
{
sem_wait(&sem); //后打印,所以可以用sem_wait阻塞等待一下
printf("B ");
fflush(NULL);
sem_post(&sem1);
sleep(1);
}
}

int main(int argc, char const *argv[])
{
pthread_t consumer_tid, producer_tid;
if (pthread_create(&producer_tid, NULL, producer_thread, NULL))
{
perror(“create producer thread err”);
return -1;
}
if (pthread_create(&consumer_tid, NULL, consumer_thread, NULL))
{
perror(“create cunsumer thread err”);
return -1;
}

//信号量初始化
if (sem_init(&sem, 0, 0) != 0)
{
    perror("sem init err");
    return -1;
}

if (sem_init(&sem1, 0, 1) != 0)
{
    perror("sem init err");
    return -1;
}


pthread_join(producer_tid, NULL);
pthread_join(consumer_tid, NULL);

printf("main after sleep\n");
return 0;

}

条件变量:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t lock;
pthread_cond_t cond;

void *funA(void *arg)
{
while(1)
{
sleep(1);
pthread_mutex_lock(&lock);
printf(“A”);
fflush(NULL);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);
}

pthread_exit(NULL);

}
void *funB(void *arg)
{
while(1)
{
pthread_mutex_lock(&lock);
pthread_cond_wait(&cond, &lock);
printf("B ");
fflush(NULL);
pthread_mutex_unlock(&lock);
}
putchar(‘\n’);
pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
pthread_t tid1, tid2;
pthread_cond_init(&cond, NULL);

if (pthread_create(&tid1, NULL, funA, NULL) < 0)
{
    perror("tid1 err");
    return -1;
}
if (pthread_create(&tid2, NULL, funB, NULL) < 0)
{
    perror("tid1 err");
    return -1;
}

pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

return 0;

}

3.有名管道
3.1 特点
1)有名管道可以使互不相关的两个进程互相通信。
2)有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
3)进程通过文件IO来操作有名管道
4)有名管道遵循先进先出规则
5)不支持如lseek() 操作

3.2 函数接口
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
mode:权限
返回值:成功:0
失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)

注:函数只是在路径下创建管道文件,往管道中写的数据依然写在内核空间。
先创建有名管道,然后用文件IO操作:打开、读写和关闭。

3.3 注意事项
1)只写方式打开阻塞,一直到另一个进程把读打开
2)只读方式打开阻塞,一直到另一个进程把写打开
3)可读可写,如果管道中没有数据,读阻塞

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
char buf[32] = “”;
int fd;
if (mkfifo(“./fifo”, 0666) < 0)
{
if (errno == EEXIST) //判断错误号,如果是EEXIST则打印提示而不是报错退出
printf(“file exist\n”);
else
{
perror(“mkfifo err”);
return -1;
}
}
printf(“mkfifo succuss!\n”);

//打开文件
fd = open("fifo", O_RDWR);
if (fd < 0)
{
    perror("open err");
    return -1;
}

//读写文件
write(fd, "hello", 5);
read(fd, buf, 32);
printf("%s\n", buf);

return 0;

}

练习:通过两个进程实现cp功能。
./input srcfile
./output destfile
input.c 读源文件
//创建有名管道,打开管道文件拿到文件描述符
//循环读源文件放到buf, 把buf中保存内容写到管道中
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
char buf[32] = “”;
int fd, fd_file;
ssize_t s;
if (mkfifo(“./fifo”, 0666) < 0)
{
if (errno == EEXIST)
printf(“file exist\n”);
else
{
perror(“mkfifo err”);
return -1;
}
}
printf(“mkfifo succuss!\n”);

//打开文件
fd = open("fifo", O_WRONLY);
fd_file = open(argv[1], O_RDONLY);

//读源文件,写进管道
while (1)
{
    //从源文件中读数据到buf
    s = read(fd_file, buf, 32);
    if (s == 0)
        break;
    //把buf中数据写到管道
    write(fd, buf, s);
}

close(fd);
close(fd_file);
return 0;

}

output.c 写目标文件
//创建有名管道,打开管道文件拿到文件描述符
//循环读管道,写到目标文件
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
char buf[32] = “”;
int fd, fd_file;
ssize_t s;
if (mkfifo(“./fifo”, 0666) < 0)
{
if (errno == EEXIST)
printf(“file exist\n”);
else
{
perror(“mkfifo err”);
return -1;
}
}
printf(“mkfifo succuss!\n”);

//打开文件
fd = open("fifo", O_RDONLY);
fd_file = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666);

//读管道,写进目标文件
while (1)
{
    //把管道中内容读到buf
    s = read(fd,buf,32);
    if (s == 0)
        break;
    //把buf中内容写到目标文件
    write(fd_file,buf,s);
}

close(fd);
close(fd_file);
return 0;

}

编译:
gcc input.c -o input
gcc output.c -o output
执行:

3.4 有名管道和无名管道的区别

无名管道	有名管道

使用场景 具有亲缘关系的进程 不相关两个进程可以使用
特点 半双工通信方式
固定读端fd[0]写端fd[1]
可做一个特殊的文件
通过文件IO操作 在文件系统中会存在管道文件,读写的数据放在内核空间
通过文件IO进行操作
函数 pipe()
直接read/write mkfifo()
先打开管道文件open,再读写read/write
读写特性 当管道中无数据时读阻塞(关闭写端无数据,读直接返回)
当管道写满时写阻塞,至少有4K空间才可以继续写
读端关闭,往管道中写会管道破裂 只读方式打开会阻塞,直到另一个进程把写打开
只写方式打开会阻塞,直到另一个进程把读打开
可读可写,管道中无数据读阻塞

4.信号
kill -l: 显示系统中的信号
kill -num PID:给某个进程发送信号
4.1 概念
1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

信号的生命周期:

4.2 信号的响应方式
1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
3)执行缺省操作:Linux对每种信号都规定了默认操作

4.3 信号种类
SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程
SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件
SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。
SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。
SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination
SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。
SIGCONT(18):继续执行信号,用于恢复先前停止的进程。
SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。
SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。

4.4 函数接口
4.4.1 信号发送和挂起
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
sig:要发送的信号
返回值:成功 0
失败 -1

int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0
失败 -1
相当于:kill(getpid(), sig);

int pause(void);
功能:用于将调用进程挂起。

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

int main(int argc, char const *argv[])
{
// kill(getpid(),SIGKILL); //给指定的进程发送信号,此例子给当前进程发送强制杀死信号
// raise(SIGKILL); //给自己发送强制杀死信号
// while(1);
pause(); //将进程挂起,直到收到信号结束挂起。作用和死循环类似,但是不占用CPU。

return 0;

}

4.4.2 定时器
man 2 alarm
unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器。当定时器指定的时间到了时,它就向进程发送SIGALARM信号。
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。
取消闹钟操作:alarm(0),返回值旧闹钟剩余的秒数。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
printf(“%d\n”,alarm(10)); //设定闹钟10秒后响铃,第一次设闹钟返回0
sleep(1);
printf(“%d\n”,alarm(3)); //重设闹钟3秒后响铃,上个闹钟还剩余9秒
//以最后一次闹钟为准

pause();    //3秒后收到SIGALARM信号结束进程

return 0;

}

注意:如果不对SIGALARM信号做处理,系统对SIGALRM闹钟信号的默认处理方式时结束进程。

4.4.3 信号处理函数signal()
man 2 signal
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
handler:信号处理方式
SIG_IGN:忽略信号 (忽略 ignore)
SIG_DFL:执行默认操作 (默认 default)
handler:捕捉信号 (handler为函数名,可以自定义)
void handler(int sig){} //函数名可以自定义, 参数为要处理的信号
返回值:成功:设置之前的信号处理方式
失败:-1

#include <stdio.h>

//给普通数据类型int重命名
typedef int size4;

//给指针类型int* 重命名
typedef int *int_p;

//给数组类型int [10]重命名
typedef int intArr10[10];

//给函数指针void (*)()重命名
typedef void (*fun_p)();

void fun()
{
printf(“fun\n”);
}

int main(int argc, char const argv[])
{
size4 a = 10; //相当于int a
int_p p = &a; //相当于int
p
intArr10 arr = {1, 2, 3}; //相当于int arr[10]
fun_p fp = fun; //相当于void (*fp)()=fun;

printf("%d\n", *p);
printf("%d\n", arr[0]);
fp();

return 0;

}

总而言之,定义变量的变量名写哪里用typedef重命名的重命名就写哪里。使用的时候重命名定义变量的格式可以为:新名字 变量名;

例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig)
{
printf(“ctrl C\n”);
}

int main(int argc, char const *argv[])
{
//signal(SIGINT,SIG_IGN); //忽略信号
signal(SIGINT, SIG_DFL); //默认处理信号
signal(SIGINT, handler); //捕捉信号,比较常用的处理方式
while (1);
return 0;
}

练习:用信号的知识实现司机和售票员问题。
1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let’s gogogo)
2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4)司机等待售票员下车,之后司机再下车。
分析:司机(父进程)、售票员(子进程)
售票员:
捕捉:SIGINT SIGQUIT SIGUSR1
忽略:SIGTSTP

司机:
捕捉:SIGUSR1 SIGUSR2 SIGTSTP
忽略:SIGINT SIGQUIT

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

pid_t pid;

void saler(int sig)
{
if (sig == SIGINT)
kill(getppid(), SIGUSR1);
else if (sig == SIGQUIT)
kill(getppid(), SIGUSR2);
else if (sig == SIGUSR1)
{
printf(“[saler] pls get off the bus!\n”);
exit(0);
}
}

void driver(int sig)
{
if (sig == SIGUSR1)
printf(“[driver] lets gogogo!\n”);
else if (sig == SIGUSR2)
printf(“[driver] stop the bus!\n”);
else if (sig == SIGTSTP)
{
kill(pid, SIGUSR1);
wait(NULL); //等待子进程结束以后回收资源再结束
exit(0);
}
}

int main(int argc, char const *argv[])
{
pid = fork();
if (pid < 0)
{
perror(“fork err”);
return -1;
}
else if (pid == 0) //售票员
{
printf(“hi,i am saler!~\n”);
signal(SIGINT, saler);
signal(SIGQUIT, saler);
signal(SIGUSR1, saler);
signal(SIGTSTP, SIG_IGN);
}
else //司机
{
printf(“hi,i am driver!\n”);
signal(SIGUSR1, driver);
signal(SIGUSR2, driver);
signal(SIGTSTP, driver);
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
}

while (1)
    pause(); //不能只发送一个信号就结束挂起了,所以可以循环挂起,不占用CPU

return 0;

}

5.共享内存
5.1 特点
1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程
将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
3)由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等

5.2 步骤
(1)创建key值
(2)创建或打开共享内存
(3)映射共享内存到用户空间
(4)撤销映射
(5)删除共享内存

5.3 函数接口
man 3 ftok
key_t ftok(const char *pathname, int proj_id);
功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存
参数:
Pathname:已经存在的可访问文件的名字
Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
失败:-1

int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
key 键值
size 共享内存的大小
shmflg IPC_CREAT|IPC_EXCL|0777
返回值:成功 shmid
出错 -1
//当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。若已有该块共享内存,则返回-1。

void *shmat(int shmid,const void *shmaddr,int shmflg); //attaches
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
shmid 共享内存的id号
shmaddr 一般为NULL,表示由系统自动完成映射
如果不为NULL,那么有用户指定
shmflg:SHM_RDONLY就是对该共享内存只进行读操作
0 可读可写
返回值:成功:完成映射后的地址,
出错:-1(地址)
用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)

int shmdt(const void *shmaddr); //detaches
功能:取消映射
参数:要取消的地址
返回值:成功0
失败的-1

int shmctl(int shmid,int cmd,struct shmid_ds *buf); //control
功能:(删除共享内存),对共享内存进行各种操作
参数:
shmid 共享内存的id号
cmd IPC_STAT 获得shmid属性信息,存放在第三参数
IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
IPC_RMID:删除共享内存,此时第三个参数为NULL即可
buf shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null
返回:成功0
失败-1
用法:shmctl(shmid,IPC_RMID,NULL);

#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
key_t key;
int shmid;
char *p = NULL;
key = ftok(“./shm.c”, ‘d’);
if (key < 0)
{
perror(“ftok err”);
return -1;
}
printf(“key: %#x\n”, key); //十六进制打印

//打开或者创建共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存
if (shmid <= 0)
{
    //避免如果共享内存已存在会报错shmget err: File exists,因为已存在不算错误
    if (errno == EEXIST)                //如果已存在则直接打开共享内存
        shmid = shmget(key, 128, 0666); //直接打开共享内存
    else
    {
        perror("shmget err");
        return -1;
    }
}
printf("shmid: %d\n", shmid);

//映射共享内存
p = (char *)shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
    perror("shma err");
    return -1;
}

//操作共享内存
scanf("%s", p);
printf("%s\n", p);

//取消映射
shmdt(p);

//删除共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;

}

5.4 命令
ipcs -m: 查看系统中的共享内存
ipcrm -m shmid:删除共享内存 (可能不能直接删掉还在进程使用的共享内存,需要先杀死进程)

例子:启动两个进程进行通信,通过相同的key打开相同共享内存从而操纵同一块共享内存,一个进程输入,一个进程输出:

编译执行启动两个进程实现通信:
./in实现往共享内存中输入
./out 实现从共享内存中输出

作业:
1.复习吸收当天内容,整理笔记截图发群里,完善今天代码。敲两遍发群里。
2.尝试使用共享内存多进程通信截图发群里
3.有能力的可以尝试以下练习(不强制,明天讲):
两个进程实现通信,一个进程循环从终端输入,另一个进程循环打印,当输入quit时结束
这两个标志在两个进程里,是不共享的,所以为了共享标志位可以和buf封装到一个结构体里作为共享内存。
struct msg
{
int flag;
char buf[32];
};

input.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>

typedef struct msg
{
int flag;
char buf[32];
} msg_t;

int main(int argc, char const *argv[])
{
key_t key;
int shmid;
msg_t *p = NULL;
key = ftok(“./shm.c”, ‘d’);
if (key < 0)
{
perror(“ftok err”);
return -1;
}
printf(“key: %#x\n”, key); //十六进制打印

//打开或者创建共享内存
shmid = shmget(key, sizeof(msg_t), IPC_CREAT | IPC_EXCL | 0666); //创建共享内存
if (shmid <= 0)
{
    //避免如果共享内存已存在会报错shmget err: File exists,因为已存在不算错误
    if (errno == EEXIST)                          //如果已存在则直接打开共享内存
        shmid = shmget(key, sizeof(msg_t), 0666); //直接打开共享内存
    else
    {
        perror("shmget err");
        return -1;
    }
}
printf("shmid: %d\n", shmid);

//映射共享内存
p = (msg_t *)shmat(shmid, NULL, 0);
if (p == (msg_t *)-1)
{
    perror("shma err");
    return -1;
}

p->flag = 0;
//操作共享内存
while (1)
{
    if (p->flag == 0)
    {
        scanf("%s", p->buf);
        p->flag = 1;
    }
    if (strcmp(p->buf, "quit") == 0)
        break;
}

//取消映射
shmdt(p);

//删除共享内存
shmctl(shmid, IPC_RMID, NULL);

return 0;

}

output.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>

typedef struct msg
{
int flag;
char buf[32];
} msg_t;

int main(int argc, char const *argv[])
{
key_t key;
int shmid;
msg_t *p = NULL;
key = ftok(“./shm.c”, ‘d’);
if (key < 0)
{
perror(“ftok err”);
return -1;
}
printf(“key: %#x\n”, key); //十六进制打印

//打开或者创建共享内存
shmid = shmget(key, sizeof(msg_t), IPC_CREAT | IPC_EXCL | 0666); //创建共享内存
if (shmid <= 0)
{
    //避免如果共享内存已存在会报错shmget err: File exists,因为已存在不算错误
    if (errno == EEXIST)                          //如果已存在则直接打开共享内存
        shmid = shmget(key, sizeof(msg_t), 0666); //直接打开共享内存
    else
    {
        perror("shmget err");
        return -1;
    }
}
printf("shmid: %d\n", shmid);

//映射共享内存
p = (msg_t *)shmat(shmid, NULL, 0);
if (p == (msg_t *)-1)
{
    perror("shma err");
    return -1;
}

p->flag = 0;
//操作共享内存
while (1)
{
    if (strcmp(p->buf, "quit") == 0)
        break;
    if (p->flag == 1)
    {
        printf("%s\n", p->buf);
        p->flag = 0;
    }
}

//取消映射
shmdt(p);

//删除共享内存
shmctl(shmid, IPC_RMID, NULL);

return 0;

}

编译:
gcc input.c -o in
gcc output.c -o out

6.信号灯集
线程:全局变量,同步通过信号量
初始化:sem_init(&sem,0,0);
申请资源:sem_wait(&sem); P操作 -1
释放资源:sem_post(&sem); V操作 +1

6.1 特点
信号灯(semaphore),也叫信号量,信号灯集是一个信号灯的集合。它是不同进程间或一个给定进程内部不同线程间同步的机制;
而Posix信号灯指的是单个计数信号灯:无名信号灯、有名信号灯。(咱们学的是无名信号灯)
System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。
通过信号灯集实现共享内存的同步操作

6.2 步骤
(1)创建key值:ftok
(2)创建或打开信号灯集:semget
(3)初始化信号灯:semctl
(4)PV操作:semop
(5)删除信号灯集:semctl

6.3 命令
ipcs -s:查看信号灯集
ipcrm -s semid:删除指定信号灯集

注意: 有时候可能会创建失败,或者semid为0,所以创建完了之后可以用命令查看,删除了重新创建就可以了。

6.4 函数接口
int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
nsems:信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限,通常为IPC_CREAT|IPC_EXCL|0666
返回值:成功:信号灯集ID
失败:-1

int semctl ( int semid, int semnum, int cmd…/union semun arg/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
semnum: 要操作的集合中的信号灯编号,信号灯编号从0开始
cmd:
GETVAL:获取信号灯的值,返回值是获得值
SETVAL:设置信号灯的值,需要用到第四个参数:共用体
IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
失败 -1

用法:

  1. 初始化信号灯集:
    需要自定义共用体
    union semun{
    int val;
    };
    union semun mysemun;
    mysemun.val=10;
    semctl(semid,0,SETVAL,mysemun);

  2. 获取信号灯值: semctl(semid,0,GETVAL)的返回值

  3. 删除信号灯集:semctl(semid,0,IPC_RMID);

int semop ( int semid, struct sembuf *opsptr, size_t nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
opsptr:操作方式
nops: 要操作的信号灯的个数 1个
返回值:成功 :0
失败:-1
struct sembuf {
short sem_num; // 要操作的信号灯的编号
short sem_op; // 0 : 等待,直到信号灯的值变成0
// 1 : 释放资源,V操作
// -1 : 申请资源,P操作
short sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};
用法:
struct sembuf mysembuf;
申请资源P操作:
mysembuf.sem_num=0;
mysembuf.sem_op=-1;
mysembuf.sem_flg=0;
semop(semid,&mysembuf,1);

释放资源V操作:
mysembuf.sem_num=1;
mysembuf.sem_op=1;
mysembuf.sem_flg=0;
semop(semid,&mysembuf,1);

第一次有可能创建成功,删除重新执行就可以了。

使用信号灯:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>

union semun {
int val;
};

int main(int argc, char const *argv[])
{
key_t key;
int semid;

if ((key = ftok(".", 97)) < 0)
{
    perror("ftok err");
    return -1;
}
printf("key = %d\n", key);

//创建或打开信号灯集
semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
    if (errno == EEXIST)
        semid = semget(key, 2, 0666);
    else
    {
        perror("semget err");
        return -1;
    }
}
else //确保对信号灯集的信号灯只初始化创建完之后的第一次,下一次直接打开还继续获得上一次的值。如果要修改初值需要删除然后重新创建。
{
    union semun sem;
    sem.val = 10;
    semctl(semid, 0, SETVAL, sem); //对0号信号灯初始化为10

    sem.val = 0;
    semctl(semid, 1, SETVAL, sem); //对1号信号灯初始化为0
}
printf("semid: %d\n", semid);

//获取信号灯初值
printf("%d\n", semctl(semid, 0, GETVAL));
printf("%d\n", semctl(semid, 1, GETVAL));

//PV操作
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = -1; //申请资源 P操作 -1
buf.sem_flg = 0;
semop(semid, &buf, 1);

buf.sem_num = 1;
buf.sem_op = 1; // 释放资源 V操作 +1
buf.sem_flg = 0;
semop(semid, &buf, 1);

//获取信号灯值
printf("%d\n", semctl(semid, 0, GETVAL));
printf("%d\n", semctl(semid, 1, GETVAL));

return 0;

}

函数操作:
input.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

union semun {
int val;
};

//初始化
void initsem(int semid, int num, int val)
{
union semun sem;
sem.val = val;
semctl(semid, num, SETVAL, sem); //对num号信号灯初始化为val
}

//PV操作
void sempv(int semid, int num, int op)
{
struct sembuf buf;
buf.sem_num = num;
buf.sem_op = op;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}

int main(int argc, char const *argv[])
{
key_t key;
int semid;
int shmid;

if ((key = ftok(".", 97)) < 0)
{
    perror("ftok err");
    return -1;
}
printf("key = %d\n", key);

//创建或打开共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid <= 0)
{
    if (errno == EEXIST)
        shmid = shmget(key, 128, 0666);
    else
    {
        perror("shmget err");
        return -1;
    }
}
printf("shmid: %d\n", shmid);

//创建或打开信号灯集
semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
    if (errno == EEXIST)
        semid = semget(key, 2, 0666);
    else
    {
        perror("semget err");
        return -1;
    }
}
else //确保对信号灯集的信号灯只初始化创建完之后的第一次,下一次直接打开还继续获得上一次的值。如果要修改初值需要删除然后重新创建。
{
    initsem(semid, 0, 0); 
    initsem(semid, 1, 1);
}
printf("semid: %d\n", semid);

//共享内存映射
char *p = shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
    perror("shmat err");
    return -1;
}

while (1)
{
    //申请资源:信号灯1
    sempv(semid,1,-1);
    scanf("%s", p);
    //释放资源:信号灯0
    sempv(semid, 0, 1);
    if (strcmp(p, "quit") == 0)
        break;
}

// //删除信号灯
// semctl(semid,0,IPC_RMID); //指定任意一个信号灯就会删除整个信号灯集

return 0;

}

output.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

union semun {
int val;
};

//初始化
void initsem(int semid, int num, int val)
{
union semun sem;
sem.val = val;
semctl(semid, num, SETVAL, sem); //对num号信号灯初始化为val
}

//PV操作
void sempv(int semid, int num, int op)
{
struct sembuf buf;
buf.sem_num = num;
buf.sem_op = op;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}

int main(int argc, char const *argv[])
{
key_t key;
int semid;
int shmid;

if ((key = ftok(".", 97)) < 0)
{
    perror("ftok err");
    return -1;
}
printf("key = %d\n", key);

//创建或打开共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid <= 0)
{
    if (errno == EEXIST)
        shmid = shmget(key, 128, 0666);
    else
    {
        perror("shmget err");
        return -1;
    }
}
printf("shmid: %d\n", shmid);

//创建或打开信号灯集
semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
    if (errno == EEXIST)
        semid = semget(key, 2, 0666);
    else
    {
        perror("semget err");
        return -1;
    }
}
else //确保对信号灯集的信号灯只初始化创建完之后的第一次,下一次直接打开还继续获得上一次的值。如果要修改初值需要删除然后重新创建。
{
    initsem(semid, 0, 0);
    initsem(semid, 1, 1);
}
printf("semid: %d\n", semid);

//共享内存映射
char *p = shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
    perror("shmat err");
    return -1;
}

while (1)
{
    //申请资源:信号灯0
    sempv(semid, 0, -1);
    if (strcmp(p, "quit") == 0)
        break;
    printf("%s\n", p);
    //释放资源:信号灯1
    sempv(semid,1,1);
}

// //删除信号灯
// semctl(semid,0,IPC_RMID); //指定任意一个信号灯就会删除整个信号灯集

return 0;

}

编译执行:

7.消息队列 message queue
传统:无名管道、有名管道、信号
system V: 共享内存、信号灯集、消息队列

按消息的类型添加或读取消息
队列原则

7.1 特点
消息队列是IPC对象(活动在内核级别的一种进程间通信的工具)的一种
一个消息队列由一个标识符 (即队列ID)来标识
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
消息队列可以按照类型(自己设一个值作为类型)来发送/接收消息

7.2 步骤
(1)创建key值: ftok()
(2)创建或打开消息队列: msgget()
(3)添加消息:按照类型把消息添加到已经打开的队列末尾: msgsnd()
(4)读取消息:可以按照类型把消息从队列中取走: msgrcv()
(5)删除消息队列:msgctl()

7.3 操作命令
ipcs -q: 查看消息队列
ipcrm -q msgid: 删除消息队列

7.4 函数接口
int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数: key值
flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
失败:-1

int msgsnd(int msqid, const void *msgp, size_t size, int flag);
功能:添加消息
参数:msqid:消息队列的ID
msgp:指向消息的指针。常用消息结构msgbuf如下:
struct msgbuf{
long mtype; //消息类型
char mtext[N]}; //消息正文
size:发送的消息正文的字节数
flag:IPC_NOWAIT消息没有发送完成函数也会立即返回
0:直到发送完成函数才返回
返回值:成功:0
失败:-1
使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。

int msgrcv(int msgid, void* msgp, size_t size, long msgtype, int flag);
功能:读取消息
参数:msgid:消息队列的ID
msgp:存放读取消息的空间
size:接受的消息正文的字节数(sizeof(msgp)-sizeof(long))
msgtype:
0:接收消息队列中第一个消息。
大于0:接收消息队列中第一个类型为msgtyp的消息.
小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
flag:
0:若无消息函数会一直阻塞
IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
失败:-1

int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,删除消息队列
参数:msqid:消息队列的队列ID
cmd:
IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
IPC_SET:设置消息队列的属性。这个值取自buf参数。
IPC_RMID:从系统中删除消息队列。
buf:消息队列缓冲区
返回值:成功:0
失败:-1
用法:msgctl(msgid, IPC_RMID, NULL)

创建或打开消息队列:如果开始创建不成功则删除重新执行就可以了

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <errno.h>

struct msgbuf
{
long type; //必须有且是第一个,代表消息的类型,值必须>0
int num; //除了第一个消息类型其他都是消息正文,随便定义
char ch;
};

int main(int argc, char const *argv[])
{
key_t key;
int msgid;
key = ftok(“.”, ‘d’);
if (key <= 0)
{
perror(“ftok err”);
return -1;
}
printf(“%#x\n”, key);

//打开或创建消息队列
msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid <= 0)
{
    if (errno == EEXIST)
        msgid = msgget(key, 0666);
    else
    {
        perror("msgget err");
        return -1;
    }
}
printf("msgid: %d\n", msgid);

//添加消息
struct msgbuf msg;
msg.type = 10;
msg.num = 100;
msg.ch = 'a';
msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0); //阻塞:直到发完消息才返回

msg.type = 20;
msg.num = 200;
msg.ch = 'b';
msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

//读取消息
struct msgbuf m;
msgrcv(msgid, &m, sizeof(m) - sizeof(long), 20, 0); //读取消息队列中类型为20的第一个消息
printf("%d %c\n", m.num, m.ch);

// msgrcv(msgid, &m, sizeof(m) - sizeof(long), 10, 0); //读取消息队列中类型为10的第一个消息
// printf("%d %c\n", m.num, m.ch);

msgrcv(msgid, &m, sizeof(m) - sizeof(long), 0, 0); //读取消息队列中第一个消息
printf("%d %c\n", m.num, m.ch);

//删除消息队列
msgctl(msgid, IPC_RMID, NULL);

return 0;

}

两个进程收发:

总结: IO进程
IO部分
1.标准IO: 概念、特点、函数(fopen/freopen、flose、fgetc/fputc、fgets/fputs、fread/fwrite、rewind/fseek/ftell)
练习:cat、wc -l、head、cp、计算时间
2.文件IO:概念、特点、函数(open、close、read、write、lseek)
练习:cp
3.文件属性获取:stat
练习:ls -l
4.目录操作:opendir、closedir、readdir、chdir
5.库: 概念、分类、静态库和动态库的区别、步骤、gcc 选项 -L -l -I(大写i)

进程部分:
进程基础:进程和程序的区别、特点、三个段、状态、函数(fork、wait/waitpid、exit/_exit、getpid/getppid、execl函数族)、守护进程(特点、步骤:fork、setsid、chdir(“”)、umask(0)、close)
练习:父子进程cp
线程:和进程的区别、函数(pthread_create、pthread_join/phtread_detach、pthread_exit)、
同步(信号量、函数:sem_init、sem_wait、sem_post)、
互斥(互斥锁、函数:pthread_mutex_init、pthread_mutex_lock、pthread_mutex_unlock、pthread_mutex_destroy)、
条件变量(和互斥锁代培实现同步、函数:pthread_cond_init、pthread_cond_wait、pthread_cond_signal、pthread_cond_destroy)

进程间通信:
1.无名管道:特点、pipe打开无名管道、步骤(read\write)
2.有名管道:特点、mkfifo打开有名管道、步骤(open、read\write)
3.信号: 特点(异步)、信号处理方式、函数(kill、raise、pause、alarm、signal)
4.共享内存:特点、步骤、函数
5.信号灯集:特点、步骤、函数
6.消息队列:特点、步骤、函数

面试题
1.标准IO和文件IO的区别是什么?
2.静态库和动态库的区别
3.什么是孤儿进程?什么是僵尸进程?
4.什么是守护进程?创建步骤?
5.进程和线程的区别?
6.线程的同步和互斥?什么是异步?什么是阻塞? 什么是非阻塞?
7.进程间通讯方式有哪些?效率最高是哪种?
8.无名管道和有名管道的区别?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值