ZYZ
记录一次Maven问题解决

清除IDEA缓存后,删除.m2文件后依旧无法解析依赖,在setting.xml中新增镜像后解决问题

image-20211207094410113

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>*,!jeecg,!jeecg-snapshots,!getui-nexus</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>

<mirror>
<id>nexus-aliyun</id>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
KafKa学习路上遇到的问题

Connection to node -1 could not be established. Broker may not be available.

卡夫卡配置文件中监听地址存在问题,未设置IP

SpringBoot学习路上遇到的问题

数据库无法连接

  1. 配置文件异常 例如:

    1
    2
    3
    4
    5
    6
    7
    spring:
    datasource:
    driver-class-name: com.mysql.jdbc.Driver
    Url: jdbc:mysql://:/WXDinner?
    username:
    password:

    但是下方的方式可以使用

    1
    2
    3
    4
    spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://:/WXDinner?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false
    spring.datasource.username=
    spring.datasource.password=
  2. 没装连接用的jar包

SpringBoot测试

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleTest {

@Autowired
private UserMapper userMapper;

@Test
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List<User> userList = userMapper.selectList(null);
Assert.assertEquals(5, userList.size());
userList.forEach(System.out::println);
}

}

Service错误

未使用注释@Service,若使用serviceimp则应在impl中标注@Service

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)

mapper.java与mapper.xml文件名应相同

1
mapper-locations未配置正确
MyBatis学习路上遇到的问题

mybatis Plus 多表联合查询

//实体类package com.sk.skkill.entity;

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.List;


@TableName("orders")
@Data
public class Order implements Serializable
{
public static final long serialVersionUID =1L;
private String id;
private String orderName;
private Date createTime;
private Date updateTime;
private String userID;

@TableField(exist = false)
private List<Users> listUsers;


public Order(){

}

public Order(String id, String orderName) {
this.id = id;
this.orderName = orderName;
}


}



//dao层
package com.sk.skkill.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.sk.skkill.entity.Order;
import com.sun.javafx.collections.MappingChange;
import org.apache.ibatis.annotations.Select;

import java.util.List;
import java.util.Map;

public interface OrderMapper extends BaseMapper<Order>
{
List<Order> selectOrder();
int addOrder(Order order);

//多表联合查询 按条件orderID
@Select("select t1.*,t2.user_name,t2.nick_name from orders t1 LEFT JOIN users t2 ON t1.user_id =t2.id WHERE t1.user_id= #{id}")
List<Map<String,Object>> orderUserList(Page<Map<String,Object>> page,String id);

}

//service层
package com.sk.skkill.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sk.skkill.entity.Order;

import java.util.List;
import java.util.Map;

public interface OrderService extends IService<Order>
{
List<Order> selectOrder();
int addOrder(Order order);
// List<Map<String,Object>> orderUserList(Page<Map<String,Object>> page, String id);
Page<Map<String,Object>> selectListPage(int current,int number,String id);
}
//serviceImpl层
package com.sk.skkill.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sk.skkill.entity.Order;
import com.sk.skkill.mapper.OrderMapper;
import com.sk.skkill.service.OrderService;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService
{

@Override
public List<Order> selectOrder() {
return baseMapper.selectList(null);
}

@Override
public int addOrder(Order order) {
return baseMapper.insert(order);
}



@Override
public Page<Map<String, Object>> selectListPage(int current, int number,String id) {
//新建分页
Page<Map<String,Object>> page =new Page<Map<String,Object>>(current,number);
//返回结果
return page.setRecords(this.baseMapper.orderUserList(page,id));
}


}
//controller层
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
package com.sk.skkill.controller;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.sk.skkill.entity.Order;
import com.sk.skkill.service.impl.OrderServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("order")
public class OrderController
{
@Autowired
private OrderServiceImpl service;

@RequestMapping("selectOrder")
public List<Order> selectOrder()
{
return service.selectOrder();
}
@RequestMapping("addOrder")
public int addOrder(Order order){
order=new Order("FGGG","蒙牛MILK");
return service.addOrder(order);

}
@RequestMapping("selectListPage")
public List<Map<String,Object>> selectListPage(String id)
{
Page<Map<String, Object>> page = service.selectListPage(1, 2,id);
return page.getRecords();
}


}

MapperScan配置问题

在Application中著名包名,例

1
@MapperScan("com.InterviewPreparation.learning.MyBatisPlus.mapper")

写一个配置类,例

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

/**
* @author ZYZ
* @date 2021/5/13 20:40
* @Description
*/
@Configuration
@MapperScan(value = {"com.InterviewPreparation.learning.MyBatisPlus.mapper*"})
public class MybatisPlusConfig {

}

配置文件中表示

1
mapper-locations: classpath*:org/jeecg/modules/**/xml/*Mapper.xml

代码生成器使用

CRUD 接口

#Service CRUD 接口

说明:

  • 通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper条件构造器

#Save

1
2
3
4
5
6
// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)
boolean saveBatch(Collection<T> entityList, int batchSize);
#参数说明
类型 参数名 描述
T entity 实体对象
Collection entityList 实体对象集合
int batchSize 插入批次数量

#SaveOrUpdate

1
2
3
4
5
6
7
8
// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);
#参数说明
类型 参数名 描述
T entity 实体对象
Wrapper updateWrapper 实体对象封装操作类 UpdateWrapper
Collection entityList 实体对象集合
int batchSize 插入批次数量

#Remove

1
2
3
4
5
6
7
8
// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);
#参数说明
类型 参数名 描述
Wrapper queryWrapper 实体包装类 QueryWrapper
Serializable id 主键ID
Map<String, Object> columnMap 表字段 map 对象
Collection<? extends Serializable> idList 主键ID列表

#Update

1
2
3
4
5
6
7
8
9
10
// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);
#参数说明
类型 参数名 描述
Wrapper updateWrapper 实体对象封装操作类 UpdateWrapper
T entity 实体对象
Collection entityList 实体对象集合
int batchSize 更新批次数量

#Get

1
2
3
4
5
6
7
8
9
10
// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
#参数说明
类型 参数名 描述
Serializable id 主键ID
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper
boolean throwEx 有多个 result 是否抛出异常
T entity 实体对象
Function<? super Object, V> mapper 转换函数

#List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
#参数说明
类型 参数名 描述
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper
Collection<? extends Serializable> idList 主键ID列表
Map<?String, Object> columnMap 表字段 map 对象
Function<? super Object, V> mapper 转换函数

#Page

1
2
3
4
5
6
7
8
// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);
#参数说明
类型 参数名 描述
IPage page 翻页对象
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper

#Count

1
2
3
4
// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);
#参数说明
类型 参数名 描述
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper

#Chain

#query

1
2
3
4
5
6
7
8
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery();

// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();

#update

1
2
3
4
5
6
7
8
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin
LambdaUpdateChainWrapper<T> lambdaUpdate();

// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);

#Mapper CRUD 接口

说明:

  • 通用 CRUD 封装BaseMapper (opens new window)接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器
  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus 不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper条件构造器

#Insert

1
2
// 插入一条记录
int insert(T entity);
#参数说明
类型 参数名 描述
T entity 实体对象

#Delete

1
2
3
4
5
6
7
8
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
#参数说明
类型 参数名 描述
Wrapper wrapper 实体对象封装操作类(可以为 null)
Collection<? extends Serializable> idList 主键ID列表(不能为 null 以及 empty)
Serializable id 主键ID
Map<String, Object> columnMap 表字段 map 对象

#Update

1
2
3
4
// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);
#参数说明
类型 参数名 描述
T entity 实体对象 (set 条件值,可为 null)
Wrapper updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)

#Select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
#参数说明
类型 参数名 描述
Serializable id 主键ID
Wrapper queryWrapper 实体对象封装操作类(可以为 null)
Collection<? extends Serializable> idList 主键ID列表(不能为 null 以及 empty)
Map<String, Object> columnMap 表字段 map 对象
IPage page 分页查询条件(可以为 RowBounds.DEFAULT)

#mapper 层 选装件

说明:

选装件位于 com.baomidou.mybatisplus.extension.injector.methods 包下 需要配合Sql 注入器使用,案例(opens new window)
使用详细见源码注释(opens new window)

#AlwaysUpdateSomeColumnById(opens new window)

1
int alwaysUpdateSomeColumnById(T entity);

#insertBatchSomeColumn(opens new window)

1
int insertBatchSomeColumn(List<T> entityList);

#deleteByIdWithFill(opens new window)

1
int deleteByIdWithFill(T entity);

#条件构造器

说明:

  • 以下出现的第一个入参boolean condition表示该条件是否加入最后生成的sql中,例如:query.like(StringUtils.isNotBlank(name), Entity::getName, name) .eq(age!=null && age >= 0, Entity::getAge, age)
  • 以下代码块内的多个方法均为从上往下补全个别boolean类型的入参,默认为true
  • 以下出现的泛型Param均为Wrapper的子类实例(均具有AbstractWrapper的所有方法)
  • 以下方法在入参中出现的R为泛型,在普通wrapper中是String,在LambdaWrapper中是函数(例:Entity::getId,Entity为实体类,getId为字段idgetMethod)
  • 以下方法入参中的R column均表示数据库字段,当R具体类型为String时则为数据库字段名(字段名是数据库关键字的自己用转义符包裹!)!而不是实体类数据字段名!!!,另当R具体类型为SFunction时项目runtime不支持eclipse自家的编译器!!!
  • 以下举例均为使用普通wrapper,入参为MapList的均以json形式表现!
  • 使用中如果入参的Map或者List,则不会加入最后生成的sql中!!!
  • 有任何疑问就点开源码看,看不懂函数点击我学习新知识(opens new window)

警告:

不支持以及不赞成在 RPC 调用中把 Wrapper 进行传输

  1. wrapper 很重
  2. 传输 wrapper 可以类比为你的 controller 用 map 接收值(开发一时爽,维护火葬场)
  3. 正确的 RPC 调用姿势是写一个 DTO 进行传输,被调用方再根据 DTO 执行相应的操作
  4. 我们拒绝接受任何关于 RPC 传输 Wrapper 报错相关的 issue 甚至 pr

#AbstractWrapper

说明:

QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的父类
用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件
注意: entity 生成的 where 条件与 使用各个 api 生成的 where 条件没有任何关联行为

#allEq

1
2
3
allEq(Map<R, V> params)
allEq(Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, Map<R, V> params, boolean null2IsNull)

个别参数说明:

params : key为数据库字段名,value为字段值
null2IsNull : 为true则在mapvaluenull时调用 isNull 方法,为false时则忽略valuenull

  • 例1: allEq({id:1,name:"老王",age:null})—>id = 1 and name = '老王' and age is null
  • 例2: allEq({id:1,name:"老王",age:null}, false)—>id = 1 and name = '老王'
1
2
3
allEq(BiPredicate<R, V> filter, Map<R, V> params)
allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)

个别参数说明:

filter : 过滤函数,是否允许字段传入比对条件中
paramsnull2IsNull : 同上

  • 例1: allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null})—>name = '老王' and age is null
  • 例2: allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null}, false)—>name = '老王'

#eq

1
2
eq(R column, Object val)
eq(boolean condition, R column, Object val)
  • 等于 =
  • 例: eq("name", "老王")—>name = '老王'

#ne

1
2
ne(R column, Object val)
ne(boolean condition, R column, Object val)
  • 不等于 <>
  • 例: ne("name", "老王")—>name <> '老王'

#gt

1
2
gt(R column, Object val)
gt(boolean condition, R column, Object val)
  • 大于 >
  • 例: gt("age", 18)—>age > 18

#ge

1
2
ge(R column, Object val)
ge(boolean condition, R column, Object val)
  • 大于等于 >=
  • 例: ge("age", 18)—>age >= 18

#lt

1
2
lt(R column, Object val)
lt(boolean condition, R column, Object val)
  • 小于 <
  • 例: lt("age", 18)—>age < 18

#le

1
2
le(R column, Object val)
le(boolean condition, R column, Object val)
  • 小于等于 <=
  • 例: le("age", 18)—>age <= 18

#between

1
2
between(R column, Object val1, Object val2)
between(boolean condition, R column, Object val1, Object val2)
  • BETWEEN 值1 AND 值2
  • 例: between("age", 18, 30)—>age between 18 and 30

#notBetween

1
2
notBetween(R column, Object val1, Object val2)
notBetween(boolean condition, R column, Object val1, Object val2)
  • NOT BETWEEN 值1 AND 值2
  • 例: notBetween("age", 18, 30)—>age not between 18 and 30

#like

1
2
like(R column, Object val)
like(boolean condition, R column, Object val)
  • LIKE ‘%值%’
  • 例: like("name", "王")—>name like '%王%'

#notLike

1
2
notLike(R column, Object val)
notLike(boolean condition, R column, Object val)
  • NOT LIKE ‘%值%’
  • 例: notLike("name", "王")—>name not like '%王%'

#likeLeft

1
2
likeLeft(R column, Object val)
likeLeft(boolean condition, R column, Object val)
  • LIKE ‘%值’
  • 例: likeLeft("name", "王")—>name like '%王'

#likeRight

1
2
likeRight(R column, Object val)
likeRight(boolean condition, R column, Object val)
  • LIKE ‘值%’
  • 例: likeRight("name", "王")—>name like '王%'

#isNull

1
2
isNull(R column)
isNull(boolean condition, R column)
  • 字段 IS NULL
  • 例: isNull("name")—>name is null

#isNotNull

1
2
isNotNull(R column)
isNotNull(boolean condition, R column)
  • 字段 IS NOT NULL
  • 例: isNotNull("name")—>name is not null

#in

1
2
in(R column, Collection<?> value)
in(boolean condition, R column, Collection<?> value)
  • 字段 IN (value.get(0), value.get(1), …)
  • 例: in("age",{1,2,3})—>age in (1,2,3)
1
2
in(R column, Object... values)
in(boolean condition, R column, Object... values)
  • 字段 IN (v0, v1, …)
  • 例: in("age", 1, 2, 3)—>age in (1,2,3)

#notIn

1
2
notIn(R column, Collection<?> value)
notIn(boolean condition, R column, Collection<?> value)
  • 字段 NOT IN (value.get(0), value.get(1), …)
  • 例: notIn("age",{1,2,3})—>age not in (1,2,3)
1
2
notIn(R column, Object... values)
notIn(boolean condition, R column, Object... values)
  • 字段 NOT IN (v0, v1, …)
  • 例: notIn("age", 1, 2, 3)—>age not in (1,2,3)

#inSql

1
2
inSql(R column, String inValue)
inSql(boolean condition, R column, String inValue)
  • 字段 IN ( sql语句 )
  • 例: inSql("age", "1,2,3,4,5,6")—>age in (1,2,3,4,5,6)
  • 例: inSql("id", "select id from table where id < 3")—>id in (select id from table where id < 3)

#notInSql

1
2
notInSql(R column, String inValue)
notInSql(boolean condition, R column, String inValue)
  • 字段 NOT IN ( sql语句 )
  • 例: notInSql("age", "1,2,3,4,5,6")—>age not in (1,2,3,4,5,6)
  • 例: notInSql("id", "select id from table where id < 3")—>id not in (select id from table where id < 3)

#groupBy

1
2
groupBy(R... columns)
groupBy(boolean condition, R... columns)
  • 分组:GROUP BY 字段, …
  • 例: groupBy("id", "name")—>group by id,name

#orderByAsc

1
2
orderByAsc(R... columns)
orderByAsc(boolean condition, R... columns)
  • 排序:ORDER BY 字段, … ASC
  • 例: orderByAsc("id", "name")—>order by id ASC,name ASC

#orderByDesc

1
2
orderByDesc(R... columns)
orderByDesc(boolean condition, R... columns)
  • 排序:ORDER BY 字段, … DESC
  • 例: orderByDesc("id", "name")—>order by id DESC,name DESC

#orderBy

1
orderBy(boolean condition, boolean isAsc, R... columns)
  • 排序:ORDER BY 字段, …
  • 例: orderBy(true, true, "id", "name")—>order by id ASC,name ASC

#having

1
2
having(String sqlHaving, Object... params)
having(boolean condition, String sqlHaving, Object... params)
  • HAVING ( sql语句 )
  • 例: having("sum(age) > 10")—>having sum(age) > 10
  • 例: having("sum(age) > {0}", 11)—>having sum(age) > 11

#func

1
2
func(Consumer<Children> consumer)
func(boolean condition, Consumer<Children> consumer)
  • func 方法(主要方便在出现if…else下调用不同方法能不断链)
  • 例: func(i -> if(true) {i.eq("id", 1)} else {i.ne("id", 1)})

#or

1
2
or()
or(boolean condition)
  • 拼接 OR

注意事项:

主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接)

  • 例: eq("id",1).or().eq("name","老王")—>id = 1 or name = '老王'
1
2
or(Consumer<Param> consumer)
or(boolean condition, Consumer<Param> consumer)
  • OR 嵌套
  • 例: or(i -> i.eq("name", "李白").ne("status", "活着"))—>or (name = '李白' and status <> '活着')

#and

1
2
and(Consumer<Param> consumer)
and(boolean condition, Consumer<Param> consumer)
  • AND 嵌套
  • 例: and(i -> i.eq("name", "李白").ne("status", "活着"))—>and (name = '李白' and status <> '活着')

#nested

1
2
nested(Consumer<Param> consumer)
nested(boolean condition, Consumer<Param> consumer)
  • 正常嵌套 不带 AND 或者 OR
  • 例: nested(i -> i.eq("name", "李白").ne("status", "活着"))—>(name = '李白' and status <> '活着')

#apply

1
2
apply(String applySql, Object... params)
apply(boolean condition, String applySql, Object... params)
  • 拼接 sql

注意事项:

该方法可用于数据库函数 动态入参的params对应前面applySql内部的{index}部分.这样是不会有sql注入风险的,反之会有!

  • 例: apply("id = 1")—>id = 1
  • 例: apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")—>date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
  • 例: apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08")—>date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")

#last

1
2
last(String lastSql)
last(boolean condition, String lastSql)
  • 无视优化规则直接拼接到 sql 的最后

注意事项:

只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用

  • 例: last("limit 1")

#exists

1
2
exists(String existsSql)
exists(boolean condition, String existsSql)
  • 拼接 EXISTS ( sql语句 )
  • 例: exists("select id from table where age = 1")—>exists (select id from table where age = 1)

#notExists

1
2
notExists(String notExistsSql)
notExists(boolean condition, String notExistsSql)
  • 拼接 NOT EXISTS ( sql语句 )
  • 例: notExists("select id from table where age = 1")—>not exists (select id from table where age = 1)

#QueryWrapper

说明:

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件
及 LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() 方法获取

#select

1
2
3
select(String... sqlSelect)
select(Predicate<TableFieldInfo> predicate)
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
  • 设置查询字段

说明:

以上方法分为两类.
第二类方法为:过滤查询字段(主键除外),入参不包含 class 的调用前需要wrapper内的entity属性有值! 这两类方法重复调用以最后一次为准

  • 例: select("id", "name", "age")
  • 例: select(i -> i.getProperty().startsWith("test"))

#UpdateWrapper

说明:

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件
LambdaUpdateWrapper, 可以通过 new UpdateWrapper().lambda() 方法获取!

#set

1
2
set(String column, Object val)
set(boolean condition, String column, Object val)
  • SQL SET 字段
  • 例: set("name", "老李头")
  • 例: set("name", "")—>数据库字段值变为空字符串
  • 例: set("name", null)—>数据库字段值变为null

#setSql

1
setSql(String sql)
  • 设置 SET 部分 SQL
  • 例: setSql("name = '老李头'")

#lambda

  • 获取 LambdaWrapper
    QueryWrapper中是获取LambdaQueryWrapper
    UpdateWrapper中是获取LambdaUpdateWrapper

#使用 Wrapper 自定义SQL

注意事项:

需要mybatis-plus版本 >= 3.0.7 param 参数名要么叫ew,要么加上注解@Param(Constants.WRAPPER) 使用${ew.customSqlSegment} 不支持 Wrapper 内的entity生成where语句

#用注解

1
2
@Select("select * from mysql_data ${ew.customSqlSegment}")
List<MysqlData> getAll(@Param(Constants.WRAPPER) Wrapper wrapper);

#用XML

1
2
3
4
List<MysqlData> getAll(Wrapper ew);
<select id="getAll" resultType="MysqlData">
SELECT * FROM mysql_data ${ew.customSqlSegment}
</select>

#kotlin使用wrapper

kotlin 可以使用 QueryWrapperUpdateWrapper 但无法使用 LambdaQueryWrapperLambdaUpdateWrapper
如果想使用 lambda 方式的 wrapper 请使用 KtQueryWrapperKtUpdateWrapper

#链式调用 lambda 式

1
2
3
4
5
6
7
8
9
10
11
12
13
// 区分:
// 链式调用 普通
UpdateChainWrapper<T> update();
// 链式调用 lambda 式。注意:不支持 Kotlin
LambdaUpdateChainWrapper<T> lambdaUpdate();

// 等价示例:
query().eq("id", value).one();
lambdaQuery().eq(Entity::getId, value).one();

// 等价示例:
update().eq("id", value).remove();
lambdaUpdate().eq(Entity::getId, value).remove();
Unix程序设计课堂知识点整理

Linux编程基础

[TOC]

1.1 从Unix到Linux

为了提升UNICS系统的性能与兼容性,采用高级语言对其进行重构,并确定该操作系统名称为UNIX,这就是最早的 UNIX 操作系统(相对于 Multics ,UNIX 具有单一的意思)

GNU通用公共许可协议(GNU GPL)是一个广泛被使用的自由软件许可协议条款,最初由Stallman为GNU计划而撰写,GPL授予程序接受人以下权利,或称“自由”:

⚫ 以任何目的运行此程序的自由;

⚫ 再发行复制件的自由;

⚫ 改进此程序,并公开发布改进的自由

1.2 Linux概述

Linux是一个类Unix(Unix-like)的操作系统,在1991年发行了它的第一个版本

1991年11月,芬兰赫尔辛基大学的学生 Linus Torvalds写了个小程序,取名为Linux,放在互联网上。

1993,在一批高水平黑客的参与下,诞生了Linux 1.0 版

1994年,Linux 的第一个商业发行版 Slackware 问世

1996年,美国国家标准技术局的计算机系统实验室确认 Linux 版本 1.2.13(由 Open Linux 公司打包)符合 POSIX 标准

1.3 GNU & Linux

image-20210611103621201

1.4 Linux 内核

Linux内核采用的是双树系统

一棵是稳定树,主要用于发行

另一棵是非稳定树或称为开发树,用于产品开发和改进

Linux内核版本号由3位数字组成

image-20210611103506715

2.2 Vi编辑器使用

1.vi的工作模式

输入模式:输入字符为命令,可进行删除、修改、存盘等操作 。

命令模式:输入字符作为文本内容。

末行模式:命令模式下输入“:/?”三个中任意一个,可移到屏幕最底一行。

image-20210611103853138

(1)命令模式

输入模式下,按ESC可切换到命令模式,常用命令:

:q! 离开vi,并放弃刚在缓冲区内编辑的内容
:wq 将缓冲区内的资料写入磁盘中,并离开vi
:ZZ 同wq
:x 同wq
:w 将缓冲区内的资料写入磁盘中,但并不离开vi
:q 离开vi,若文件被修改过,则要被要求确认是否放弃修改的内容,此指令可与:w配合使用

(2)输入模式

输入以下命令即可进入vi输入模式

a(append) 在光标之后加入资料
A 在该行之末加入资料
i(insert) 在光标之前加入资料
I 在该行之首加入资料
o(open) 新增一行于该行之下,供输入资料用
O 新增一行于该行之上,供输入资料用
dd 删除当前光标所在行
x 删除当前光标字符
X 删除当前光标之前字符
U 撤消
F 查找
ESC 离开输入模式

2.Vi其他功能命令

(1)复制粘贴

yw 将光标所在之处到字尾的字符复制到缓冲区
yy 复制光标所在行到缓冲区
#yy 如:6yy表示拷贝从光标所在行往下数6行文字
p 将缓冲区内的字符贴到光标所在位置

(2)查找/替换

?字符串 从当前光标位置开始向后查找字符串
/字符串 从当前光标位置开始向前查找字符串
n 继续上一次查找
Shift+n 以相反的方向继续上一次查找

(3)环境设置

:set ai 自动缩进,每一行开头都与上一行的开头对齐
:set nu 编辑时显示行号
:set dir=./ 将交换文件.swp保存在当前目录
:set sw=4 设置缩进的字符数为4
:syntax on 或者 :syntax=on 开启语法着色

3.1 GCC编译器介绍

GCC是一个强大的工具集合,它包含了预处理器、编译器、汇编器、链接器等组件。它会在需要的时候调用其他组件。
输入文件的类型和传递给gcc的参数决定了gcc调用具体的哪些组件。

GCC 参数选项

Usage:

gcc [options] [filename]

Basic options:

-E: 只对源程序进行预处理(调用cpp预处理器)
-S: 只对源程序进行预处理、编译
-c: 执行预处理、编译、汇编而不链接
-o: output_file: 指定输出文件名
-g: 产生调试工具必需的符号信息
-O/On: 在程序编译、链接过程中进行优化处理
-Wall: 显示所有的警告信息
-I dir: 在头文件的搜索路径中添加dir目录
-L dir : 在库文件的搜索路径列表中添加dir目录

3.2 GCC编译过程

1、预处理

2、编译成汇编代码

3、汇编成目标代码

4、链接

1.预处理

预处理:使用-E参数

gcc –E –o gcctest.i gcctest.c

使用wc命令比较预处理后的文件与源文件,可以看到两个文件的差异

image-20210611105310558

2.编译成汇编代码

预处理文件—->汇编代码
使用-S说明生成汇编代码后停止工作

gcc –S –o gcctest.s gcctest.i

直接编译到汇编代码

gcc –S gcctest.c

image-20210611105654986

image-20210611105702671

3.编译成目标代码

汇编代码à目标代码

gcc –x assembler –c gcctest.s

直接编译成目标代码

gcc –c gcctest.c

使用汇编器生成目标代码

as –o gcctest.o gcctest.s

image-20210611105827814

4.编译成执行代码

目标代码à执行代码

gcc –o gcctest gcctest.o

直接生成执行代码

gcc –o gcctest gcctest.c

image-20210611105949690

3.3 GCC编译优化

优化编译选项有:

-O0
缺省情况,不优化

-O1

-O2

-O3

等等

image-20210611110447409

image-20210611110459630

3.4 头文件和库函数目录

1. GCC –I dir 参数使用

头文件和gcc不在同一目录下,用 –I dir指明头文件所在的目录。

#include <>:在默认路径“/usr/include”中搜索头文件

#include “”:在本目录中搜索

image-20210611111109662

image-20210611111326888

解决办法:

  1. gcc opt.c –o opt –I ./

  2. 修改main

    1
    2
    3
    #include <my.h> 

    #include “my.h”

2. GCC创建函数库

函数库:公用函数定义为函数库,供其他程序使用。函数库分为静态库和动态库。

静态库:程序编译时会链接到目标代码中,程序运行时不再需要静态库。程序生成的可执行程序比较大。后缀名为“.a”

动态库:程序编译时不会链接到目标代码,在程序运行时载入,运行时需要动态库存在。动态库可方便多个程序共享一个函数库。后缀名为”.so”

函数库的生成:由编译过的.o文件生成。

创建静态库:

1.将需要生成函数库的函数执行gcc –c,生成.o文件

​ gcc –c hello.c

2.由.o文件创建静态库,静态库命名格式为:lib静态库名.a

​ ar -rv libmyhello.a hello.o

  1. 使用静态库:在调用静态库的程序编译时指定静态库名

    $gcc –o hello main.c –L. –lmyhello

    $./hello

创建动态库:

1.由.o文件生成动态库,动态库的命名:lib动态库名.so

1
2
3
gcc –shared –fPIC –o libmyhello.so hello.o

gcc –shared –fPIC –o libmyhello.so hello.c (centos版本)

2.使用动态库:用gcc命令指定动态库名进行编译,编译之前需将动态库文件复制到系统默认库函数目录/usr/lib中或者设置搜索路径。 sudo ldconfig

1
2
3
gcc –o hello main.c –L. –lmyhello

gcc –o hello main.c –L. –lmyhello -Wl,-rpath=./

gdb commands

image-20210611111727753

Gdb using

$gdb filename

gdb将装入名为filename的可执行文件

在编译时需要使用-g选项

image-20210611111834934

image-20210611111847709

image-20210611111917435

list

image-20210611111947211

image-20210611111954948

help

image-20210611112004401

设置断点 break

image-20210611112101376

显示断点信息 info breakpoints

image-20210611112126359

清除已经定义的断点 clear

image-20210611112155046

delete [bkpoints-num]删除指定/全部断点

image-20210611112342864

image-20210611112424180

quit:退出gdb

image-20210611112436348

实例

image-20210611112521483

image-20210611112526161

image-20210611112545028

image-20210611112553240

image-20210611112603726

调试子命令总结

file :装入想要调试的可执行文件

kill:终止正在调试的程序

list:列出正在执行的程序清单

next:执行一行代码但不进入函数内部

step:执行一行代码并进入函数内部

run: 执行当前正在调试的程序

quit:终止gdb调试

break:设置断点 (break 行号)

watch:设置观察点,观察表达式的值是否发

​ 生变化

info: 查看断点信息

info breakpoints、info watchpoints

info break 显示当前断点清单,包括到达

断点处的次数等。

info files 显示被调试文件的详细信息。

info func 显示所有的函数名称。

info local 显示当函数中的局部变量信息。

info prog 显示被调试程序的执行状态。

info var 显示所有的全局和静态变量名称

delete: 删除某个或所有的断点

delete 断点号 或 delete

disable: 使断点失效(但仍存在)

enable: 使断点有效

clear: 清除断点信息

clear 断点所在行号

clear 函数入口

continue: 继续执行程序直到程序结束

5.1 Make 引入

Make的引入:

Ø文件数量太大,手工gcc编译不方便

Ø仅需要编译已经做了修改的源代码文件;其他文件只需要重新连接

Ø记录哪些文件已改变且需要编译,哪些文件仅仅需要连接很难

make & makefile

Multi-file project

IDE—Eclipse

make

make & makefile

makefile描述模块间的依赖关系;

make命令根据makefile对程序进行管理和维护;make判断被维护文件的时序关系

make

make [-f filename] [targetname]

使用方法:

v make 自动找当前目录下名为Makefile/makefile的文件

v make –f 文件名 找当前目录下指定文件名的文件

makefile的组成

  • 显式规则:明确指出目标文件的生成规则
  • 隐式规则:需要make自动推导的规则
  • 变量定义:声明时赋值,引用时加”$”
  • 文件指示:引用外部的文件
  • 注释:#

Makefile的显式规则

规则
一条规则包含3个方面的内容,

1)要创建的目标(文件),

2)创建目标(文件)所依赖的文件列表;

3)通过依赖文件创建目标文件的命令组

规则一般形式

1
2
3
4
5
6
7
*target* ... : *prerequisites* ... 

*<tab>command*

<tab>...

<tab>...

每条规则由一个带冒号的“依赖行”和一条或多条以tab开头的“命令行”组成

目标1 [目标2…]:[依赖文件列表]

[\t 命令]

ex:

1
2
3
*make_test:make_main.o* *wrtlog.o*

**<TAB>***gcc* *-o* *make_test* *make_main.o* *wrtlog.o*

冒号左边是目标,冒号右边是依赖文件

目标和依赖文件均是由字母、数字、句点和斜杠组成的字符串

目标或依赖文件的数目多于一个时,以空格分隔

image-20210611160145895

一个简单的makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
gcc -o edit main.o kbd.o command.o display.o insert.o \
search.o files.o utils.o
main.o : main.c defs.h
gcc -c main.c
kbd.o : kbd.c defs.h command.h
gcc -c kbd.c
command.o : command.c defs.h command.h
gcc -c command.c
display.o : display.c defs.h buffer.h
gcc -c display.c
insert.o : insert.c defs.h buffer.h
gcc -c insert.c
search.o : search.c defs.h buffer.h
gcc -c search.c
files.o : files.c defs.h buffer.h command.h
gcc -c files.c
utils.o : utils.c defs.h
gcc -c utils.c

clean :
rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o

Make的工作过程

default goal
在缺省的情况下,make从makefile中的第一个目标开始执行

Make的工作过程类似一次深度优先遍历过程

Makefile的变量

自定义变量
例:

object=main.o Add.o Sub.o Mul.o Div.o

exe : $(object)

gcc -o exe $(object)

特殊变量

$@:表示目标文件;

$^:表示所有依赖目标的集合,以空格分隔;

$<:表示第一个依赖文件;

Makefile的隐式规则(自动推导)

make能够自动推导文件以及文件依赖关系后面的命令

例:

object=main.o Add.o Sub.o Mul.o Div.o

exe : $(object)

gcc -o $@ $(object)

main.o : def.h

Makefile的伪目标

lmakefile使用.PHONY关键字来定义一个伪目标,具体格式为:

.PHONY : 伪目标名称

例:

.PHONY : clean

clean :

​ rm $(object)

总结: C语言编程步骤

  1. 编辑:vi ,emacs,gedit,Eclipse…
  2. 编译: gcc
  3. 调试:gdb
  4. 运行: ./executable file
  5. 项目管理:make

扩展

gdb命令

命令 解释 示例
file <文件名> 加载被调试的可执行程序文件。 因为一般都在被调试程序所在目录下执行GDB,因而文本名不需要带路径。 (gdb) file gdb-sample
r Run的简写,运行被调试的程序。 如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处。 (gdb) r
c Continue的简写,继续执行被调试程序,直至下一个断点或程序结束。 (gdb) c
b <行号> b <函数名称> b *<函数名称> b *<代码地址>d [编号] b: Breakpoint的简写,设置断点。两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置。 其中在函数名称前面加“*”符号表示将断点设置在“由编译器生成的prolog代码处”。如果不了解汇编,可以不予理会此用法。d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。 (gdb) b 8 (gdb) b main (gdb) b *main (gdb) b *0x804835c(gdb) d
s, n s: 执行一行源程序代码,如果此行代码中有函数调用,则进入该函数; n: 执行一行源程序代码,此行代码中的函数调用也一并执行。s 相当于其它调试器中的“Step Into (单步跟踪进入)”; n 相当于其它调试器中的“Step Over (单步跟踪)”。这两个命令必须在有源代码调试信息的情况下才可以使用(GCC编译时使用“-g”参数)。 (gdb) s (gdb) n
si, ni si命令类似于s命令,ni命令类似于n命令。所不同的是,这两个命令(si/ni)所针对的是汇编指令,而s/n针对的是源代码。 (gdb) si (gdb) ni
p <变量名称> Print的简写,显示指定变量(临时变量或全局变量)的值。 (gdb) p i (gdb) p nGlobalVar
display …undisplay <编号> display,设置程序中断后欲显示的数据及其格式。 例如,如果希望每次程序中断后可以看到即将被执行的下一条汇编指令,可以使用命令 “display /i pc”其中pc”其中pc 代表当前汇编指令,/i 表示以十六进行显示。当需要关心汇编代码时,此命令相当有用。undispaly,取消先前的display设置,编号从1开始递增。 (gdb) display /i $pc(gdb) undisplay 1
i Info的简写,用于显示各类信息,详情请查阅“help i”。 (gdb) i r
q Quit的简写,退出GDB调试环境。 (gdb) q
help [命令名称] GDB帮助命令,提供对GDB名种命令的解释说明。 如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。 (gdb) help display

.o文件是二进制文件

雨课堂题目整理

image-20210618143002794

image-20210618143011579

image-20210618143019456

image-20210618143027894

image-20210618143042533

image-20210618143052676

image-20210618143100984

image-20210618143111912

image-20210618143125572

image-20210618143147555

image-20210618143155638

image-20210618143205552

image-20210618143219860

image-20210618143233469

image-20210618143245888

image-20210618143254740

image-20210618143302040

image-20210618143310814

image-20210618143340720

image-20210618143348878

image-20210618143404564

image-20210618143416361

image-20210618143424721

文件I/O

1.1 文件属性

  1. 文件属性数据结构struct stat ——文件控制块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct stat {
mode_t st_mode; /*file type & mode*/
ino_t st_ino; /*inode number (serial number)*/
dev_t st_rdev; /*device number (file system)*/
nlink_t st_nlink; /*link count*/
uid_t st_uid; /*user ID of owner*/
gid_t st_gid; /*group ID of owner*/
off_t st_size; /*size of file, in bytes*/
time_t st_atime; /*time of last access*/
time_t st_mtime; /*time of last modification*/
time_t st_ctime; /*time of lat file status change*/
long st_blksize; /*Optimal block size for I/O*/
long st_blocks; /*number 512-byte blocks allocated*/
};

命令查看:ls -l file

  1. stat/fstat/lstat函数

获取文件属性

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *file_name, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *file_name, struct stat *buf);
(Return: 0 if success; -1 if failure)

1.2 文件类型

**1.**文件类型

Unix/Linux系统支持的文件类型:

  • Directory(d):目录文件
  • Link(l):链接文件
  • Pipe(p):管道文件
  • Block Device(b):块设备文件
  • Character Device(c):字符设备文件
  • Regular(-):普通文件
  • Socket(s):套接字文件

查看文件类型

使用命令:ls –l /dev/sda1

2.1设计一个程序,要求列出当前目录下的文件信息,以及系统“/dev/sda1”和“/dev/lp0”的文件信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>	                
#include<stdlib.h> /*文件预处理,包含system函数库*/
int main ()
{
int newret;
printf("列出当前目录下的文件信息:\n");
newret=system("ls -l");
printf("列出\"dev/sda1\"的文件信息:\n");
newret=system("ls /dev/sda1 -l"); /*列出“/dev/sda1”的文件信息*/
printf("列出\"dev/ lp0\"的文件信息:\n");
newret=system("ls /dev/lp0 -l"); /*列出“/dev/ lp0”的文件信息*/
return 0;
}

**2.**获取文件类型

st_mode存储文件类型和许可权限,形式如下:
type3 type2 type1 type0 suid sgid sticky rwx rwx rwx

用来确定文件类型的宏
S_ISBLK – 测试块文件
S_ISCHR – 测试字符文件
S_ISDIR – 测试目录
S_ISFIFO – 测试FIFO
S_ISREG – 测试普通文件
S_ISLNK – 测试符号链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
int main(int argc,char *argv[])
{ int i; struct stat statbuf;
for(i=1;i<argc;i++)
{ printf("%s:",argv[i]);
if(lstat(argv[i],&statbuf)==-1) { printf("error\n"); continue; }
if(S_ISDIR(statbuf.st_mode)) printf("%s is a directory\n",argv[i]);
else if(S_ISREG(statbuf.st_mode)) printf("%s is a regular file\n",argv[i]);
else if(S_ISBLK(statbuf.st_mode)) printf("%s is a block device\n",argv[i]);
else if(S_ISCHR(statbuf.st_mode)) printf("%s is a charcter device\n",argv[i]);
#ifdef S_ISLNK
else if (S_ISLNK(statbuf.st_mode)) printf("%s is a link file\n",argv[i]);
#endif
#ifdef S_ISSOCK
else if (S_ISSOCK(statbuf.st_mode)) printf("%s is a socket \n",argv[i]);
#endif
else printf("%s is an unknown type file \n",argv[i]);
}
}

image-20210611162747182

1.3 文件存取权限

**1.**文件存取权限

image-20210611162858324

读的权限:显示目录文件,进入目录

写的权限:目录下创建文件

执行的权限:显示目录文件,进入目录,创建文件

**2.**改变文件存取权限——命令

image-20210611163120067

image-20210611163125022

2.3 设计一个程序,要求把系统中“/home/mylinux目录下的myfile文件权限,设置成文件所有者可读可写,其他用户只读权限。

1
2
3
4
5
6
7
#include<sys/types.h>		
#include<sys/stat.h>
int main ()
{
chmod("/home/mylinux/myfile",S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
return 0;
}
1
2
3
4
5
6
7
chmod 函数
Change permissions of a file
#include <sys/types.h>
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(int fildes, mode_t mode);// 打开的文件
(Return: 0 if success; -1 if failure)

3. 改变文件存取权限

image-20210611163410933

4. 默认文件存取权限——umask

新创建的文件和目录的默认权限是(root)

File: -rw-r–r– 644

Directory: drwxr-xr-x 755

Why?

umask: 包含未被设置为权限位的==八进制数字(即无x位(可执行位))==。默认002为普通用户,022为root用户。666-644=022

2.4 设计一程序,要求设置系统文件和目录的权限掩码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
int main()
{ mode_t new_umask,old_umask;
new_umask=0666;
old_umask=umask(new_umask);
printf("系统原来的权限掩码是:%o\n",old_umask);
printf("系统新的权限掩码是:%o\n",new_umask);
system("touch liu1");
printf("创建了文件liu1\n");
new_umask=0444;
old_umask=umask(new_umask);
printf("系统原来的权限掩码是:%o\n",old_umask);
printf("系统新的权限掩码是:%o\n",new_umask);
system("touch liu2");
printf("创建了文件liu2\n");
system("ls liu1 liu2 -l");
return 0;
}
1
2
3
4
5
6
7
umask 函数
为进程设置文件存取权限屏蔽字,并返回以前的值
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
建立文件,文件的权限为0666-mask;
建立目录,目录权限为0777-mask。

1.4 文件其他属性

**1.chown/fchown/**lchown 函数

1
2
3
4
5
6
7
8
改变文件所有者
#include <sys/types.h>
#include <unistd.h>

int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
(Return: 0 if success; -1 if failure)

2.获取文件存取时间——stat

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<sys/stat.h>
#include<time.h>
main()
{
char *path="./2-5.c";
struct stat statbuf;
if (stat(path,&statbuf)==-1)
perror("Failed to get file status");
else printf("%s last accessed at %s",path,ctime(&statbuf.st_atime));
}

**3.**获取文件大小——stat

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h> 
#include<unistd.h>
#include<sys/stat.h>
int main ()
{
struct stat buf;
stat("/etc/passwd",&buf);
printf("\"/etc/passwd\"文件的大小是:%d\n",buf.st_size);
return 0;
}

2.1 两种I/O方式

1. 无缓冲和缓冲I/O

无缓冲I/O

  • read/write ->系统函数
  • 文件描述符
  • POSIX.1 and XPG3标准

缓冲 I/O

  1. 标准I/O库实现
  2. 处理很多细节, 如缓存分配, 以优化长度执行I/O等.
  3. 流 -> FILE类型指针

**2.**无缓冲 I/O 系统调用

基本 I/O

  • open/creat, close, read, write, lseek
  • dup/dup2
  • fcntl

2.2 文件描述符

1.文件描述符概述

非负整数

​ int fd;

​ (in <unistd.h>)

​ STDIN_FILENO (0), STDOUT_FILENO (1), STDERR_FILENO (2)

文件操作一般步骤:

open-read/write-[lseek]-close

2. 进程打开文件的内核数据结构

image-20210611170650485

image-20210611170732113

image-20210611170743253

image-20210611170755032

image-20210611170805503

image-20210611170815361

image-20210611170829150

image-20210611170853877

image-20210611170909100

image-20210611170919482

image-20210611170928977

2.3 无缓冲 I/O函数

**0.**错误处理

1
2
3
4
5
6
7
8
9
10
UNIX方式
Return value
“errno”变量( defined in /usr/include/errno.h)
extern int errno;

strerror & perror
#include <string.h>
char *strerror(int errnum);
#include <stdio.h>
void perror(const char *msg);

1. creat 函数

1
2
3
4
5
6
create a file or device 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
(Return: a new file descriptor if success; -1 if failure)

2.7 **设计一程序,要求在“/**home”目录下创建一个名称为“2-7file”的文件,并且把此文件的权限设置为所有者具有只读权限,最后显示此文件的信息。

1
2
3
4
5
6
7
8
9
10
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd;
fd=creat("./2-7file",S_IRUSR);
system("ls ./2-7file -l");
return 0;
}

参数 mode”

“mode”: 指定创建的新文件的存取权限

image-20210611171422005

参数mode& umask

umask: 一种文件保护机制

新建文件的初始存取权限

image-20210611171522883

2. Open 函数

1
2
3
4
5
6
7
8
Open and possibly create a file or device 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
(Return: a new file descriptor if success; -1 if failure)

2.8设计一个程序,要求在当前目录下以可读写方式打开一个名为“2-8file”的文件。如果该文件不存在,则创建此文件;如果存在,将文件清空后关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>			
#include<stdlib.h>
#include<fcntl.h>
int main ()
{ int fd;
if((fd=open("./2-8file",O_CREAT|O_TRUNC|O_WRONLY,0600))<0)
{ perror("打开文件出错");
exit(1);
}
else
{ printf("打开(创建)文件\"2-8file\",文件描述符为:%d\n",fd);
}
if(close(fd)<0)
{ perror("关闭文件出错");
exit(1);
}
system("ls ./2-8file -l");
return 0;
}

参数 flags

1
2
3
4
5
6
7
8
“flags”: 指定文件存取方式
One of O_RDONLY, O_WRONLY or O_RDWR which request opening the file read-only, write-only or read/write, respectively, bitwise-or’d with zero or more of the following: ( All defined in /usr/include/fcntl.h)
O_APPEND: 追加方式打开
O_TRUNC:文件存在清空
O_CREAT: 文件不存在创建.
O_EXCL: 和 O_CREAT一起用时, 文件存在则出错,打开失败

“creat” function: 等价于open函数中指定 O_CREAT|O_WRONLY|O_TRUNC

3. close 函数

1
2
3
4
Close a file descriptor
#include <unistd.h>
int close(int fd);
(Return: 0 if success; -1 if failure)

4. read/write 函数

1
2
3
4
5
6
7
8
Read from a file descriptor
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
(返回值: 读到的字节数,若已到文件尾为0,若出错为-1)
Write to a file descriptor
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
(返回值: 若成功为已写的字节数,若出错为-1)

2.9设计一个程序,完成文件的复制工作。要求通过read函数和write函数复制“/etc/passwd”文件到目标文件中,目标文件名在程序运行时从键盘输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{ int fdsrc,fddes,nbytes; int z; char buf[20], des[20];
int flags=O_CREAT | O_TRUNC | O_WRONLY;
printf("请输入目标文件名:");
scanf("%s",des);
fdsrc=open("/etc/passwd",O_RDONLY);
if(fdsrc<0) { exit(1); }
fddes=open(des,flags,0644);
if(fddes<0) { exit(1); }
while((nbytes=read(fdsrc,buf,20))>0)
{ z=write(fddes,buf,nbytes);
if(z<0) { perror("写目标文件出错"); } }
close(fddes);
close(fdsrc);
printf("复制\"/etc/passwd\"文件为\"%s\"文件成功!\n",des);
exit(0);
}

5. lseek 函数

read/write 定位文件指针

1
2
3
4
5
6
7
#include <sys/types.h>

#include <unistd.h>

off_t lseek(int fildes, off_t offset, int whence);

(Return: the resulting offset location if success; -1 if failure)

该指令的“那里”:

​ SEEK_SET: the offset is set to “offset” bytes指针位移量为设定值

​ SEEK_CUR: the offset is set to its current location plus “offset” bytes指针位移量为当前位移加设定值

​ SEEK_END: the offset is set to the size of the file plus “offset” bytes指针位移量为文件尾加设定值

image-20210611213210597

空洞文件

使用lseek修改文件偏移量后,当前文件偏移量有可能大于文件的长度

在这种情况下,对该文件的下一次写操作,将加长该文件

这样文件中形成了一个空洞。对空洞区域进行读,均返回0

image-20210611213233714

6. dup/dup2 函数

1
2
3
4
5
复制文件描述符
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
(Return: the new file descriptor if success; -1 if failure)

image-20210611213303221

image-20210611213316880

假设进程已打开文件描述符0、1、2

调用dup2(1, 6),dup2返回值是多少?

然后再调用dup(6),dup返回值是多少?

image-20210611213407986

image-20210611213417786

7. fcntl函数

1
2
3
4
5
6
查看、修改打开的文件描述符
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
int result=fcntl(int fd,int cmd);
int result=fcntl(int fd,int cmd,long arg,…);

lcmd取值:

F_DUPFD 复制文件描述符

F_GETFD 获得文件描述符

F_SETFD 设置文件描述符

F_GETFL 获取文件描述符当前模式

F_SETFL设置文件描述符当前模式

F_GETLK 获得记录锁

F_SETLK 设置记录锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
分析以下程序运行情况
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
char buf1[]="abcdefghij";
char buf2[]="ABCDEFGHEIJ";
main()
{int fd;
if((fd=open("file.hole",O_WRONLY|O_CREAT/*|O_APPEND*/,0644))<0)
perror("creat error");
if(write(fd,buf1,10)!=10)
perror("buf1 write error");
if(lseek(fd,40,SEEK_SET)==-1)
perror("lseek error");
if(write(fd,buf2,10)!=10)
perror("buf2 write error");
exit(0);
}
1)ls –l 查看文件大小多少字节?
2)cat 查看文件内容?
3)od –c 查看文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
分析以下程序运行结果
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
main()
{ int fd;
if((fd=open("myout",O_WRONLY|O_CREAT,0644))==-1)
perror("myout open error");
if(dup2(fd,STDOUT_FILENO)==-1)
perror("redirect stand output failed");
printf("this is a test program for redirect\n");
close(fd);
}
1)查看程序运行结果?
2)cat 查看文件myout的内容

3.1 标准I/O

为什么要设计标准I/O库?

​ 直接使用API进行文件访问时,需要考虑许多细节问题

​ 例如:read、write时,缓冲区的大小该如何确定,才能使效率最优

标准I/O库封装了诸多细节问题,包括缓冲区分配

I/O效率示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#define BUFFSIZE 4096
#include<stdio.h>
#include<unistd.h>
int main()
{
int n;
char buf[BUFFSIZE];
while((n = read(STDIN_FILENO, buf, BUFFSIZE))>0)
if(write(STDOUT_FILENO, buf,n)!= n)
perror("write error");
return 0;
}
程序中,影响效率的关键:BUFFSIZE的取值

image-20210611213732273

原因

  • Linux文件系统采用了某种预读技术
  • 当检测到正在进行顺序读取时,系统就试图读入比应用程序所要求的更多数据
  • 并假设应用程序很快就会读这些数据
  • 当BUFFSIZE增加到一定程度后,预读就停止了

标准 I/O 缓冲

标准I/O库提供缓冲的目的:尽可能减少使用read、write调用的次数,以提高I/O效率。

通过标准I/O库进行的读写操作,数据都会被放置在标准I/O库缓冲中中转。

3.2 文件流

1
2
3
4
5
6
7
8
9
typedef struct  {
..............................
char fd; /* File descriptor */
short bsize; /* Buffer size */
unsigned char *buffer; /* Data transfer buffer */
....................................
} FILE;
标准I/O库
管理的缓冲区

3.3 标准 I/O 函数

  • 流 open/close
  • 流 read/write
    • ​ 每次一个字符的I/O:fgetc,fputc
    • ​ 每次一行的I/O: fgets,fputs,gets,puts
    • ​ 直接I/O(二进制I/O): fread,fwrite
    • ​ 格式化I/O:scanf,printf,fscanf,fprintf
  • 流定位:fseek,ftell,frewind
  • 流刷新:fflush

1. 流打开/关闭

2.10设计一个程序,要求用流文件I/O操作打开文件“2-10file”,如果该文件不存在,则创建文件。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE * fp;
if((fp=fopen("./2-10file","a+"))==NULL)
{
printf("打开(创建)文件出错"); exit(0);
}
fclose(fp);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Open a stream
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
Parameter “mode”
“r”: Open text file for reading.
“w”: Truncate file to zero length or create text file for writing.
“a”: Open for appending.
“r+”: Open for reading and writing.
“w+”: Open for reading and writing. The file is created if it does not exist, otherwise it is truncated.
“a+”: Open for reading and appending. The file is created if does not exist.

Close a stream
#include <stdio.h>
int fclose(FILE *fp);
(Return: 0 if success; -1 if failure)

**2.流读/**写

对流有三种读写方式

  • 每次读写一个字符

  • 每次读写一行

  • 每次读写任意长度的内容

1)输入/出一个字符

1
2
3
4
5
6
7
8
9
10
11
输入
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
输出
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
(Return: the character if success; -1 if failure)

(2) 输入/出一行

1
2
3
4
5
6
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);
#include <stdio.h>
int fputs(const char *s, FILE *stream);
int puts(const char *s);

(3) 二进制流输入/输出

1
2
3
4
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
(Return: the number of a items successfully read or written.)

例2.11设计两个程序,要求一个程序把三个人的姓名和账号余额信息通过一次流文件I/O操作写入文件“2-11file”,另一个格式输出账号信息,把每个人的账号和余额一一对应显示输出。

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
/*程序:把帐号信息从文件读出*/  
#include<stdio.h>
#define nmemb 3
struct test
{ char name[20];
int pay;
}s[nmemb];
int main( )
{ FILE * fp;
int i;
fp = fopen("2-11file", "r");
fread(s,sizeof(struct test),nmemb,fp);
fclose(fp);
for(i=0;i<nmemb;i++)
printf("帐号[%d]:%-20s余额[%d]:%d\n",i,s[i].name,i,s[i].pay);
return 0;
}

/*程序:把帐号信息写入文件*/
#include<stdio.h>
#include<string.h>
#define set_s(x,y,z) {strcpy(s[x].name,y);s[x].pay=z;} /*自定义宏,用于赋值*/
#define nmemb 3
struct test
{ char name[20];
int pay;
}s[nmemb];
int main()
{ FILE * fp;
set_s(0,"张三",12345);
set_s(1,"李四",200);
set_s(2,"王五",50000);
fp=fopen("2-11file","a+"); /*打开(创建)文件*/
fwrite(s,sizeof(struct test),nmemb,fp);
fclose(fp);
return 0;
}

(4) 格式化 I/O

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);

3.流定位

1
2
3
4
#include <stdio.h>
int fseek(FILE *stream, long int offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);

例2.12 设计一程序,要求用fopen函数打开系统文件“/etc/passwd”,先把位置指针移动到第10个字符前,再把位置指针移动到文件尾,最后把位置指针移动到文件头,输出三次定位的文件偏移量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main()
{
FILE *stream;
long offset;
fpos_t pos;
stream=fopen("/etc/passwd","r");
fseek(stream,10,SEEK_SET);
printf("文件流的偏移量:%d\n",ftell(stream));
fseek(stream,0,SEEK_END);
printf("文件流的偏移量:%d\n",ftell(stream));
rewind(stream);
printf("文件流的偏移量:%d\n",ftell(stream));
fclose(stream);
return 0;
}

image-20210617212155113

4. 流刷新

刷新文件流。把流里的数据立刻写入文件—fork前使用fflush
#include <stdio.h>
int fflush(FILE *stream);

自动刷新:

  • 流关闭fclose;
  • exit终止;
  • 行缓冲“\n”;
  • 缓冲区满;
  • 执行输入操作读文件:printf(“hello”);scanf(“%d”,&a)

5. 流缓冲

  • 三种类型的缓冲
  • 块(全)缓冲block buffered (fully buffered):一般C库函数写入文件是全缓冲的
  • 行缓冲line buffered:引用标准交互设备的流stdin,stdout
    例:    for(i=1;i<=10;i++) fputc(c,stdout);
               fputc("\n",stdout);
    
  • 无缓冲Unbuffered:标准错误流stderr

全缓冲

  • 在填满标准I/O缓冲区后,才进行实际I/O操作(例如调用write函数)
  • 调用fflush函数也能强制进行实际I/O操作

行缓冲

  • 在输入和输出遇到换行符时,标准I/O库执行I/O操作
  • 因为标准I/O库用来收集每一行的缓存的长度是固定的,所以,只要填满了缓存,即使没有遇到新行符,也进行I/O操作
  • 终端(例如标准输入和标准输出),使用行缓冲

不带缓冲

  • 标准I/O库不对字符进行缓冲存储
  • 标准出错是不带缓冲的,为了让出错信息尽快显示出来

6. 流和文件描述符

确定流使用的底层文件描述符
#include <stdio.h>
int fileno(FILE *fp);
根据已打开的文件描述符创建一个流
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);

4.1 目录文件

mkdir/rmdir
chdir/fchdir, getcwd
读目录操作
opendir/closedir
readdir
telldir
seekdir

例2.13设计一程序,要求读取当前目录文件中所有的目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<sys/types.h>
#include<dirent.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
DIR * dir;
struct dirent * ptr;
int i;
dir=opendir("./");
while((ptr = readdir(dir))!=NULL)
{
printf("目录: %s\n",ptr->d_name);
}
closedir(dir);
}

image-20210618151814819

1. 读目录

数据结构
DIR, struct dirent
操作函数
opendir/closedir
readdir
telldir
seekdir

2. 数据结构

DIR

​ 目录流对象的数据结构

​ in <dirent.h>
​ typedef struct __dirstream DIR;

struct dirent

​ 目录项

​ Defined in <dirent.h>

​ ino_t d_ino; /* inode number /
​ char d_name[NAME_MAX + 1]; /
file name */

3. 操作函数

目录的打开、关闭、读、定位
#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
int closedir(DIR *dir);
struct dirent *readdir(DIR *dir);
off_t telldir(DIR *dir);
void seekdir(DIR *dir, off_t offset);

目录扫描程序——ls -R命令

1
2
3
4
5
6
7
8
9
10
11
12
13
DIR *dp;
struct dirent *entry;

if ( (dp = opendir(dir)) == NULL )
err_sys(…);
while ( (entry = readdir(dp)) != NULL ) {
lstat(entry->d_name, &statbuf);
if ( S_ISDIR(statbuf.st_mode) )

else

}
closedir(dp);

mkdir/rmdir 函数

创建一个空目录
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
(Return: 0 if success; -1 if failure)
删除一个空目录
#include <unistd.h>
int rmdir(const char *pathname);
(Return: 0 if success; -1 if failure)

chdir/fchdir 函数

Change working directory
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
(Return: 0 if success; -1 if failure)
当前工作目录是进程的属性,所以该函数只影响调用chdir的进程本身
cd(1) command

4.2 链接文件

ln 命令
link/unlink 函数
给一个文件创建一个链接.
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
(Return: 0 if success; -1 if failure)
删除文件链接
#include <unistd.h>
int unlink(const char *pathname);
(Return: 0 if success; -1 if failure)

ln –s命令
创建一个符号链接
#include <unistd.h>
int symlink(const char *oldpath, const char *newpath);
(Return: 0 if success; -1 if failure)
读取符号链接的值
#include <unistd.h>
int readlink(const char *path, char *buf, size_t bufsiz);
(Return: the count of characters placed in the buffer if success; -1 if failure)

例2.14设计一程序,要求为“/etc/passwd”文件建立软链接“2-14link”,并查看此链接文件和“/etc/passwd”文件。

1
2
3
4
5
6
7
#include<unistd.h>
int main()
{
symlink("/etc/passwd","2-14link");
system("ls 2-14link -l");
system("ls /etc/passwd -l");
}

image-20210618152519176

例2.15设计一程序,要求为“/etc/passwd”文件建立硬链接“2-15link”,并查看此链接文件和“/etc/passwd”文件。

1
2
3
4
5
6
7
#include<unistd.h>
int main()
{
link("./myfile","2-15link");
system("ls 2-15link -l");
system("ls /etc/passwd -l");
}

4.3 设备文件

1.设备文件名

ls –C 列出当前系统加载的设备对应的文件

ls –li 列出当前终端设备的属性

2.设备文件读写

open,read,write,close,stat

例2-16 向终端pts1写入100个“unix”。

1
2
3
4
5
6
7
8
#include<fcntl.h>
int main()
{ int i,fd;
fd=open("/dev/pts/1",O_WRONLY);
for(i=0;i<100;i++)
write(fd, "Unix",4);
close(fd);
}

扩展

fseek()

C 库函数 int fseek(FILE *stream, long int offset, int whence) 设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。

声明

下面是 fseek() 函数的声明。

1
int fseek(FILE *stream, long int offset, int whence)

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • offset – 这是相对 whence 的偏移量,以字节为单位。
  • whence – 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量 描述
SEEK_SET 文件的开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件的末尾

rewind()

将文件指针重新指向文件开头

readdir()

头文件:#include <sys/types.h> #include <dirent.h>

定义函数:struct dirent * readdir(DIR * dir);

函数说明:readdir()返回参数dir 目录流的下个目录进入点。结构dirent 定义如下:
struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};

返回值:成功则返回下个目录进入点. 有错误发生或读取到目录文件尾则返回NULL.

fdopen()

头文件:#include <stdio.h>

定义函数:FILE * fdopen(int fildes, const char * mode);

**函数说明:fdopen()会将参数fildes 的文件描述词, 转换为对应的文件指针后返回.参数mode 字符串则代表着文件指针的流形态, 此形态必须和原先文件描述词读写模式相同. 关于mode 字符串格式请参考fopen(). **

返回值:转换成功时返回指向该流的文件指针. 失败则返回NULL, 并把错误代码存在errno 中.

opendir()

头文件:#include <sys/types.h> #include <dirent.h>

函数:DIR *opendir(const char *name);

含义: opendir()用来打开参数name 指定的目录, 并返回DIR*形态的目录流, 和open()类似, 接下来对目录的读取和搜索都要使用此返回值.

readdir()

头文件:#include<sys/types.h> #include <dirent.h>

函数:struct dirent *readdir(DIR *dir);

含义:readdir()返回参数dir 目录流的下个目录进入点。

struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};

fileno()

功 能:把文件流指针转换成文件描述符
相关函数:open, fopen
表头文件:#include <stdio.h>
定义函数:int fileno(FILE *stream)
函数说明:fileno()用来取得参数stream指定的文件流所使用的文件描述词
返回值 :返回和stream文件流对应的文件描述符。如果失败,返回-1。
范例:
#include <stdio.h>
main()
{
FILE *fp;
int fd;
fp = fopen(“/etc/passwd”, “r”);
fd = fileno(fp);
printf(“fd = %d\n”, fd);
fclose(fp);
}

文件描述词是Linux编程中的一个术语。当一个文件打开后,系统会分配一部分资源来保存该文件的信息,以后对文件的操作就可以直接引用该部分资源了。文件描述词可以认为是该部分资源的一个索引,在打开文件时返回。在使用fcntl函数对文件的一些属性进行设置时就需要一个文件描述词参数。
以前知道,当程序执行时,就已经有三个文件流打开了,它们分别是标准输入stdin,标准输出stdout和标准错误输出stderr。和流式文件相对应的是,也有三个文件描述符被预先打开,它们分别是0,1,2,代表标准输入、标准输出和标准错误输出。需要指出的是,上面的流式文件输入、输出和文件描述符的输入输出方式不能混用,否则会造成混乱。

telldir()

头文件:#include <dirent.h>

定义函数:off_t telldir(DIR *dir);

函数说明:telldir()返回参数dir 目录流目前的读取位置. 此返回值代表距离目录文件开头的偏移量返回值返回下个读取位置, 有错误发生时返回-1.

exit()和_eixt()区别

_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit() 函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。

文件流指针

在应用编程层面,程序对流的操作体现在文件流指针FILE上,在操作一个文件前,需要打开该文件,而使用ANSI C库函数fopen()打开一个文件后,将返回一个文件流指针与该文件关联,所有针对该文件的读写操作都通过该文件流指针完成,以下是应用层所能访问的FILE结构体,因此,结构体成员可以在用户空间中访问。
typedef struct _IO_FILE FILE;
struct _IO_FILE{
int _flags;
char* _IO_read_ptr; //如果以读打开,当前读指针
char* _IO_read_end; //如果以读打开,读区域结束位置
char* _IO_read_base; //Start of putback+get area
char* _IO_write_base; //如果以写打开,写区起始区
char* _IO_write_ptr; //如果以写打开,当前写指针
char* _IO_write_end; //如果以写打开,写区域结束位置
char* _IO_buf_base; //如果显示设置缓冲区,其起始位置
char* _IO_buf_end; //如果显示设置缓冲区,其结束位置。

int _fileno; //文件描述符

}
在此结构体中,包含了I/O库为管理该流所需要的所有信息,如用于实现I/O的文件描述符、指向流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数和出错标志等。

Linux文件存储结构

大部分的Linux文件系统(如ext2、ext3)规定,一个文件由目录项、inode和数据块组成

  • 目录项:包括文件名和inode节点号
  • Inode:又称文件索引节点,包含文件的基础信息以及数据块的指针
  • 数据块:包含文件的具体内容。

Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。

目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码

雨课堂题目整理

image-20210610214812080

image-20210610214821927

image-20210610214834210

image-20210610214845524

image-20210610214854641

image-20210610214906327

image-20210610214915564

!==[image-20210610214926804]==(D:\SyncDisk\笔记整理\Linux\image-20210610214926804.png)

image-20210610214936008

image-20210610214944544

image-20210610215005400

image-20210610215953437

image-20210610220011507

image-20210610220021904

==image-20210610220032690

image-20210610220042546

image-20210610220052131

image-20210610220104981

image-20210610220118232

image-20210610220131685

image-20210610214618550

image.pngimage.pngimage.png

image-20210610214629301

image-20210610214639221

image-20210618143645924

image-20210618143654774

image-20210618143702180

image-20210618143710258

image-20210618143719274

image-20210618143727064

进程编程

1.1. 进程

进程:一个或多个线程执行的地址空间,线程执行时需要系统资源

1.2. 进程启动

System call “fork” 系统调用fork
Process resources
struct task_struct
System space stack

System call “exec”系统调用exec
The entry of C programs C程序入口

1.3. 进程终止

进程终止的五种方式
Normal termination 正常终止
Return from “main” function 从main函数返回
Call “exit” function 调用exit函数
Call “_exit” function 调用_exit函数
Abnormal termination 异常终止
Call “abort” function 调用abort函数
Terminated by a signal 信号终止

1.4. 进程分类

Foreground process前台进程
要求用户启动它们或与它们交互的进程称为前台进程。
前台进程不结束,终端就不会出现系统提示符,直到进程终止。
缺省情况下,程序和命令作为前台进程运行。

Background process 后台进程
独立于用户运行的进程称为后台进程。
用户在输入命令行后加上“&”字符然后按键就启动了后台进程。
Shell不等待命令终止,就立即出现系统提示符,让该命令进程在后台运行,用户可以继续执行新的命令。

Daemon 守护进程
总是运行在后台的系统进程。
守护程序通常在系统启动时启动,并且它们一直运行到系统停止
守护进程常常用于向用户提供各种类型的服务和执行系统管理任务。
守护程序进程由 root 用户或 root shell 启动,并只能由 root 用户停止。

2.1. 进程标识符

image-20210618161417909

2.2. 进程创建

1.fork

fork: create a child process
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
returned value:
pid of child (in the current (parent) process),
0 (in child process),
-1 (failure)

fork创建子进程代码结构

……

pid = fork();

if (pid<0) {perror(“fork()”);exit(1);}

else if (pid==0) { child process }

else { parent process }

文件共享

所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项

image-20210618161638801

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
42
43
44
45
46
int main(int argc,char *argv[]) 
{
pid_t pid;
int fd;
int i=1;
int status;
char *ch1="hello";
char *ch2="world";
char *ch3="IN";
if((fd=open("test.txt",O_RDWR|O_CREAT,0644))==-1)
{ perror("parent open"); exit(EXIT_FAILURE); }
if(write(fd,ch1,strlen(ch1))==-1)
{ perror("parent write"); exit(EXIT_FAILURE); }
if((pid=fork())==-1)
{ perror("fork"); exit(EXIT_FAILURE); }
else
if(pid==0)
{ i=2;
printf("in child\n"); //打印 i 值,以示与父亲进程区别
printf("i=%d\n",i);
if(write(fd,ch2,strlen(ch2))==-1) //写文件 test.txt,与父进程共享
perror("child write");
return 0; }
else
{ sleep(1); //等待子进程先执行
printf("in parent\n");
printf("i=%d\n",i); //打印 i 值,以示与子进程区别
if(write(fd,ch3,strlen(ch3))==-1) //写操作,结果添加到文件后
perror("parent,write");
wait(&status); //等待子进程结束
return 0; }
}
main()
{ int i,j,mark;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
for (j=2;j<i/2;j++)
{ if (i%j==0)
{ mark=0;
break;
}
}
if (mark)
printf("%d is a primer!\n",i);
}
}
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
单个进程
#define LEFT 30000000
#define RIGHT 30000200main()
{ int i,j,mark;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
for (j=2;j<i/2;j++)
{ if (i%j==0)
{ mark=0;
break;
}
}
if (mark)
printf("%d is a primer!\n",i);
}
}
多个进程
main()
{ int i,j,mark;
pid_t pid;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
pid = fork();
if (pid==0)
{ for (j=2;j<i/2;j++)
{ if (i%j==0) { mark=0; break; }
}
if (mark) printf("%d is a primer!\n",i); exit(0);
}
}
exit(0);

fork 应用场合

进程复制自己,使父子进程同一时刻执行不同的代码——网络服务
进程要执行另一个不同的程序:fork-exec——shell
Question:效率问题?父子进程各自占一段逻辑地址空间,fork之后立即exec,地址空间浪费。
“写—复制”

2. vfork

vfork

#include <sys/types.h>

#include <unistd.h>

pid_t vfork(void);

功能:类似fork,创建一个新进程,效率髙。

与fork区别:

(1) vfork创建的进程与父进程共用地址空间

(2) vfork创建子进程后,阻塞父进程,直到子进程调用exec或exit,内核才唤醒父进程。

1
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
int global=5;
main()
{int pid; char *string="I am father:"; int local=10;
printf("before vfork\n");
if((pid=fork())<0) {perror("vfork failed");exit(0);}
if(pid==0)
{string="I am child.";
global++;
printf("%s global=%d, local=%d\n",string,global,local);
exit(0); }
else
{local++;
printf("%s my pid is %d \n" "global=%d\n local=%d\n", string,getpid(),global,local);
exit(0);
}}


int global=5;
main()
{int pid; char *string="I am father:"; int local=10;
printf("before vfork\n");
if((pid=vfork())<0) {perror(“vfork failed”);exit(0);}
//共用进程空间,子进程先执行
if(pid==0)
{string="I am child.";
global++;
printf("%s global=%d, local=%d\n",string,global,local);
exit(0); }
else
{local++;
printf("%s my pid is %d \n" "global=%d\n local=%d\n", string,getpid(),global,local);
exit(0);
}}

sleep函数

函数原型:

#include <unistd.h>

unsigned int sleep(unsigned int seconds);

seconds:暂停时间(秒)

3.exec 系列函数

用一个新的进程映像替换当前的进程映像,执行新程序的进程保持原进程的一系列特征:

pid, ppid, uid, gid, working directory, root directory …

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, …);

int execlp(const char *file, const char *arg, …);

int execle(const char *path, const char *arg, …, char * const envp[]);

int execv(const char *path, char * const argv[]);

int execvp(const char *file, char * const argv[]);

int execve(const char *filename, char * const argv[], char * const envp[]);

1.两大类:

execl开头:参数以列表形式arg0,arg1…NULL结束。

execv开头:参数以指向字符串数组argv[]指针形式,arg[0]必须为程序名。

2.含有字母p的函数可以使用相对路径,根据环境变量PATH查找文件;其他函数必须使用绝对路径;

3.含有字母e的函数,需要通过指向envp[]数组的指针指明新的环境变量,其他函数使用当前环境变量。

1
2
3
4
5
6
7
8
9
10
11
用exec函数使新进程执行“/bin/ps” 程序。
#include<unistd.h>
const char*ps_argv[]={“ps”,”-af”, NULL};
const char *ps_envp[] = {“PATH=/bin:/usr/bin”, “TERM=console”, NULL};
六种情况:
execl(“/bin/ps”,”ps”,”-af”,NULL);
execlp(“ps”,”ps”,”-af”,NULL);
execle(“/bin/ps”,”ps”,”-af”,NULL,ps_envp);
execv(“/bin/ps”,ps_argv);
execvp(“ps”,ps_argv);
execve(“/bin/ps”,ps_argv,ps_envp);

fork和exec一起使用

父子进程各自执行不同的代码,进程要执行另一个程序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
printf("%% ");	/* print prompt  */
while (fgets(buf, MAXLINE, stdin) != NULL) {
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ( (pid = fork()) < 0 )
err_sys(“fork error”);
else if ( pid == 0 ) { /* child */
execlp(buf, buf, (char *) 0);
fprintf(stderr, "couldn't execute: %s", buf);
exit(127);
}
if ( (pid = waitpid(pid, &status, 0)) < 0 ) /* parent */
err_sys(“waitpid error”);
printf("%% ");
}

2.3 进程终止

1.exit函数

函数原型:

#include <stdlib.h>

void exit(int status);

status:进程状态

功能:正常终止目前进程的执行,把参数status返回给父进程,进程所有的缓冲区数据会自动写回并关闭未关闭的文件。

2._exit函数

函数原型:

#include <stdlib.h>

void ——exit(int status);

status:进程状态

功能:立刻终止目前进程的执行,把参数status返回给父进程,并关闭未关闭的文件。不处理标准I/O缓冲区。

2.4 父子进程关系

1. 两种进程概念

父进程在子进程前终止——孤儿进程

​ Orphan process——init

子进程在父进程前终止——可能成为僵尸进程

​ SIGCHLD signal 忽略SIGCHLD信号

​ Handled by wait/waitpid in parent 父进程中用wait/waitpid处理

​ Not handled by wait/waitpid in parent -> zombie父进程没有用wait/waitpid处理->僵尸进程

2. 僵尸进程

僵尸进程:已终止运行,但尚未被清除的进程。

子进程运行结束后(正常或异常),它并没有马上从系统的进程分配表中被删掉,而是进入僵死状态(Zombie),一直等到父进程来回收它的结束状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main()
{ int pid; char *message; int n;
printf("fork program starting\n");
pid=fork();
switch(pid)
{ case -1: exit(0);
case 0: message="this is child"; n=2; break;
default: n=5; message="this is parent"; sleep(60); break;
}
for(;n>0;n--)
{ puts(message);
sleep(n-1);
}
exit(0);
}

**3.**僵尸进程解决方法

僵尸进程的proc结构一直存在直到父进程正常结束或系统重启

如何消除僵尸进程?

方法一:wait/waitpid阻塞父进程,子进程先终止

方法二:父进程不阻塞,两次fork

方法三:使用signal信号处理

方法一:wait example

例3.5设计一个程序,要求复制进程,子进程显示自己的进程号后暂停一段时间,父进程等待子进程正常结束,打印显示等待的进程号和等待的进程退出状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
main()
{ int i,j,mark; pid_t pid;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
pid = fork();
if (pid==0)
{
for (j=2;j<i/2;j++)
if (i%j==0) { mark=0; break; }
if (mark) printf("%d is a primer!\n",i);
sleep(100);
exit(0);
} }
for (i=LEFT;i<=RIGHT;i++)
wait(NULL);
exit(0);
}
wait & waitpid functions

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

waitpid的第一个参数pid的意义:

pid > 0: 等待进程id为pid的子进程。

pid == 0: 等待与自己同组的任意子进程。

pid == -1: 等待任意一个子进程

pid < -1: 等待进程组号为-pid的任意子进程。因此,wait(&stat)等价于waitpid(-1, &stat, 0)

waitpid例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>			
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main ()
{ pid_t pid,wpid; int status,i;
pid=fork();
if(pid==0)
{ printf("这是子进程,进程号(pid)是:%d\n",getpid());
sleep(5); exit(6); }
else
{ printf("这是父进程,正在等待子进程……\n");
wpid=waitpid(pid,&status,0);
i=WEXITSTATUS(status);
printf("等待的进程的进程号(pid)是:%d ,结束状态:%d\n",wpid,i);
}}

方法二:两次fork Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void)
{pid_t pid;
if((pid=fork())<0) perror("fork error");
else if(pid==0) /*first child*/
{ if((pid=fork())<0) perror("fork error");
else if(pid>0) exit(0); /*parent of second child=first child*/
sleep(10); /*second child*/
printf("second child,parent pid=%d\n",getppid());
exit(0);
}
if (waitpid(pid,NULL,0)!=pid) /*wait for first child,parent(the original process)*/
perror("waitpid error");
printf("parent exit");
exit(0);
}

方法三:singnal函数处理

Singnal函数处理:

(1)设置信号处理函数

​ signal(SIGCHLD,fun)

(2)忽略子进程终止信号

​ signal(SIGCHLD,SIG_IGN)

​ 内核回收

4.1 登录方式

终端登录

image-20210618204829654

网络登录

image-20210618204928040

4.2 进程组和会话期

进程组

一个或多个过程的集合

getpgrp/setpgid functions

会话期Session

一个或多个进程组的集合。

getsid/setsid function

setsid函数

image-20210618211351096

image-20210618211443043

4.3 守护进程Daemon

精灵进程或守护进程

后台执行, 没有控制终端或登录 Shell 的进程

ps –aux 命令查看

Init:进程1,启动系统服务

Keventd:为内核中运行的函数提供进程上下文

Kswapd:页面调出守护进程

bdflush,kupdated:调整缓存中的数据写到磁盘

portmap:将RPC程序号映射为端口号

inetd(xinetd):侦听网络接口,获取网络服务进程请求

注意:大多数守护进程都以超级用户(用户ID为0)特权运行。没有一个守护进程具有控制终端,其终端名设置为问号(?)。

ps axj命令查看

daemon特征:

sid,pid,pgid相同,均为pid

ppid为1

tty为?

daemon进程实现规则

编程规则
首先调用fork,然后使父进程exit
调用setsid创建一个新的会话期 setsid()
将当前工作目录更改为特定目录chdir(./)
进程的umask设为0 umask(0)
关闭不需要的文件描述符 close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int daemon_init(void) {
pid_t pid;

if ( (pid = fork()) < 0)
return(-1);
else if (pid != 0)
exit(0); /* parent goes bye-bye */

/* child continues */
setsid(); /* become session leader */
chdir("/tmp"); /* change working directory */
umask(0); /* clear our file mode creation mask */
for(i=0;i<MAXFILE;i++)
close(i); /* close file */
return(0);
}

编写守护进程的要点

(1)创建子进程,终止父进程

pid=fork();

if(pid>0)

{exit(0);} /终止父进程/

(2)在子进程中创建新会话

setsid函数用于创建一个新的会话,并担任该会话组的组长,其作用:

①让进程摆脱原会话的控制;

②让进程摆脱原进程组的控制;

③让进程摆脱原控制终端的控制。

而setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

(3)改变工作目录

改变工作目录的常见函数是chdir。

(4)重设文件创建掩码

文件创建掩码是指屏蔽掉文件创建时的对应位。

把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。

设置文件创建掩码的函数是umask。

(5)关闭文件描述符

通常按如下方式关闭文件描述符:

for(i=0;i<NOFILE;i++)

close(i);

或者也可以用如下方式:

for(i=0;i<MAXFILE;i++)

close(i);

守护进程编写

例3.7 设计两个程序,主程序和初始化程序。要求主程序每隔10秒向/tmp目录中的日志报告运行状态。初始化程序中的init_daemon函数负责生成守护进程。

image-20210618211952186

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
/*主程序每隔一分钟向/tmp目录中的日志3-7.log报告运行状态*/
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <time.h>
#include<stdlib.h>
#include<unistd.h>

void init_daemon(void);
int main()
{
FILE *fp;
time_t t;
init_daemon();
while(1)
{ sleep(10);
if((fp=fopen("./3-7.log","a+")) >=0
{ t=time(0);
fprintf(fp,"守护进程还在运行,时间是: %s",asctime(localtime(&t)) );
fclose(fp);
} } }
void init_daemon(void)
{ pid_t child1,child2; int i;
child1=fork();
if(child1>0) exit(0);
else if(child1< 0)
{ perror("创建子进程失败"); exit(1); }
setsid();
chdir("/tmp");
umask(0);
for(i=0;i< NOFILE;++i) close(i);
return;
}

注意:fopen函数必须具有root权限。如果没有root权限,可以看到守护进程的运行,但不会在文件里写入任何字符。

例3-8:设计一个程序,要求运行后成为守护进程,守护进程又复制出一个子进程,守护进程和它的子进程都调用syslog函数,把结束前的状态写入系统日志文件。

image-20210618212139554

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
42
43
44
45
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<syslog.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/stat.h>
int main()
{
pid_t child1,child2;
int i;
child1=fork();
if(child1>0) /*(1)创建子进程,终止父进程*/
exit(0); /*这是第一子进程,后台继续执行*/
else if(child1< 0)
{
perror("创建子进程失败"); /*fork失败,退出*/
exit(1);
}
setsid(); /*(2)在子进程中创建新会话*/
chdir("/"); /*(3)改变工作目录到“/”*/
umask(0); /*(4)重设文件创建掩码*/
for(i=0;i< NOFILE;++i) /*(5)关闭文件描述符*/
close(i);
openlog("例3-8程序信息",LOG_PID,LOG_DAEMON);/* 调用openlog,打开日志文件*/
child2=fork();
if(child2==-1)
{ perror("创建子进程失败");
exit(1);
}
else if(child2==0)
{ syslog(LOG_INFO,"第二子进程暂停5秒!");
sleep(5); /*睡眠5秒钟*/
syslog(LOG_INFO,"第二子进程结束运行。");
exit(0);
}
else {
waitpid(child2,NULL,0);
syslog(LOG_INFO, "第一子进程在等待第二子进程结束后,也结束运行。");
closelog(); /*调用closelog,关闭日志服务*/
while(1) { sleep(10); }
}
}

注意:调用openlog、syslog函数,操作的系统日志文件“/var/log/syslog”或“/var/log/messages ”,必须具有root权限。

openlog函数说明

image-20210618212254955

syslog函数说明

image-20210618212315332

5.1 信号概念

信号
Software interrupt软件中断
Mechanism for handling asynchronous events异步事件
Having a name (beginning with SIG)
Defined as a positive integer (in <signal.h>)
信号产生
按终端键,硬件异常,kill(2)函数,kill(1)命令,软件条件,…

SIG信号(1-31)是从UNIX系统中继承下来的称为不可靠信号(也称为非实时信号)。
SIGRTMIN~SIGRTMAX是为了解决前面“不可靠信号”问题而进行更改和扩充的信号,称为可靠信号(也称为实时信号)。

可靠信号(实时信号):支持排队,发送用户进程一次就注册一次,发现相同信号已经在进程中注册,也要再注册。
不可靠信号(非实时信号):不支持排队,发送用户进程判断后注册,发现相同信号已经在进程中注册,就不再注册,忽略该信号。

名称 **说明 ** 默认操作
SIGABRT 进程异常终止(调用abort函数产生此信号)
SIGALRM 超时(alarm 终止
SIGFPE 算术运算异常(除以0,浮点溢出等)
SIGHUP 连接断开 终止
SIGILL 非法硬件指令
SIGINT 终端终端符(Clt**-C)** 终止
SIGKILL 立即结束程序运行(不能被捕捉、阻塞或忽略) 终止
SIGPIPE 向没有读进程的管道写数据
SIGQUIT 终端退出符(Clt-\) 终止
SIGTERM 终止(由kill命令发出的系统默认终止信号) 退出
SIGUSR1 用户定义信号 退出
SIGUSR2 用户定义信号 退出
SIGSEGV 无效存储访问(段违例)
SIGCHLD 子进程停止或退出 忽略
SIGCONT 使暂停进程继续 继续/忽略
SIGSTOP 暂停一个进程(不能被捕捉、阻塞或忽略) 终止
SIGTSTP 终端挂起符(Clt-Z) 停止进程
SIGTTIN 后台进程请求从控制终端读
SIGTTOUT 后台进程请求向控制终端写

信号处理

忽略信号
不能忽略的信号:
SIGKILL, SIGSTOP
一些硬件异常信号
执行系统默认动作
捕捉信号

5.2 信号相关命令

Kill

• 暂停 kill –STOP

• 恢复 kill –CONT

• 终止 kill –KILL

函数 功能
kill 发送信号给进程或进程组
raise 发送信号给进程自身
alarm 定时器时间到,向进程发送SIGALRM信号
pause 没有捕捉信号前一直将进程挂起
signal 捕捉信号SIGINT, SIGQUIT时执行信号处理函数
sigemptyset 初始化信号集合为空
sigfillset 初始化信号集合为所有信号集合
sigaddset 将指定信号加入到指定集合
sigdelset 将指定信号从信号集中删除
sigprocmask 判断检测或更改信号屏蔽字

1 信号发送——kill & raise

kill: send signal to a process

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

(Returned Value: 0 if success, -1 if failure)

The “pid” parameter

pid > 0: 发送信号给进程id为pid的进程。

pid =0:发送信号给与自己同组的所有进程。

pid = -1: 发送系统内所有进程

pid < -1: 发送给进程组号为-pid的所有进程。

raise: send a signal to the current process

#include <signal.h>

int raise(int sig);

(Returned Value: 0 if success, -1 if failure)

例3-9 设计一程序,要求用户进程复制出一个子进程,父进程向子进程发出SIGKILL信号,子进程收到此信号,结束子进程的运行。

分析:用户进程fork子进程后,子进程使用raise函数发送SIGSTOP信号,使自己暂停;父进程使用kill函数向子进程发送SIGKILL信号,子进程收到信号结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main ()             	
{ pid_t result; int ret; int newret;
result=fork();
if(result<0)
{ perror("创建子进程失败");
exit(1); }
else if (result==0)
{ raise(SIGSTOP); /*调用raise函数,发送SIGSTOP 使子进程暂停*/
exit(0); }
else
{ printf("子进程的进程号(PID)是:%d\n",result);
if((waitpid(result,NULL,WNOHANG))==0)
{ if((ret=kill(result,SIGKILL))==0)
printf("用kill函数返回值是:%d,发出的SIGKILL信号结束的进程进程号:%d\n",ret,result);
else { perror("kill函数结束子进程失败"); waitpid(result,NULL,0); }
} }
}
Waitpid(pid,NULL,WNOHANG)没有子进程终止立即返回,返回值为0
由此例可知,系统调用kill函数和raise函数,都是简单地向某一进程发送信号。kill函数用于给特定的进程或进程组发送信号,raise函数用于向一个进程自身发送信号。

2 信号处理—— “signal” 函数

为编号为sgn的信号安装一个新的信号处理程序。

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

(Returned Value: the previous handler if success, SIG_ERR if error)

The “handler” parameter

​ a user specified function, or

​ SIG_DFL, or

​ SIG_IGN

1
2
3
4
5
6
7
8
9
10
11
static void sig_usr(int);
int main(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR2");

for ( ; ; )
pause();
}

3 alarm & pause 函数

alarm: 为信号的传送设置闹钟

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

(Returned value: 0, or the number of seconds remaining of previous alarm)

pause: wait for a signal

#include <unistd.h>

int pause(void);

(Returned value: -1, errno is set to be EINTR)

**4.**信号阻塞

有时既不希望进程在接收到信号时立刻中断进程的执行,也不希望此信号完全被忽略掉,而是延迟一段时间再去调用信号处理函数,这个时候就需要信号阻塞来完成。

信号集处理函数

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set,int signum);

int sigdelset(sigset_t *set,int signum);

int sigismember(const sigset_t *set,int signum);

参数:set 信号集 ;signum 信号

返回值:若成功返回0,若出错返回-1。

sigismember若真返回1,若假返回0,若出错返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <signal.h>
#include<stdlib.h>
main()
{ sigset_t *set;
set=(sigset_t*)malloc(sizeof(set));
sigemptyset(set);
sigaddset(set,SIGUSR1);
sigaddset(set,SIGINT);
if((sigismember(set,SIGUSR1))==1) printf("SIGUSR1\n");
if((sigismember(set,SIGUSR2))==1) printf("SIGUSR2\n");
if((sigismember(set,SIGINT))==1) printf("SIGINT\n");
}

信号集处理函数sigprocmask

#include <signal.h>

功能:检测或更改信号屏蔽字

函数:int sigprocmask(int how,const sigsett_t *set,sigset_t *oldset);

参数:how 操作方式set

how决定函数的操作方式。

SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中。

SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合。

SIG_SETMASK:将当前的信号集合设置为信号阻塞集合。

返回值:若成功返回0,若出错返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <signal.h>
#include<stdlib.h>
main()
{
sigset_t *set;
set=(sigset_t*)malloc(sizeof(set));
sigemptyset(set);
sigaddset(set,SIGINT);
sigprocmask(SIG_SETMASK,set,NULL);
//也可以使用:sigprocmask(SIG_BLOCK,set,NULL);
while(1);
}

程序先定义信号集set,然后把信号SIGINT添加到set信号集中,最后把set设置为信号阻塞集合。运行程序时,进程进入死循环。按“ctrl+c”系统并没有中断程序,SIGINT信号屏蔽掉了。按”ctrl+z”来结束程序。

信号实例

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
void handler() 
{ alarm(1); }
main(int argc,char *argv[])
{ char buf[BUFSIZE];
signal(SIGALRM,handler);
alarm(1);
sfd = open(argv[1],O_RDONLY);
/*do
{ sfd = open(argv[1],O_RDONLY);
if (sfd<0)
{ if (errno!= EINTR)
{ perror("open()");
exit(1); } }
}while(sfd<0);*/
while(1)
{ pause();
len = read(sfd,buf,10);
/* while((len = read(sfd,buf,BUFSIZE))<0)
{ if (errno == EINTR)
continue;
perror("read()");
break;
}
if (len==0) break;*/

ret = write(1,buf,len);
/*while(len>0)
{ ret = write(1,buf,len);
if (ret <0)
{ if (errno == EINTR)
continue;
perror("write");
exit(1);
}
len -= ret;
}*/
}
}

6.1 进程间通信

IPC: Inter-Process Communication 进程间通信

IPC机制

​ shared file

​ signal

​ pipe, FIFO (named pipe), message queue, semaphore, shared memory

​ socket

image-20210618215455459

Simple Client-Server or IPC model

image-20210618215509159

6.2 pipe 概念

Pipe

Pipe mechanism in a shellShell中的管道机构

​ e.g. cmd1 | cmd2

Pipe is half-duplex 半双工

管道只能在共同祖先的进程间使用

管道也是文件

命名管道(FIFO)

pipe 函数

The pipe function: create a pipe

#include <unistd.h>

int pipe(int filedes[2]);

(Returned value: 0 if success, -1 if failure)

A pipe: First In, First Out

filedes[0]:read, filedes[1]: write

单个进程使用管道

image-20210619121308293

父子进程使用管道

image-20210619121321411

管道使用(1):单个进程

类似共享文件

pipe—write——read

管道使用(2)多进程

父-子进程/子-子进程

使用模式:

pipe——fork——write(写进程)

​ ——read(读进程)

注意:pipe-fork顺序

管道使用(3)用于标准输入输出

管道:shell中的形式

​ cmd1 | cmd2

​ 重定向 cmd > file

实现代码

​ 执行cmd1前

​ if (fd[1] != STDOUT_FILENO) {

​ if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)

​ err_sys(“dup2 error to stdout);

​ }

执行cmd2前

​ if (fd[0] != STDIN_FILENO) {

​ if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO)

​ err_sys(“dup2 error to stdin);

​ }

image-20210619121557088

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{ int data_processed; int file_pipes[2];
const char some_data[] = "123"; pid_t fork_result;
if (pipe(file_pipes) == 0)
{ fork_result = fork();
if (fork_result == (pid_t)-1)
{ fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE); }
if (fork_result == (pid_t)0)
{ close(0);
dup(file_pipes[0]);
close(file_pipes[0]);
close(file_pipes[1]);
execlp("od", "od", "-c", (char *)0);
exit(EXIT_FAILURE);
}
else
{ close(file_pipes[0]);
data_processed = write(file_pipes[1], some_data,strlen(some_data));
close(file_pipes[1]);
printf("%d - wrote %d bytes\n", (int)getpid(),data_processed);
}
} exit(EXIT_SUCCESS);
}

od –c file 以字符方式显示文件内容,如果没指定文件则以标准输入作为默认输入

6.3 popen & pclose 函数

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

image-20210619121713195

fp=popen(cmd,”r”)的pipe实现:
FILE * popen(char * cmd,char * type)
{ int fd; int p;
pipe(fd);
p=fork();
if(p==0) { close(1);dup(fd[1]);
execl(cmd,cmd,NULL); }
else { wait();
fp=fdopen(fd[0],”r”); return fp; }
}

image-20210619121805879

例3-13 设计一程序,要求用popen创建管道,实现“ls –l|grep fifo”的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main ()	
{ FILE *fp; int num; char buf[500];
memset(buf,0,sizeof(buf));
printf("建立管道……\n");
fp=popen("ls -l","r");
if(fp!=NULL)
{ num=fread(buf,sizeof(char),500,fp);
if(num>0)
{ printf("第一个命令是“ls–l”,运行结果如下:\n");
printf("%s\n",buf); }
pclose(fp); }
else
{ printf("用popen创建管道错误\n");
return 1; }
fp=popen(“grep fifo","w");
printf("第二个命令是“grep fifo”,运行结果如下:\n");
fwrite(buf,sizeof(char),500,fp);
pclose(fp);
return 0;
}

使用popen函数读写管道,实际上也是调用pipe函数建立一个管道,再调用fork函数建立子进程,接着会建立一个shell环境,并在这个shell环境中执行参数指定的进程。

例3-14 在程序中获得另一个程序的输出

popen_two.c

gcc myuclc.c -o myuclc

gcc popen_two.c -o two

输入大写字母

按Ctrl+D结束程序

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
程序头文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
char line[MAXLINE]; FILE *fpin;
fpin = popen("./myuclc", "r");
if( NULL == fpin ) error_quit("popen error");
while( 1 )
{
fputs("prompt> ", stdout);
fflush(stdout);
if( fgets(line, MAXLINE, fpin) == NULL ) break;
if( fputs(line, stdout) == EOF ) perror("fputs error to pipe");
}
if( pclose(fpin) == -1 ) perror ("pclose error");
putchar('\n');
return 0;
}
//Myuclc.c
int main()
{
int c;
while( 1 )
{
c = getchar();
if( EOF == c )
break;
if( isupper(c) )
c = tolower(c);
putchar(c);
if( '\n' == c )
fflush(stdout);
}
return 0;
}

6.4 FIFO: named pipe命名管道

管道和命名管道
相同点
不同点
文件系统中
内存传输数据
同步:一个重要的考虑
mkfifo(1), mkfifo(3), mknod(1), mknod(2)

创建FIFO

命令行方式:mknod filename p

​ mkfifo filename

程序方式:mkfifo: make a FIFO special file (a named pipe)

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

(Returned value: 0 if success, -1 if failure)

例: mkfifo(“/tmp/myfifo”,0666)

访问FIFO

命令行方式

(1)cat < /tmp/myfifo 读FIFO文件

(2)echo hello >/tmp/myfifo 向FIFO写数据

(3)cat < /tmp/myfifo &

​ echo “hello” >/tmp/myfifo

程序方式:用open打开一个FIFO

Review: “open” system call

int open(const char *pathname, int flags);

“flags” parameter

​ 必须指定的互斥模式:

​ O_RDONLY, O_WRONLY, O_NONBLOCK

​ O_RDONLY:若无进程写方式打开FIFO,open阻塞

​ O_RDONLY |O_NONBLOCK:若无进程写方式打开FIFO,open立即返回文件描述符

​ O_WRONLY:若无进程读方式打开FIFO,open阻塞

​ O_WRONLY| O_NONBLOCK:若无进程读方式打开FIFO,open返回ENXIO错误,-1

例3-15 两个程序通过FIFO传递数据,一个生产者程序创建并打开一个FIFO,向管道中写入数据。(3-15fifo_p.c)一个消费者程序,从FIFO中读取数据(3-15fifo_c.c)。

1
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
42
43
44
45
46
47
48
49
50
51
52
#define FIFO_NAME "/tmp/my_fifo“
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)
int main()
{ int pipe_fd; int res; int open_mode = O_WRONLY;
int bytes_sent = 0; char buffer[BUFFER_SIZE + 1];
if (access(FIFO_NAME, F_OK) == -1)
/*int access(const char *filenpath, int mode)
R_OK 只判断是否有读权限
W_OK 只判断是否有写权限
X_OK 判断是否有执行权限
F_OK 只判断是否存在
有效,则函数返回0,否则函数返回-1
*/
{ res = mkfifo(FIFO_NAME, 0777);
if (res != 0) { fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME); exit(EXIT_FAILURE); }
}
printf("Process %d opening FIFO O_WRONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if (pipe_fd != -1)
{ while(bytes_sent < TEN_MEG)
{ res = write(pipe_fd, buffer, BUFFER_SIZE);
if (res == -1) { fprintf(stderr, "Write error on pipe\n"); exit(EXIT_FAILURE); }
bytes_sent += res; }
(void)close(pipe_fd); }
else { exit(EXIT_FAILURE); }
printf("Process %d finished\n", getpid());
exit(EXIT_SUCCESS);
}

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
int main()
{ int pipe_fd; int res; int open_mode = O_RDONLY; char buffer[BUFFER_SIZE + 1];
int bytes_read = 0;
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if (pipe_fd != -1) {
do { res = read(pipe_fd, buffer, BUFFER_SIZE);
bytes_read += res;
} while (res > 0);
(void)close(pipe_fd);
}
else {
exit(EXIT_FAILURE); }
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
exit(EXIT_SUCCESS);
}

FIFO的应用(1)

用FIFO复制输出流

image-20210619124043625

mkfifo fifo1

prog3 < fifo1 &

prog1 < infile | tee fifo1 | prog2

tee命令读取标准输入,把这些内容同时输出到标准输出和(多个)文件中

image-20210619124101884

FIFO的应用(2)

C/S应用程序

image-20210619124145265

例3-16 client.c, server.c

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//Client.h
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define SERVER_FIFO_NAME "/tmp/serv_fifo"
#define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"
#define BUFFER_SIZE 20
struct data_to_pass_st {
pid_t client_pid;
char some_data[BUFFER_SIZE - 1];
};
int main() // 客户端程序
{ int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data;
int times_to_send; char client_fifo[256];
server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);
if (server_fifo_fd == -1) {
fprintf(stderr, "Sorry, no server\n");
exit(EXIT_FAILURE); }
my_data.client_pid = getpid();
sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);
if (mkfifo(client_fifo, 0777) == -1) {
fprintf(stderr, "Sorry, can't make %s\n", client_fifo);
exit(EXIT_FAILURE); }
for (times_to_send = 0; times_to_send < 5; times_to_send++) {
sprintf(my_data.some_data, "Hello from %d", my_data.client_pid);
printf("%d sent %s, ", my_data.client_pid, my_data.some_data);
write(server_fifo_fd, &my_data, sizeof(my_data));
client_fifo_fd = open(client_fifo, O_RDONLY);
if (client_fifo_fd != -1) {
if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0) {
printf("received: %s\n", my_data.some_data); }
close(client_fifo_fd); } }
close(server_fifo_fd);
unlink(client_fifo);
exit(EXIT_SUCCESS);}
int main() //服务端程序
{ int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data; int read_res; char client_fifo[256]; char *tmp_char_ptr;
mkfifo(SERVER_FIFO_NAME, 0777);
server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);
if (server_fifo_fd == -1) {
fprintf(stderr, "Server fifo failure\n");
exit(EXIT_FAILURE); }
sleep(10); /* lets clients queue for demo purposes */
do { read_res = read(server_fifo_fd, &my_data, sizeof(my_data));
if (read_res > 0) {
tmp_char_ptr = my_data.some_data;
while (*tmp_char_ptr) {
*tmp_char_ptr = toupper(*tmp_char_ptr);
tmp_char_ptr++; }
sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);
client_fifo_fd = open(client_fifo, O_WRONLY);
if (client_fifo_fd != -1) {
write(client_fifo_fd, &my_data, sizeof(my_data));
close(client_fifo_fd); }
} } while (read_res > 0);
close(server_fifo_fd);
unlink(SERVER_FIFO_NAME);
exit(EXIT_SUCCESS);}

例3-17 l设计两个程序,要求用命名管道FIFO实现简单的聊天功能。

Zhang.c

Li.c

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
42
43
44
45
46
int main()
{ int i, rfd,wfd,len=0,fd_in; char str[32]; int flag,stdinflag; fd_set write_fd,read_fd; struct timeval net_timer;
mkfifo("fifo1",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
mkfifo("fifo2",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
wfd =open("fifo1",O_WRONLY); rfd =open("fifo2",O_RDONLY); if(rfd<=0||wfd<=0)return 0; printf(“这是张三端!\n");
while(1)
{ FD_ZERO(&read_fd);
FD_SET(rfd,&read_fd);
FD_SET(fileno(stdin),&read_fd); /**/
net_timer.tv_sec=5;
net_timer.tv_usec=0;
memset(str,0,sizeof(str));
if(i=select(rfd+1, &read_fd,NULL, NULL, &net_timer) <= 0) continue;
if(FD_ISSET(rfd,&read_fd))
{ read(rfd,str,sizeof(str)); printf("----------------------------\n");
printf("李四:%s\n",str); }
if(FD_ISSET(fileno(stdin),&read_fd))
{ printf("----------------------------\n"); fgets(str,sizeof(str),stdin);
len=write(wfd,str,strlen(str));
} }
close(rfd);
close(wfd);}

int main()
{ int i, rfd,wfd,len=0,fd_in; char str[32]; int flag,stdinflag;
fd_set write_fd,read_fd; struct timeval net_timer;
mkfifo("fifo1",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
mkfifo("fifo2",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
rfd =open("fifo1",O_RDONLY); wfd =open("fifo2",O_WRONLY);
if(rfd<=0||wfd<=0)return 0; printf("这是李四端!\n");
while(1)
{ FD_ZERO(&read_fd); FD_SET(rfd,&read_fd);
FD_SET(fileno(stdin),&read_fd); net_timer.tv_sec=5;
net_timer.tv_usec=0; memset(str,0,sizeof(str));
if(i=select(rfd+1,&read_fd,NULL, NULL, &net_timer) <= 0) continue;
if(FD_ISSET(rfd,&read_fd))
{ read(rfd,str,sizeof(str));
printf("----------------------------\n");
printf("张三:%s\n",str); }
if(FD_ISSET(fileno(stdin),&read_fd))
{ printf("----------------------------\n");
fgets(str,sizeof(str),stdin);
len=write(wfd,str,strlen(str)); /*写入管道*/ }
} close(rfd);
close(wfd);
}

7.1 System V IPC的共同特征

进程间通信(interprocess communication)

IPC objects

  • 信号量(semaphore set)

  • 消息队列(message queue)

  • 共享内存(shared memory)

shell命令

  • ipcs -q/-m/-s

  • ipcrm –q/-m/-s

  • ipcrm -Q/-M/-S

标识符与关键字
创建IPC对象时指定关键字(key_t key;)
key的选择;预定义常数IPC_PRIVATE;ftok函数
引用IPC对象:标识符
内核将关键字转换成标识符
许可权结构
和文件类比
struct ipc_perm

SV IPC System Calls Overview

功能 消息队列 信号量 共享内存
分配一个IPC对象,获得对IPC的访问 msgget semget shmget
IPC操作: 发送/接收消息,信号量操作,连接/释放共享内存 msgsnd/ msgrcv semop shmat/ shmdt
IPC控制:获得/修改状态信息,取消IPC msgctl semctl shmctl

ftok函数

创建函数

key_t ftok( char * filename, int id);

功能说明

将一个已存在的文件名(该文件必须是存在而且可以访问的)和一个整数标识符id转换成一个key_t值

在Linux系统实现中,调用该函数时,系统将文件的索引节点号取出,并在前面加上子序号,从而得到key_t的返回值

创建IPC对象

key:可由ftok()函数产生或定义为IPC_PRIVATE常量

flag:包括读写权限,还可包含IPC_CREATE和IPC_EXCL标志位,组合效果如下

image-20210619133437323

7.2 Message queue

消息队列
消息队列是消息的链表,存放在内核中并由消息队列标识符标识。
First in, first out
message type: 优先级=
块数据

消息队列——程序结构

proto.h:约定的消息队列通信格式
send.c
receive.c

proto.h:约定的消息队列通信格式:

#define KEYPATH “/etc/services”

#define KEYPROJ ‘a’

struct msg_st {

​ char * msg;

​ long type;

​ …

}

消息队列——系统函数

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

int msgget(key_t key, int flag);
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
int msgctl(int msqid, int cmd, struct shmid_ds *buf);

msgget

Ø函数原型 int msgget(key_t key, int flag);

Ø参数说明

ü key:待创建/打开队列的键值

ü flag:创建/打开方式

​ 常取IPC_CREAT|IPC_EXCL|0666

​ 若不存在key值的队列,则创建;否则,若存在,则打开队列

​ 0666表示与一般文件权限一样

Ø返回值

​ 成功返回消息队列描述字,否则返回-1

Ø说明

IPC_CREAT一般由服务器程序创建消息队列时使用

若是客户程序,须打开现有消息队列,而不用IPC_CREAT

msgsnd

Ø函数原型

int msgsnd(int msqid, struct msgbuf *msgp, size_t size, int flag);

Ø说明

flag有意义的标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待

Ø内核须对msgsnd( )函数完成的工作

ü检查消息队列描述符、许可权及消息长度

​ 若合法,继续执行;否则,返回

ü内核为消息分配数据区,将消息正文拷贝到消息数据区

ü分配消息首部,并将它链入消息队列的末尾

ü修改消息队列头数据,如队列中的消息数、字节总数等

ü唤醒等待消息的进程

msgrcv

Øint msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);

Ø参数说明

l msgid:消息队列描述字

l msgp:消息存储位置

l size:消息内容的长度

l type:请求读取的消息类型

l flag:规定队列无消息时内核应做的操作

​ IPC_NOWAIT:无满足条件消息时返回,此时errno=ENOMSG

​ IPC_EXCEPT:type>0时使用,返回第一个类型不为type的消息

​ IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将丢失

msgctl: message control operations

函数原型
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
“cmd” 参数
IPC_STAT: 把msqid_ds结构中的数据置为消息队列的当前关联值
IPC_SET: 在进程有足够权限的前提下,把消息队列的当前关联值置为msqid_ds结构给出的值
IPC_RMID: 删除消息队列

消息队列——Example

A C/S application
一台服务器,多个客户机:只需要一个队列。与FIFO实现相比

消息队列属性

消息队列创建后,操作系统在内核中分配了一个名称为msqid_ds的数据结构用于管理该消息队列。
在程序中可以通过函数msgctl对该结构进行读写,从而实现对消息队列的控制功能。

成员说明:
1)msg_perm:IPC许可权限结构。
2)msg_stime:最后一次向该消息队列发送消息(msgsnd)的时间。
3)msg_rtime:最后一次从该消息队列接收消息(msgrcv)的时间。
4)msg_ctime:最后一次调用msgctl的时间。
5)msg_cbytes:当前该消息队列中的消息长度,以字节为单位。
6)msg_qnum:当前该消息队列中的消息条数。
7)msg_qbytes:该消息队列允许存储的最大长度,以字节为单位。
8)msg_lspid:最后一次向该消息队列发送消息(msgsnd)的进程ID。
9)msg_lrpid:最后一次从该消息队列接收消息(msgrcv)的进程ID。

使用:
msg_sinfo.msg_qbytes = 1666;
msgctl(msgqid,IPC_SET,&msg_sinfo)

例3-18 msg_stat.c

image-20210619133908134

image-20210619133920223

image-20210619133947032

image-20210619133957944

image-20210619134026407

7.3 Shared memory

共享内存是内核为进程创建的一个特殊内存段,它可连接(attach)到自己的地址空间,也可以连接到其它进程的地址空间

​ 最快的进程间通信方式

​ 不提供任何同步功能

image-20210619134117718

共享内存实现途径

mmap()系统调用

将普通文件在不同的进程中打开并映射到内存

不同进程通过访问映射来达到共享内存目的

POSIX共享内存机制(Linux 2.6未实现)

System V共享内存

在内存文件系统tmpfs中建立文件

文件映射到不同进程空间

mmap()

lmmap()系统调用使得==进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。==

#include < unistd.h >

#include <sys/mman.h >

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

int munmap( void * addr, size_t len )

int msync ( void * addr , size_t len, int flags)

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

addr:指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。

len:映射到调用进程地址空间的字节数,从被映射文件开头offset个字节开始算起。

prot :指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。

flags;指定映射对象的类型,映射选项和映射页是否可以共享。由以下几个常值指定:MAP_SHARED , MAP_PRIVATE 必选其一。

fd:为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANONYMOUS,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,只能用于具有亲缘关系的进程间通信)。

offset参数一般设为0,表示从文件头开始映射。

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

mmap()用于共享内存的两种方式 :

(1)使用普通文件提供的内存映射:适用于任何进程之间;需要打开或创建一个文件,然后再调用mmap();调用代码如下:

fd=open(name, flag, mode);

if(fd>0)

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE,

​ MAP_SHARED , fd , 0);

(2) 使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;

l调用代码如下:

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS ,-1 , 0);

由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。在调用fork()之后,子进程继承父进程匿名映射后的地址空间,也继承mmap()返回的地址

不必指定具体的文件,只要设置相应的标志即可

munmap()

int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

msync()

int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

例3-19mmap.c l设计一个程序,要求复制进程,父子进程通过匿名映射实现共享内存。

1
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
typedef struct     	
{ char name[4];
int age;
}people;
main(int argc, char** argv)
{ pid_t result; int i; people *p_map; char temp;
p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
result=fork();
if(result<0)
{ perror("创建子进程失败"); exit(0); }
else if (result==0)
{ sleep(2);
for(i = 0;i<5;i++)
printf("子进程读取: 第 %d 个人的年龄是: %d\n",i+1,(*(p_map+i)).age);
(*p_map).age = 110;
munmap(p_map,sizeof(people)*10); /*解除内存映射关系*/
exit(0);
}
else {
temp = 'a';
for(i = 0;i<5;i++)
{ temp += 1;
memcpy((*(p_map+i)).name, &temp,1);
(*(p_map+i)).age=20+i;
}
sleep(5);
printf( "父进程读取: 五个人的年龄和是: %d\n",(*p_map).age );
printf("解除内存映射……\n");
munmap(p_map,sizeof(people)*10);
printf("解除内存映射成功!\n");
}
}

使用特殊文件提供匿名内存映射,适用于具有亲缘关系的进程之间。==一般而言,子进程单独维护从父进程继承下来的一些变量。而mmap函数的返回地址,由父子进程共同维护。==

System V IPC共享内存的实现

通过映射到tmpfs中的shm文件对象实现共享主存

  • 每个共享主存区对应tmpfs中的一个文件(通过shmid_kernel关联)

创建过程

  1. 从主存申请共享主存管理结构,初始化相应shmid_kernel结构
  2. 在tmpfs中创建并打开一个同名文件
  3. 在主存中建立与该文件对应的dentry及inode结构
  4. 返回相应标识符

System V shared memory

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget(key_t key, int size, int flag);

void *shmat(int shmid, void *addr, int flag);

int shmdt(void *addr);

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

mmap和System V

1.System V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。

2.System V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

7.4 信号量

用来协调进程对共享资源的访问

相关函数semget,semop,semctl

所需头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

相关函数——semget

创建一个新信号量或取得一个已有信号量,原型为:int semget(key_t key, int num_sems, int sem_flags);

参数key****:整数值

参数num_sems:指定需要的信号量数目,几乎总是1。

参数sem_flags:一组标志,信号量不存在时创建一个新的信号量,指定IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。

指定IPC_CREAT | IPC_EXCL,创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

返回值:成功返回一个相应信号标识符(非零),失败返回-1.

相关函数——semop

操作一个或一组信号 ,原型为:

int semop(int sem_id, struct sembuf *sem_opa, size_t nsops);

​ semid:信号集的识别码,可通过semget获取。

​ sem_opa:指向存储信号操作结构的数组指针,信号操作结构的原型如下

struct sembuf{

short sem_num; //信号量集中的信号量编号0,1……

short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,一个是+1。

short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号并在进程没有释放该信号量而终止时,操作系统释放信号量

};

lnsops:信号量操作结构的数量,大于或等于1

相关函数——semctl

该函数用来直接控制信号量信息,它的原型为

int semctl(int sem_id, int sem_num, int command, …);

第四个参数,它通常是一个union semum结构,定义如下:

union semun{

int val;

struct semid_ds *buf;

unsigned short *arry;

};

例3-20sem1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char *argv[])  
{ char message = 'X'; int i = 0;
if(argc > 1)
message = argv[1][0];
for(i = 0; i < 10; ++i)
{ printf("%c", message);
fflush(stdout);
sleep(rand() % 3);
printf("%c", message);
fflush(stdout);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
exit(EXIT_SUCCESS);
}

例3-21sem2.c

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
static int set_semvalue()  
{ union semun sem_union;
sem_union.val = 1; //用于初始化信号量
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}

static void del_semvalue()
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{ //对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}

static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
union semun
{ int val; struct semid_ds *buf; unsigned short *arry; };
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{ char message = 'X'; int i = 0;
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); //创建信号量
if(argc > 1)
{ if(!set_semvalue()) //程序第一次被调用,初始化信号量
{ fprintf(stderr, “Failed to initialize semaphore\n”);
exit(EXIT_FAILURE); }
message = argv[1][0]; //设置要输出到屏幕中的信息,即其参数的第一个字符
sleep(2);
}
for(i = 0; i < 10; ++i)
{ if(!semaphore_p()) //进入临界区 前执行P操作
exit(EXIT_FAILURE);
printf("%c", message); fflush(stdout); sleep(rand() % 3);
printf("%c", message); //离开临界区前再一次向屏幕输出数据
fflush(stdout);
if(!semaphore_v())
exit(EXIT_FAILURE); //离开临界区,休眠随机时间后继续循环
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if(argc > 1) { sleep(3); del_semvalue(); }
//如果程序是第一次被调用,则在退出前删除信号量
exit(EXIT_SUCCESS);
}

雨课堂题目整理

image-20210610220213435

image-20210610220229500

image-20210610220240603

image-20210610220250059

image-20210610220300339

image-20210610220310461

image-20210610220321107

image-20210610220333714

image-20210610220410424

image-20210610220418478

image-20210610220429370

image-20210610220438832

image-20210610220449449

image-20210610220502760

image-20210610220526628

image-20210610220536612

image-20210610220544837

image-20210610220603324

image-20210610220612511

image-20210610220619429

image-20210610220627025

image-20210610220637248

image-20210610220655591

image-20210610220705258

image-20210610220715535

image-20210610220730081

image-20210610220748981

image-20210610220804988

image-20210610220811818

image-20210610220821845

image-20210610220829105

image-20210610220836629

image-20210610220844183

image-20210610220851954

image-20210610220908753

image-20210610220915232

image-20210610220927300

image-20210610220935403

image-20210610220943592

image-20210610220952495

image-20210610220959961

image-20210610221006902

image-20210610221014835

image-20210610221021551

image-20210610221029470

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <unistd.h>

#include <sys/mman.h>

#include <fcntl.h>

#include <stdio.h>

#include<string.h>

#include<stdlib.h>



typedef struct

{

char name[4];

int num;

} number;



main(int argc, char** argv)

{

FILE *fp;

pid_t result;

int i;

number *p_map;

char temp;

fp=fopen("/etc/passwd","r");

p_map=(number*)mmap(NULL,sizeof(number)*10000,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS ,fp,0);

result=fork();

if(result<0)

{

perror("创建子进程失败");

exit(0);

}

else if (result==0)

{

printf("子进程开始读取");

char tempC;

int temp=1,length=0;

while(1)

{

temp = read(fp, &tempC, sizeof(char));

printf("%c",tempC);

if(temp==0)

{



break;

}

else if(tempC=='a')

{

readSize = readSize+1;

printf("子进程读取: 第 %d 个a\n",++i);

}



}

printf("子进程读取wan: 第 %d 个a\n",++i);

(*p_map).num = readSize;

munmap(p_map,sizeof(number)*10);

exit(0);

}

else

{

sleep(5);

printf( "父进程读取: a的个数是: %d\n",(*p_map).num );

printf("解除内存映射……\n");

munmap(p_map,sizeof(number)*10000);

printf("解除内存映射成功!\n");

}

}

网络编程

0 服务器和客户机的信息函数

**1.**字节序列转换

每一台机器内部对变量的字节存储顺序不同,而网络传输的数据是一定要统一顺序的。所以对内部字节表示顺序与网络字节顺序(大端)不同的机器,一定要对数据进行转换

真正转换还是不转换是由系统函数自己来决定的。

头文件:include <arpa/inet.h>

unsigned short int htons(unsigned short int hostshort):
主机字节顺序转换成网络字节顺序,对无符号短型进行操作4bytes
unsigned long int htonl(unsigned long int hostlong):
主机字节顺序转换成网络字节顺序,对无符号长型进行操作8bytes
unsigned short int ntohs(unsigned short int netshort):
网络字节顺序转换成主机字节顺序,对无符号短型进行操作4bytes
unsigned long int ntohl(unsigned long int netlong):
网络字节顺序转换成主机字节顺序,对无符号长型进行操作8bytes

**2.**地址格式转换

头文件:#include <sys/types.h>   

​ #include <sys/socket.h>   

​ #include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);

转换字符串到网络地址 。 返回:1成功;-1出错

const char* inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

转换网络二进制结构到ASCII类型的地址

返回:成功,指向结果的指针;出错,NULL

1 Socket 基础

socket是网络编程的一种接口,它是一种特殊的I/O,用socket函数建立一个Socket连接,此函数返回一个整型的socket描述符,随后进行数据传输。

一个IP地址,一个通讯端口,就能确定一个通讯程序的位置。为此开发人员专门设计了一个套接结构,就是把网络程序中所用到的网络地址和端口信息放在一个结构体中。

一般套接口地址结构都以“sockaddr”开头。socket根据所使用的协议的不同可以分TCP套接口和UDP套接口,又称为流式套接口和数据套接口。

UDP是一个无连接协议,TCP是个可靠的端对端协议。传输UDP数据包时,LINUX不知道也不关心它们是否已经安全到达目的地,而传输TCP数据包时,则应先建立连接以保证传输的数据被正确接收。

三种类型套接字

流套接字(SOCK_STREAM)

​ 可靠的、面向连接的通信。

​ 使用TCP协议

数据报套接字(SOCK_DGRAM)

​ 无连接服务

​ 使用UDP协议

原始套接字(SOCK_RAW)

​ 允许对底层协议如IP、ICMP直接访问

1. socket套接字的数据结构

两个重要的数据类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,如IP地址、通信端口等。

sockaddr——虚拟定义的地址(取决于协议):

1
2
3
4
5
6
struct sockaddr
{ unsigned short sa_family;
/*可以为AF_INET,代表TCP/IP地址族*/
char sa_data[14];
/*14个字节,包含IP地址和端口号*/
};

sockaddr_in(AF_INET中使用的地址定义):

1
2
3
4
5
6
7
8
9
struct sockaddr_in
 { short sin_family; /*AF_INET(地址族)*/
 unsigned short sin_port;
/*端口号(必须要采用网络数据格式)*/
 struct in_addr sin_addr;
/*网络字节序的IP地址*/
  unsigned char sin_zero[8];
/*与SOCKADDR结构保持同样大小*/
 };

2. 基于连接的服务

image-20210619170114870

Server程序的作用

程序初始化

持续监听一个固定的端口

收到Client的连接后建立一个socket连接

与Client进行通信和信息处理

​ 接收Client通过socket连接发送来的数据,进行相应处理并返回处理结果

​ 通过socket连接向Client发送信息

通信结束后中断与Client的连接

Client程序的作用

程序初始化

连接到某个Server上,建立socket连接

与Server进行通信和信息处理

​ 接收Server通过socket连接发送来的数据,进行相应处理

​ 通过socket连接向Server发送请求信息

通信结束后中断与Server的连接

3. 无连接的服务

image-20210619170325054

UDP编程的适用范围

部分满足以下几点要求时,应该用UDP

​ 面向数据报

​ 网络数据大多为短消息

​ 拥有大量Client

​ 对数据安全性无特殊要求

​ 网络负担非常重,但对响应速度要求高

例子:ICQ、视频点播

具体编程时的区别

socket()的参数不同

UDP Server不需要调用listen和accept

UDP收发数据用sendto/recvfrom函数

TCP:地址信息在connect/accept时确定

UDP:在sendto/recvfrom函数中每次均需指定地址信息

UDP:shutdown函数无效

2 TCP编程

基于TCP协议的编程,其最主要的特点是建立完连接后,才进行通信。

常用的基于TCP网络编程函数及功能

头文件

#include <sys/types.h>

#include <sys/socket.h>

image-20210619170614129

1.基于TCP网络编程函数

socket: 创建用于通信的端点并返回描述符.

​ int socket(int domain, int type, int protocol);

“domain” parameter

​ 指定通信域,即选择协议族,如 AF_INET,AF_INET6 …

“type” parameter

​ 指定通信语义。 三种主要类型: SOCK_STREAM, SOCK_DGRAM, SOCK_RAW

“protocol” parameter

​ usually 0 (default).

bind: binds a name to a socket

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

struct sockaddr {

sa_family_t sa_family;

char sa_data[14];

}

inet_aton & inet_ntoa

互联网地址操作例程

​ int inet_aton(const char *cp, struct in_addr *inp);

​ char* inet_ntoa (struct in_addr in);

inet_ntoa将一个32位数字表示的IP地址转换成点分十进制IP地址字符串

listen

listen: listen for connections on a socket

int listen(int s, int backlog);

参数说明:

s:socket()返回的套接口文件描述符。

backlog:进入队列中允许的连接的个数。进入的连接请求在使用系统调用accept()应答之前要在进入队列中等待。这个值是队列中最多可以拥有的请求的个数。大多数系统的缺省设置为20。

accept

accept()函数将响应连接请求,建立连接

int accept(int sockfd,struct sockaddr *addr,int *addrlen);

accept缺省是阻塞函数,阻塞直到有连接请求

sockfd: 被动(倾听)的socket描述符

如果成功,返回一个新的socket描述符(connected socket descriptor)来描述该连接。这个连接用来与特定的Client交换信息

addr将在函数调用后被填入连接对方的地址信息,如对方的IP、端口等。

connect

connect: initiate a connection on a socket (connect to a server).

int connect(int sockfd, struct sockaddr *servaddr, int addrlen);

主动的socket

servaddr是事先填写好的结构,Server的IP和端口都在该数据结构中指定。

**send/**recv

send/recv: 面向连接

int send(int s, const void *msg, size_t len, int flag);

s:发送数据的套接口文件描述符。

msg:发送的数据的指针

len:数据的字节长度

flag:标志设置为0。

int recv(int s, void *buf, size_t len, int flag);

s:读取的套接口文件描述符。

buf:保存读入信息的地址。

len:缓冲区的最大长度。

flag:设置为0。

sendto/recvfrom

int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socketlen_t tolen);

int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

close & shutdown

close

​ int close(int sockfd);

shutdown

​ int shutdown(int sockfd, int how);

​ how: SHUT_RD, SHUT_WR, SHUT_RDWR

shutdown直接对TCP连接进行操作,close只是对套接字描述符操作。

例4.1:服务器通过socket连接后,向客户端发送字符串“连接上了”。在服务器上显示客户端的IP地址或域名。

image-20210619171521320

主要语句说明:
服务端
建立socket:socket(AF_INET, SOCK_STREAM, 0);
绑定bind:bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr);
建立监听listen:listen(sockfd, BACKLOG);
响应客户请求:accept(sockfd,(struct sockaddr *)&remote_addr, &sin_size);
发送数据send:send(client_fd, “连接上了 \n”, 26, 0);
关闭close:close(client_fd);

客户端:
建立socket:socket(AF_INET, SOCK_STREAM, 0);
请求连接connect:connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr));
接收数据recv:recv(sockfd, buf, MAXDATASIZE, 0);
关闭close:close(sockfd);

1
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//服务端源程序代码
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#define SERVPORT 3333
#define BACKLOG 10
int main()
{ int sockfd,client_fd; int sin_size;
struct sockaddr_in my_addr; struct sockaddr_in remote_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0) ;
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(SERVPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) == -1)
{ perror("bind 出错!"); exit(1); }
if (listen(sockfd, BACKLOG) == -1)
{ perror("listen 出错!"); exit(1); }
while(1)
{
sin_size = sizeof(struct sockaddr_in);
if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1)
{ perror("accept error"); continue; }
printf("收到一个连接来自: %s\n", inet_ntoa(remote_addr.sin_addr));
if (!fork())
{
if (send(client_fd, "连接上了 \n", 26, 0) == -1) error("send 出错!"); close(client_fd); exit(0);
}
close(client_fd);
}
}
//客户端源程序代码 :
#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h> #define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc, char *argv[]){
int sockfd, recvbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if (argc < 2) { fprintf(stderr,"Please enter the server's hostname!\n");
exit(1); }
if((host=gethostbyname(argv[1]))==NULL)
{ herror("gethostbyname error!"); exit(1); }
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{ perror("socket create error!"); exit(1); }
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1) {
perror("connect error!");
exit(1);
}
if ((recvbytes=recv(sockfd, buf, MAXDATASIZE, 0)) ==-1) {
perror("connect 出错!");
exit(1);
}
buf[recvbytes] = '\0';
printf("收到: %s",buf);
close(sockfd);
}

3 UDP编程

基于UDP协议的编程,其最主要的特点是不需要用函数bind把本地IP地址与端口号进行绑定,也能进行通信。

常用的基UDP网络编程函数及功能:

image-20210619171733752

例4.2:服务器端接受客户端发送的字符串。客户端将打开liu文件,读取文件中的3个字符串,传送给服务器端,当传送给服务端的字符串为”stop”时,终止数据传送并断开连接。

image-20210619171749586

主要语句说明:
服务端:
建立socket:socket(AF_INET,SOCK_DGRAM,0)
绑定bind:bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet));
接收数据recvfrom:recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_clnt,&len);
关闭close:close(sockfd);

客户端:
建立socket:socket(AF_INET, SOCK_STREAM, 0);
读取liu文件:fopen(“liu”,”r”);
发送数据sendto:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr,sizeof(adr_srvr));
关闭close:close(sockfd);

1
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//服务端源程序代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/types.h>

int port=8888;
int main()
{ int sockfd; int len; int z;
struct sockaddr_in adr_inet;
struct sockaddr_in adr_clnt; char buf[256];
printf("等待客户端....\n");
adr_inet.sin_family=AF_INET;
adr_inet.sin_port=htons(port);
adr_inet.sin_addr.s_addr =htonl(INADDR_ANY);
bzero(&(adr_inet.sin_zero),8);
len=sizeof(adr_clnt);
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd==-1)
{ perror("socket 出错"); exit(1); }
z=bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet));
if(z==-1)
{ perror("bind 出错"); exit(1); }
while(1)
{
z=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_clnt,&len);
if(z<0)
{ perror("recvfrom 出错"); exit(1); }
buf[z]=0;
printf("接收:%s",buf);
if(strncmp(buf,"stop",4)==0)
{ printf("结束....\n"); break; }
}
close(sockfd);
exit(0);
}
//客户端源程序代码 :
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/types.h>
int port=8888;

int main()
{
int sockfd; int i=0; int z;
char buf[80],str1[80]; struct sockaddr_in adr_srvr;
FILE *fp;
printf("打开文件......\n");
fp=fopen("liu","r");
if(fp==NULL)
{ perror("打开文件失败"); exit(1); }
printf("连接服务端...\n");
adr_srvr.sin_family=AF_INET;
adr_srvr.sin_port=htons(port);
adr_srvr.sin_addr.s_addr = htonl(INADDR_ANY);
bzero(&(adr_srvr.sin_zero),8);
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd==-1)
{ perror("socket 出错"); exit(1); }
printf("发送文件 ....\n");
for(i=0;i<3;i++)
{ fgets(str1,80,fp); printf("%d:%s",i,str1);
sprintf(buf,"%d:%s",i,str1);
z=sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr, sizeof(adr_srvr));
}
printf("发送.....\n"); sprintf(buf,"stop\n");
z=sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr,
sizeof(adr_srvr));
if(z<0)
{ perror("sendto 出错"); exit(1); }
fclose(fp); close(sockfd); exit(0);
}

4 网络高级编程

I/O Models
Block mode 阻塞模式
Non-block mode 非阻塞模式
I/O multiplexing I/O多路复用
多进程并发
多线程并发

阻塞方式

阻塞方式:在数据通信中,当服务器运行函数accept() 时,假设没有客户机连接请求到来,那么服务器就一直会停止在accept()语句上,等待客户机连接请求到来,出现这样的情况就称为阻塞。

非阻塞方式

阻塞与非阻塞方式的比较
errno - EWOULDBLOCK
非阻塞的实现
int flags;
if ( (flags=fcntl(sock_fd, F_GETFL, 0)) < 0)
err_sys();
flags |= O_NONBLOCK;
if ( fcntl(sock_fd, F_SETFL, flags) < 0)
err_sys();

I/O 多路复用

基本思想:

先构造一张有关描述符的表,然后调用一个函数(如select),该函数到这些描述符中的一个已准备好进行I/O时才返回,返回时告诉进程哪个描述符已准备好进行I/O.

“select”

select: synchronous I/O multiplexing.

#include <sys/select.h>

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

FD_ZERO(fd_set *set);

FD_SET(int fd, fd_set *set);

FD_CLR(int fd, fd_set *set);

FD_ISSET(int fd, fd_set *set);

image-20210619172130327

多进程并发

1
2
3
4
5
6
7
8
9
10
listenfd = socket(...);
bind(...);
listen(...);
while(1) {
    connectfd = accpet(...);
     if(fork() == 0)    { close(listenfd);
     process(...);
     close(...);
     exit();    }
else { close(connectfd); ... continue;}    close(...); }

雨课堂题目整理

image-20210611101124982

image-20210611101135283

image-20210611101144547

image-20210611101151866

image-20210611101159284

image-20210611101205637

image-20210611101212054

image-20210611101220777

image-20210611101228856

Unix程序设计头歌知识点整理

Linux编程基础

vi/vim工作模式切换

vi/vim编辑器有三种工作模式,每种工作模式都有不同的作用,以下是这三种工作模式的详细介绍:

  1. **命令模式: **查看当前文件内容,此时不能对文件内容进行写入操作,从该模式可以切换为插入模式和底线命令模式。
  2. **插入模式: **可以对文件内容进行编辑操作,从该模式可以切换为命令模式。
  3. **底线命令模式: **不可以对文件内容进行编辑,在此模式下可以执行一些vi/vim的命令,例如: 退出命令、保存内容命令等等。从该模式可以切换为命令模式。

img

注意: 启动vi/vim后,首先进入的是命令模式。

命令模式与插入模式相互切换

首先启动vi/vim编辑器后,首先进入的工作模式是命令模式,在当前模式下,我们只能查看文件内容,不能对文件内容进行写入操作。如果想对文件进行写入操作,那么我们只有进入插入模式下。

  1. 命令模式->插入模式方法 从命令模式到插入模式的切换方法有多种,我们介绍如下3中常用方法:
输入命令 说明
i, I i 为『从目前光标所在处输入』,I 为『在目前所在行的第一个非空格符处开始输入』。
a, A a 为『从目前光标所在的下一个字符处开始输入』, A 为『从光标所在行的最后一个字符处开始输入』。
o, O 这是英文字母 o 的大小写。o 为『在目前光标所在的下一行处输入新的一行』; O 为在目前光标所在处的上一行输入新的一行。
  1. 插入模式->命令模式方法 由插入模式切换到命令模式比较简单,我们只需要点击ESC键即可返回到命令模式。

案例演示1:

使用vi/vim编辑器打开文件testFile,并且将工作模式切换到插入模式,输入Hello vi/vim字符串,最后保存文件并退出,可以使用如下命令:

1
vi testFile` 或 `vim testFile

打卡testFile文件命令; img

首先进入的是命令模式; img

按下字母i后进入插入模式; img

输入Hello vi/vim字符后,按下ESC键后返回命令模式,最后输入:wq保存退出文件; img [请在右侧“命令行”里直接体验]

命令模式与底线命令模式相互切换

vi/vim底线命令模式下如何执行写复杂的命令,例如我们常用的保存退出命令(wq)等。

  1. 命令模式->底线命令模式方法 从命令模式到底线命令模式的切换比较简单,我们只需要输入:字符即可,注意:是英文输入法下的冒号。
  2. 底线命令模式->命令模式方法 由底线命令模式切换到命令模式比较简单,我们只需要点击ESC键即可返回到命令模式。

vi/vim命令模式

vi/vim命令模式下,我们可以对文件进行删除、复制和粘贴操作。

命令模式移动光标操作

vi/vim编辑器与其它编辑器最大的不同之处是不能使用鼠标进行操作(可以在配置文件中设置鼠标属性,默认是禁止使用鼠标),我们可以在命令模式下移动光标位置,常见移动命令如下所示:

命令 说明
h 或 向左箭头键(←) 光标向左移动一个字符
j 或 向下箭头键(↓) 光标向下移动一个字符
k 或 向上箭头键(↑) 光标向上移动一个字符
l 或 向右箭头键(→) 光标向右移动一个字符
[Ctrl] + [f] 屏幕『向下』移动一页,相当于 [Page Down]按键
[Ctrl] + [b] 屏幕『向上』移动一页,相当于 [Page Up] 按键
[Ctrl] + [d] 屏幕『向下』移动半页
[Ctrl] + [u] 屏幕『向上』移动半页

案例演示1:

使用vi/vim编辑器打开文件oldFile,移动当前光标到第一行的第二字符处,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

移动光标到第一行的第5个字符处(按5次→); img

最后输入:q退出文件; img [请在右侧“命令行”里直接体验]

命令模式删除操作

我们不光可以在插入模式下可以对文件内容进行删除操作,我们可以直接在命令模式下对文件进行删除操作,常见删除命令如下所示:

命令 说明
x, X 在一行字当中,x 为向后删除一个字符 (相当于 [del] 按键), X 为向前删除一个字符(相当于 [backspace] 亦即是退格键)
nx n 为数字,连续向后删除 n 个字符。例如,我要连续删除 5 个字符 ,则可以使用5x
dd 删除光标所在的那一整行
ndd n 为数字。删除光标所在的向下 n 行,例如10dd则是删除 10 行
d1G 删除光标所在到第一行的所有数据
dG 删除光标所在到最后一行的所有数据

案例演示1:

使用vi/vim编辑器打开文件oldFile,删除当前文件的第二行所有内容,最后保存文件并退出,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

移动光标到文件第二行; img

输入dd字符后删除当前行内容,最后输入:wq保存退出文件; img [请在右侧“命令行”里直接体验]

命令模式复制粘贴操作

常见复制命令如下所示:

命令 说明
yy 复制光标所在的那一行
nyy n 为数字。复制光标所在的向下 n 行,例如 10yy 则是复制 10 行
y1G 复制光标所在行到第一行的所有数据
yG 复制光标所在行到最后一行的所有数据
y0 复制光标所在的那个字符到该行行首的所有数据
y$ 复制光标所在的那个字符到该行行尾的所有数据

常见粘贴命令为p, Pp 为将已复制的数据在光标下一行贴上,P 则为贴在游标上一行!

案例演示1:

使用vi/vim编辑器打开文件oldFile,将第一行内容复制,然后粘贴到文件的末尾,最后保存文件并退出,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

复制第一行内容(yy),移动光标到最后一行,粘贴(p)内容到当前行的下一行,最后输入:wq保存退出文件; img [请在右侧“命令行”里直接体验]

vi/vim底线命令模式

搜索替换

vi/vim编辑器在底线命令模式下可以对文件内容进行查找和替换操作,常见查找和替换命令如下所示:

命令 说明
/word 向光标之下寻找一个名称为 word 的字符串。例如要在档案内搜寻 vbird 这个字符串,就输入 /vbird 即可。
?word 向光标之上寻找一个字符串名称为 word 的字符串。
n 这个 n 是英文字母。代表重复前一个搜寻的动作。举例来说, 如果刚刚我们执行 /vbird 去向下搜寻 vbird 这个字符串,则按下 n 后,会向下继续搜寻下一个名称为 vbird 的字符串。
N 这个 N 是英文按键。与 n 刚好相反,为『反向』进行前一个搜寻动作。 例如 /vbird 后,按下 N 则表示『向上』搜寻 vbird 。
[:n1,n2s/word1/word2/g n1 与 n2 为数字。在第 n1 与 n2 行之间寻找 word1 这个字符串,并将该字符串取代为 word2 。
:1,$s/word1/word2/g 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2。
:1,$s/word1/word2/gc 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 !且在取代前显示提示字符给用户确认 (confirm) 是否需要取代。

案例演示1:

使用vi/vim编辑器打开文件oldFile,将所有line单词替换为words单词,并保存退出,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

首先输入:切换当前模式为底线命令模式,然后输入1,$s/line/words/g后回车; img

img [请在右侧“命令行”里直接体验]

底线命令模式下执行特殊命令

常见在底线命令模式执行的命令如下所示:

命令 说明
:w 将编辑的数据写入硬盘档案中
:w! 若文件属性为『只读』时,强制写入该档案。不过,到底能不能写入, 还是跟你对该档案的档案权限有关啊!
:q! 若曾修改过档案,又不想储存,使用 ! 为强制离开不储存档案。
:w [filename] 将编辑的数据储存成另一个档案(类似另存新档)
:n1,n2 w [filename] 将 n1 到 n2 的内容储存成 filename 这个档案。
:! command 暂时离开 vi 到指令行模式下执行 command 的显示结果!
:set nu 显示行号,设定之后,会在每一行的前缀显示该行的行号
:set nonu 与 set nu 相反,为取消行号!

案例演示1:

使用vi/vim编辑器打开文件oldFile,显示当前文件行号,将当前文件的第1-3行内容另存为oldFileCpy文件,使用cat命令查看新生成文件内容,可以使用如下步骤:

打卡oldFile文件命令; img

输入:set nu后回车,显示行号; img

img

输入:1,3 w oldFileCpy后回车 img

最后在vi中使用cat命令查看新生成的文件oldFileCpy内容; img

img

按下回车键后返回当前vi编辑器,最后输入q退出文件; img [请在右侧“命令行”里直接体验]

Linux之静态库编写

在实际的软件开发时, 应该遵守一个基本原则:不要重复造轮子。如果某些代码经常要用到,不仅这个项目能使用,而且也能复用到其它项目上,那么就应该把这部分代码从项目中分离出来,将其编译为库文件,供需要的程序调用。

程序库分为两类,分别是静态库动态库。本关将主要讲解如何生成静态库

Windows系统上的静态库是以.lib为后缀,而Linux系统上的静态库是以.a为后缀的特殊的存档。

Linux系统的标准系统库可在目录/usr/lib/lib中找到。比如,在类Unix系统中C语言的数序库一般存储为文件/usr/lib/libm.a。该库中函数的原型声明在头文件/usr/include/math.h中。

生成静态库

Linux下,我们可以使用gccar工具制作和使用自己的静态库,具体过程如下:

1
2
将源码文件编译成.o目标文件;
使用ar工具将多个目标文件打包成.a的静态库文件;

注意Linux系统上默认的静态库名格式为:libxxx.a,其中xxx为生成库的名称。

案例演示1:

编写一个函数printHello,其功能为打印“Hello world”字符串,将其编译生成名为Hello的静态库,可以使用如下命令:

1
2
3
4
vim printHello.h
vim printHello.c
gcc -c printHello.c -o printHello.o
ar rcs libHello.a printHello.o
  • 使用vim编写printHello.h(声明printHello函数,方便以后被其它程序调用)
1
2
3
4
5
#ifndef __PRINTHELLO_H__
#define __PRINTHELLO_H__
#include <stdio.h>
void printHello();
#endif
  • 使用vim编写printHello.c
1
2
3
4
#include <stdio.h>
void printHello(){
printf("Hello world\n");
}

img [请在右侧“命令行”里直接体验]

使用静态库

静态库的使用方法只需要在编译程序的时候指明要链接的库名称即可,gcc中有两个参数是关于链接库时所使用的,**分别是:-L-l**。

1
2
-L:用于告诉gcc要链接的库所在目录;
-l:用于指明链接的库名称(小写l);

案例演示1:

调用以上案例生成的printHello函数,可以使用如下命令:

1
2
3
vim main.c
gcc main.c -o exe -L ./ -lHello(可以换成Hello.o)
./exe
  • 使用vim编写main.c
1
2
3
4
#include "printHello.h"
int main(){
printHello();
return 0;}

img

[请在右侧“命令行”里直接体验]

Linux之动态库编写

  • 静态库动态库的区别:
静态库 动态库
名称 命名方式是”libxxx.a”,库名前加”lib”,后缀用”.a”,”xxx”为静态库名 命名方式是”libxxx.so”, 库名前加”lib”,后缀用”.so”, “xxx”为动态库名
链接时间 静态库的代码是在编译过程中被载入程序中 动态库在编译的时候并没有被编译进目标代码,而是当你的程序执行到相关函数时才调用该函数库里的相应函数
优点 在编译后的执行程序不在需要外部的函数库支持,因为所使用的函数都已经被编进去了。 动态库的改变不影响你的程序,所以动态函数库升级比较方便
缺点 如果所使用的静态库发生更新改变,你的程序必须重新编译 因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库

Windows系统上的动态库是以.dll为后缀,而Linux系统上的动态库是以.so为后缀的特殊的存档。

生成动态库

Linux下,我们可以使用gcc制作和使用动态库,具体制作过程如下:

1
使用gcc命令加-fPIC参数将源码文件编译成.o目标文件;使用gcc命令加-shared参数将多个目录文件生成一个动态库文件;

注意Linux系统上默认的动态库名格式为:libxxx.so,其中xxx为生成库的名称。

案例演示1:

编写一个函数printHello,其功能为打印”Hello world”字符串,将其编译生成名为Hello的动态库,可以使用如下命令:

1
2
3
4
vim printHello.h
vim printHello.c
gcc -fPIC -c printHello.c -o printHello.o
gcc -shared printHello.o -o libHello.so
  • 使用vim编写printHello.h(申明printHello函数,方便以后被其它程序调用)
1
2
3
4
5
#ifndef __PRINTHELLO_H__
#define __PRINTHELLO_H__
#include <stdio.h>
void printHello();
#endif
  • 使用vim编写printHello.c
1
2
3
4
#include <stdio.h>
void printHello(){
printf("Hello world\n");
}

img [请在右侧“命令行”里直接体验]

使用动态库

动态库的使用方法与静态库使用方式略有不同,除了使用gcc中的-L-l参数外,想要调用动态库还需要更新Linux系统中/etc/ld.so.cache或者修改LD_LIBRARY_PATH环境变量,否则在运行程序的时候会报”No such file or directory”错误。

案例演示1:

调用以上案例生成的printHello函数,可以使用如下命令:

1
2
3
vim main.c
gcc main.c -o exe -L ./ -lHello
./exe

img [使用vim编写程序]

img [请在右侧“命令行”里直接体验]

此时编译正常,当运行的时候会报”No such file or directory”错误。

更新/etc/ld.so.cache来运行动态库
  • 编辑/etc/ld.so.conf配置文件,然后加入需要加载的动态库目录。
  • 运行ldconfig更新/etc/ld.so.cache

案例演示1:

更新/etc/ld.so.cache,然后运行上一个案例生成的exe,可以使用如下命令:

1
2
3
sudo vim /etc/ld.so.conf
sudo ldconfig
./exe

img [使用vim/etc/ld.so.conf文件添加/home/fzm路径]

img [请在右侧“命令行”里直接体验]

修改LD_LIBRARY_PATH环境变量

在运行可执行文件前修改LD_LIBRARY_PATH变量为可执行程序指定需要加载的动态库路径。

案例演示1:

修改LD_LIBRARY_PATH,然后运行上一个案例生成的exe,可以使用如下命令:

1
LD_LIBRARY_PATH=.  ./exe

img [请在右侧“命令行”里直接体验]

注意

1
2
LD_LIBRARY_PATH告诉了exe程序现在当前目录下寻找链接的动态库;
当运行环境中同时存在同名的静态库和动态库时,默认优先链接动态库;

Makefile初体验

什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些WindowsIDE都为你做了这个工作,但是要作一个专业的程序员,makefile还是要懂的。makefile其实就是描述了整个工程中所有文件的编译顺序,编译规则,并且由make命令来读取makefile文件,然后根据makefile文件中定义的规则对其进行解析,完成对整个项目的编译操作。

makefilelinux操作系统中是比较常见的,例如,我们在使用源码安装一个软件的时候,通常只需执行make命令即可完成对软件的编译,正是因为软件开发者已经编写了makefile文件,所以只需执行make命令就会完成对整个工程的自动编译。

本关将介绍makefile的语法,使用makefile来完成对软件的编译。

Makefile规则

makefile文件中包含了一组用来编译应用程序的规则,一项规则可分成三个部分组成:

1
2
3
工作目标(target)
依赖条件(prerequisite)
所要执行的命令(command)

格式为:

1
2
target : prereq1 prereq2
commands

以上格式就是一个文件的依赖关系,也就是说,target这个目标文件依赖于多个prerequisites文件,其生成规则定义在commands中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,commands所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。

注意

1
2
3
commands前面使用的是TAB键,而不是空格,使用空格会出现错误;
commands可以是任意的shell命令;
在执行make命令时,make会解析第一项规则;

案例演示1:

存在一个源码文件main.c文件,编译一个makefile规则来编译该文件,并生成一个名为HelloWorld的可执行文件,具体操作如下:

1
2
vim makefile
make
  • 使用vim编写如下代码
1
2
3
4
5
#include <stdio.h>
int main(){
printf("Hello world\n");
return 0;
}
  • 使用vim编写makefile
1
2
HelloWorld : main.c    
gcc -o HelloWorld main.c

img [请在右侧“命令行”里直接体验]

通过以上案例可以看到,编写好makefile后,只需要输入make命令即自动只需定义好的规则。

注意:gcc -o HelloWorld main.c命令前是TAB键而不是空格。

案例演示2:

假设一个项目中包含5个源码文件,分别是Add.cSub.cMul.cDiv.cmain.c和一个头文件def.h,编译一个makefile规则来编译该项目,并生成一个名为exe的可执行文件,具体操作如下:

1
2
vim makefile
make
  • vim Add.c
1
2
3
4
#include <stdio.h>
int Add(int a, int b){
return a + b;
}
  • vim Sub.c
1
2
3
4
#include <stdio.h>
int Sub(int a, int b){
return a - b;
}
  • vim Mul.c
1
2
3
4
#include <stdio.h>
int Mul(int a, int b){
return a * b;
}
  • vim Div.c
1
2
3
4
#include <stdio.h>
int Div(int a, int b){
return a / b;
}
  • vim main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include "def.h"
int main(){
int add = Add(10, 5);
int sub = Sub(10, 5);
int mul = Mul(10, 5);
int div = Div(10, 5);
printf("10 + 5 = %d\n", add);
printf("10 - 5 = %d\n", sub);
printf("10 * 5 = %d\n", mul);
printf("10 / 5 = %d\n", div);
return 0;
}
  • vim def.h
1
2
3
4
5
6
7
8
#ifndef __DEF_H__
#define __DEF_H__
#include <stdio.h>
int Add(int a, int b);
int Sub(int a, int b);
int Mul(int a, int b);
int Div(int a, int b);
#endif
  • vim makefile
1
2
3
4
5
6
7
8
9
10
11
12
exe : main.o Add.o Sub.o Mul.o Div.o    
gcc -o exe main.o Add.o Sub.o Mul.o Div.o (若要重命名生成的文件,两个exe都要改名)
main.o : main.c def.h
gcc -c main.c -o main.o
Add.o : Add.c
gcc -c Add.c -o Add.o
Sub.o : Sub.c
gcc -c Sub.c -o Sub.o
Mul.o : Mul.c
gcc -c Mul.c -o Mul.o
Div.o : Div.c
gcc -c Div.c -o Div.o

img [请在右侧“命令行”里直接体验]

以上案例,当只需make命令时,首先解析目标为exe的规则,然后发现exe依赖于main.o、Add.o和Sub.o,然后分别对main.o、Add.o和Sub.o规则进行解析,即分别执行目标为main.o、Add.o和Sub.o的命令。当main.o、Add.o和Sub.o生成后,最后执行exe对应的命令。

Makefile之变量使用

makefile 变量的命令可以包含字符、数字、下划线(可以是数字开头),并且大小写敏感。

makefile变量在声明时需要对其进行赋值,而在使用该变量时需要在变量名前加上**$符号 例如$(VARNAME),如果用户需要在makefile文件中使用真实的$字符,则使用$$**表示。

makefile中对变量的赋值方式有三种,分别是:

1
递归赋值(=):递归赋值,即赋值后并不马上生效,等到使用时才真正的赋值,此时通递归找出当前的值;直接赋值(:=):是将":="右边中包含的变量直接展开给左边的变量赋值;条件赋值(?=):只有此变量在之前没有赋值的情况下才会对这个变量进行赋值,所有一般用在第一次赋值;

makefile除了可以自定义变量外,还存在一些系统默认的特殊变量,这些特殊变量可以方便帮助我们快速的编写makefile文件,例如:$@、$<和$^等等。

本关将介绍makefile的变量的定义和使用方法,以及使用特殊变量来编写makefile文件。

Makefile 自定义变量

自定义变量格式:

  • 递归赋值 变量名 = 变量内容
  • 直接赋值 变量名 := 变量内容
  • 条件赋值 变量名 ?= 变量内容

变量的使用格式为: $变量名或者${变量名}或者$(变量名)

案例演示1:

在上一关中案例2中的项包含了5个源码文件和一个头文件,如果使用变量来编写makefile则会显示出比较简洁的格式,具体操作如下:

1
vim makefilemake
  • vim makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o exe $(object)

main.o : main.c def.h
gcc -c main.c -o main.o

Add.o : Add.c
gcc -c Add.c -o Add.o

Sub.o : Sub.c
gcc -c Sub.c -o Sub.o

Mul.o : Mul.c
gcc -c Mul.c -o Mul.o

Div.o : Div.c
gcc -c Div.c -o Div.o

img [makefile内容]

img [请在右侧“命令行”里直接体验]

可以看到,我们使用object来表示main.o Add.o Sub.o Mul.o Div.o,这样我们就可以使用$(object)来表示以上目标文件,而不是每次输入这5个目标文件。

Makefile 特殊变量

makefile常用的特殊变量有:

1
2
3
$@:表示所有目标;
$^:表示所有依赖目标的集合,以空格分隔;
$<:表示依赖目标中第一个目标的名子;

案例演示1:

接着上一个案例中的项目,如果使用特殊变量来编写makefile则会显示出更加简洁的格式,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)main.o :

main.c def.h
gcc -c $< -o $@

Add.o : Add.c
gcc -c $< -o $@

Sub.o : Sub.c
gcc -c $< -o $@

Mul.o : Mul.c
gcc -c $< -o $@

Div.o : Div.c
gcc -c $< -o $@

img [请在右侧“命令行”里直接体验]

Makefile自动推导

make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个.o文件后都写上类似的命令。因为,我们的make会自动识别,并自己推导命令。

只要make看到一个.o文件,它就会自动的把.c文件加在依赖关系中,如果make找到一个main.o,那么main.c就会是main.o的依赖文件。并且 gcc -c main.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。

本关将介绍makefile的自动推导功能。

Makefile 自动推导

自动推导格式: 目标 : 其它依赖

案例演示1:

如果使用自动推导模式来编写上一关卡案例中的makefile,则会有更简洁的格式,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)

main.o : def.h

img

[请在右侧“命令行”里直接体验]

可以看到,我们只需要为main.o创建一个编译规则,其4个目标文件则不需要为其创建编译规则,因为make会自动的为其构造出编译规则。

Makefile伪目标

每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。

通常,我们在使用源码安装软件的时候,都会在编译完软件后,执行make install这个命令来安装软件,或者执行make clean这个命令清空临时生成的目标文件。以上操作就是利用了makefile的伪目标。

本关将介绍makefile的伪目标。

Makefile 伪目标

makefile使用.PHONY`关键字来定义一个伪目标,具体格式为:

1
.PHONY : 伪目标名称

案例演示1:

为上一关卡案例中的makefile添加清空临时目标文件标签clean,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
6
7
8
9
10
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)

main.o : def.h

.PHONY : clean

clean :
rm $(object)

img [请在右侧“命令行”里直接体验]

可以看到,当我们执行完make命令后会生成多个临时文件,然后我们执行make clean命令后,则会将生成的临时文件删除掉,其实执行make clean命令就是在执行rm main.o Add.o Sub.o Mul.o Div.o

案例演示2:

使用另一个格式来清除临时产生的目录文件和不显示删除命令,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
6
7
8
9
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)

main.o : def.h

clean :
@echo "clean object files"
@rm $(object)

img [请在右侧“命令行”里直接体验]

可以看到,当我们执行make clean命令后,将不会在终端中显示rm main.o Add.o Sub.o Mul.o Div.o命令。

注意:在命令前加了**@**符号,则不会把命令原样输出在终端。

文件编程

文件权限修改

在当前目录中新建文件test.txt

touch test.txt

增加拥有者(u)对该文件的执行权限。

chmod 777 test.txt

增加群组用户(g)对该文件的写权限。

chmod ug+w test.txt

取消其他用户(o)对该文件的读权限。

chmod o-r test.txt

文件I/O

文件的创建

相关知识

文件的创建操作是 I/O 操作的第一步。在Linux系统中creat系统调用可以实现对文件的创建。本关只介绍文件创建函数的使用方法。

在Linux系统中可以使用man命令来查询这些函数的使用方法。具体的查询命令为: man 2 函数名 其中,2表示查找系统调用函数,关于文件的创建、打开和关闭函数都是系统调用函数,因此使用2作为man命令的第一个参数。

案例演示1: 查询creat函数的使用方法可以使用以下命令: man 2 creat

img [查询结果]

通过man命令可以查询常用的系统调用函数的使用方法。

文件的创建

创建文件的系统调用函数是creat,具体的说明如下:

  • 需要的头文件如下:

    1
    2
    3
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
  • 函数格式如下:

    1
    int creat (const char *pathname,mode_t mode);

    参数说明:

    1
    pathname:需要创建文件的绝对路径名或相对路径名;mode:用于指定所创建文件的权限;

    常见的

    1
    mode

    取值及其含义见下表所示:

mode 含义
S_IRUSR 文件所有者的读权限位
S_IWUSR 文件所有者的写权限位
S_IXUSR 文件所有者的执行权限位
S_IRGRP 所有者同组用户的读权限位
S_IWGRP 所有者同组用户的写权限位
S_IXGRP 所有者同组用户的执行权限位
S_IROTH 其他用户的读权限位
S_IWOTH 其他用户的写权限位
S_IXOTH 其他用户的执行权限位
  • 函数返回值说明: 调用成功时,返回值为 文件的描述符(大于0的整数);调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下使用creat函数创建一个名为firstFile的文件,并设置文件的权限为644。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int ret = creat("firstFile", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (ret == -1)
{
printf("创建文件失败\n");
}
else
{
printf("创建文件成功\n");
}
return 0;
}

img 将以上代码保存为main.c文件中,编译执行。可以看到当前目录下存在firstFile文件,并且其权限为644

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 在当前目录下创建一个名为testFile的文件,并设置其权限为651
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
/********** BEGIN **********/
int ret = creat("testFile", S_IRUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IXOTH);

/********** END **********/

return 0;
}

文件打开与关闭

相关知识

文件的打开与关闭操作是 I/O 操作的第二步。在Linux系统中提供了以下两个系统调用函数用于打开和关闭文件操作,分别是openclose。本关将介绍文件的打开和关闭函数的使用方法。

使用man 2 函数名也可以查询这些函数的使用方法。

文件的打开

打开文件的系统调用函数是open,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>
  • 函数格式如下:

    1
    int open(coust char *pathname, int flags);int open(const char *pathname, int flags, made_t mode);

    参数说明:

    1
    pathname:需要被打开或创建的文件绝对路径名或相对路径名;flags:用于描述文件的打开方式;mode:用于指定所创建文件的权限(与上一关中creat函数中mode取值一致);

第一个open函数用于打开已经存在的文件。而第二个open函数可以创建一个不存在的文件且打开,该函数将flags参数设置为O_CREAT | O_WRONLY | O_TRUNC时等同于上一关中的creat函数。

常见的flags取值及其含义见下表所示:

flags 含义
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWY 以只读写方式打开文件
O_CREAT 若所打开文件不存在则创建此文件
O_TRUNC 若以只写或读写方式打开一个已存在文件时,将该文件截至 0
O_APPEND 向文件添加内容时将指针置于文件的末尾
O_SYNC 只在数据被写外存或其他设备之后操作才返回
  • 函数返回值说明: 调用成功时,返回值为 文件的描述符(大于0的整数);调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下使用open函数以只读方式打开一个已存在且名为firstFile的文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int ret = open("firstFile", O_RDONLY);
if (ret == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
return 0;
}

img 将以上代码保存为openFile.c文件中,编译执行。

案例演示2: 在当前目录下使用open函数创建一个名为secondFile的文件,并设置文件的权限为644。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int ret = open("secondFile", O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (ret == -1)
{
printf("创建文件失败\n");
}
else
{
printf("创建文件成功\n");
}
return 0;
}

img 将以上代码保存为secondFile.c文件中,编译执行。可以看到当前目录下存在secondFile文件,并且其权限为644

文件的关闭

关闭文件的系统调用函数是close,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: int close(int fd);

参数说明:

1
fd:需关闭文件的描述符;
  • 函数返回值说明: 调用成功时,返回值为 0;调用失败时,返回值为-1,并设置错误编号errno

案例演示1: 在当前目录下使用close函数关闭一个已经被打开的文件。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("firstFile", O_RDONLY);
if (fd == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
int ret = close(fd);
if(ret == -1)
{
printf("关闭文件失败\n");
}
else
{
printf("关闭文件成功\n");
}
return 0;
}

img 将以上代码保存为closeFile.c文件中,编译执行。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全open_File函数,使其以方式打开一个文件,并返回文件描述符fd
  • 补全close_File函数,使其关闭一个已经被打开的文件。
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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/************************
* fileName: 需要被打开的文件路径
*************************/
int open_File(char *fileName)
{
int fd = 0; //存放文件描述符
/********** BEGIN **********/
fd = open(fileName, O_RDONLY);
/********** END **********/

return fd;
}

/************************
* fd: 需要被关闭的文件描述符
*************************/
void close_File(int fd)
{
/********** BEGIN **********/
fd = close(fd);
/********** END **********/
}

文件读写操作

相关知识

文件的读写是 I/O 操作的核心内容。上一关中已经介绍了如何打开和关闭一个文件,但是要实现文件的 I/O 操作就必须对其进行读写,文件的读写操作所用的系统调用分别是readwrite。本关将介绍文件的读写函数的使用方法。

使用man 2 函数名也可以查询这些函数的使用方法。

文件的写操作

写入文件的系统调用函数是write,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: ssize_t write(int fd, void *buf, size_t count);

参数说明:

1
fd:表示将对之进行写操作的文件打开时返回的文件描述符;buf:指向存放将写入文件的数据的缓冲区的指针;count:表示本次操作所要写入文件的数据的字节数;
  • 函数返回值说明: 调用成功时,返回值为所写入的字节数;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下往firstFile文件中写入一个字符串。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("firstFile", O_WRONLY);
if (fd == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
char *data = "this is firstFile\n";
ssize_t size = write(fd, data, strlen(data));
if(size == -1)
{
printf("写入文件失败\n");
}
else
{
printf("写入文件成功:写入%ld个字符\n", size);
}
if(close(fd) == -1)
{
printf("关闭文件失败\n");
}
else
{
printf("关闭文件成功\n");
}
return 0;
}

img 将以上代码保存为writeFile.c文件中,编译执行。可以看到字符串被写入到firstFile文件中。

文件的读操作

读取文件的系统调用函数是read,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: ssize_t read(int fd, void *buf, size_t count);

参数说明:

1
fd:表示将对之进行写操作的文件打开时返回的文件描述符;buf:指向存放所读数据的缓冲区的指针;count:读操作希望读取的字节数;
  • 函数返回值说明: 调用成功时,返回值为本次读操作实际读取的字节数;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 读取当前目录下firstFile文件中的前4个字符。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("firstFile", O_RDONLY);
if (fd == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
char data[5] = "";
ssize_t size = read(fd, data, sizeof(char)*4);
if(size == -1)
{
printf("读取文件失败\n");
}
else
{
printf("读取文件成功:数据:%s\n", data);
}
if(close(fd) == -1)
{
printf("关闭文件失败\n");
}
else
{
printf("关闭文件成功\n");
}
return 0;
}

img 将以上代码保存为readFile.c文件中,编译执行。可以看到从firstFile文件中读取出了前4个字符。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全write_File函数,完成向文件写入字符串功能。并返回实际写入字符个数。
  • 补全readLine函数,完成从文件中读取一行的功能(不包括换行符),并返回实际读取的字符个数(文件的换行符号为\n)。
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
42
43
44
45
46
47
48
49
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include<string.h>
#include<stdlib.h>

/************************
* fd: 被打开文件的描述符
* buf: 被写入字符串指针
*************************/
int write_File(int fd, char *buf)
{
int writeSize = 0; //返回实际写入的字符个数

/********** BEGIN **********/
writeSize = write(fd, buf, strlen(buf));
/********** END **********/

return writeSize;
}

/************************
* fd: 被打开文件的描述符
* buf: 存放读取的字符串指针(假设buf足够大)
*************************/
int readLine(int fd, char *buf)
{
int readSize = 0; //返回实际读取的字符个数
char tempC;

//提示:使用while循环每次只读取一个字符,判断该字符是否为换行符或者是否已经读取到文件末尾(读取到文件末尾返回值为0)
/********** BEGIN **********/
int temp=1,length=0;
while(1){
temp = read(fd, &tempC, sizeof(char));
if(temp==0|tempC=='\n'){
break;
}else{
readSize = readSize+1;
buf[length++]=tempC;
}

}

/********** END **********/

return readSize;
}

文件的删除

相关知识

当不需要一个文件时,我们通常直接选中文件按下delete键对其删除,本关将介绍如何在Linux系统中使用C语言删除一个已经存在的文件。

在Linux系统中使用unlinkremove系统调用可以实现对文件的删除操作。

使用man 2 函数名或者man 3 函数名也可以查询这些函数的使用方法。

使用unlink函数删除文件

删除文件的系统调用函数是unlink,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: int unlink(const char *pathname); 参数说明:

    1
    pathname:需要删除的文件绝对路径名或相对路径名;
  • 函数返回值说明: 调用成功时,返回值为0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 删除当前目录下的firstFile文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>
#include <stdio.h>
int main()
{
int ret = unlink("firstFile");
if (ret == -1)
{
printf("删除文件失败\n");
}
else
{
printf("删除文件成功\n");
}
return 0;
}

img 将以上代码保存为deleteFile1.c文件中,编译执行。可以看到当前目录下存在firstFile文件被删除了。

使用unlink函数删除文件

remove是删除文件的另一个函数,该函数是C语言的库函数,其本质是通过调用系统调用unlink来完成文件的删除操作,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdio.h>
  • 函数格式如下: int remove(const char *pathname); 参数说明:

    1
    pathname:需要删除的文件绝对路径名或相对路径名;
  • 函数返回值说明: 调用成功时,返回值为0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 删除当前目录下的secondFile文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
int ret = remove("secondFile");
if (ret == -1)
{
printf("删除文件失败\n");
}
else
{
printf("删除文件成功\n");
}
return 0;
}

img 将以上代码保存为deleteFile2.c文件中,编译执行。可以看到当前目录下存在secondFile文件被删除了。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 删除当前目录下的testFile文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>

int main()
{
/********** BEGIN **********/

int ret = unlink("testFile");

/********** END **********/

return 0;
}

目录文件I/O

目录文件的创建与删除

相关知识

目录文件是Linux系统中一类比较特殊的文件。它对构成 Linux 系统的整个文件系统结构非常重要。Linux系统提供了两个系统调用函数来实现目录的创建和删除功能,分别是mkdirrmdir函数,这两个函数的名称和创建/删除目录命令的名称一样。其实创建/删除目录命令的背后实现方法就是调用这两个系统函数来实现对目录的创建和删除功能。

在Linux系统中可以使用man命令来查询这些函数的使用方法。具体的查询命令为: man 2 函数名 其中,2表示查找系统调用函数,关于目录的创建、打开、关闭和删除函数都是系统调用函数,因此使用2作为man命令的第一个参数。

案例演示1: 查询mkdir函数的使用方法可以使用以下命令: man 2 mkdir

img [查询结果]

通过man命令可以查询rmdir函数的使用方法。

目录文件的创建

创建目录文件的系统调用函数是mkdir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/type.h>#include <sys/stat.h>
  • 函数格式如下:

    1
    int mkdir(const char *pathname, mode_t mode);

    参数说明:

    1
    pathname:新创建的目录文件名;mode:用于指定所创建目录文件的权限;

    常见的

    1
    mode

    取值及其含义见下表所示:

mode 含义
S_IRUSR 目录所有者的读权限位
S_IWUSR 目录所有者的写权限位
S_IXUSR 目录所有者的执行权限位
S_IRGRP 所有者同组用户的读权限位
S_IWGRP 所有者同组用户的写权限位
S_IXGRP 所有者同组用户的执行权限位
S_IROTH 其他用户的读权限位
S_IWOTH 其他用户的写权限位
S_IXOTH 其他用户的执行权限位

注意:在Linux系统中,新创建目录的权限位是(mode & ~ umask & 01777),也就是umask为进程创建目录的权限位限制。因此会出现用户在代码中设定的权限与实际创建出来的权限不一致情况。同理,对于文件权限的处理也一样。

  • 函数返回值说明: 调用成功时,返回值0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下使用mkdir函数创建一个名为firstDir的目录文,并设置目录的权限为644。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <sys/type.h>
#include <sys/stat.h>
int main()
{
int ret = mkdir("firstDir", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (ret == -1)
{
printf("创建目录失败\n");
}
else
{
printf("创建目录成功\n");
}
return 0;
}

img 将以上代码保存为createDir.c文件,编译执行。可以看到当前目录下存在firstDir目录文件,并且其权限为644

目录文件的删除

删除目录文件的系统调用函数是rmdir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下:

    1
    int rmdir(const char *pathname);

    参数说明:

    1
    pathname:要被删除的目录文件名称;

注意:使用rmdir库函数删除的目录必须为空,如果该目录不为空,则必须删除该目录的所有文件(...文件除外)。

  • 函数返回值说明: 调用成功时,返回值0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 使用mkdir函数删除当前目录下名为firstDir的目录文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>
int main()
{
int ret = rmdir("firstDir");
if (ret == -1)
{
printf("删除目录失败\n");
}
else
{
printf("删除目录成功\n");
}
return 0;
}

img 将以上代码保存为deleteDir.c文件中,编译执行。可以看到当前目录下的firstDir目录文件被删除。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 在当前目录下创建一个名为testDir的目录,并设置其权限为651
  • 删除当前目录下名为Dir的空目录文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/stat.h>
#include <stdio.h>

int main()
{
/********** BEGIN **********/
int ret = mkdir("testDir", S_IRUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IXOTH);
int ret1 = rmdir("Dir");

/********** END **********/

return 0;
}

目录文件的打开与关闭

相关知识

在Linux系统中提供了以下两个系统调用函数用于打开和关闭目录操作,分别是opendirclosedir,这些库函数不属于系统调用,它们是C语言提供的库函数。本关将介绍目录的打开和关闭函数的使用方法。

因为这两个函数是C语言提供的库函数,因此可以使用man 3 函数名也可以查询这些函数的使用方法。

目录文件的打开

打开目录文件的库函数是opendir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <dirent.h>
  • 函数格式如下: DIR *opendir(const char *name);

参数说明:

1
name:需要打开的目录绝对路径名或相对路径名;

注意:打开一个目录后返回一个DIR对象,该对象指向被打开目录的目录流。

  • 函数返回值说明: 调用成功时,返回值为一个不为空的目录流指针;调用失败时,返回值为NULL的空指针,并设置错误编号errno

案例演示1: 使用opendir函数打开当前用户的家目录(本实验环境的用户家目录为/home/fzm)。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int main()
{
DIR* dirp = opendir("/home/fzm");
if (dirp == NULL)
{
printf("打开目录失败\n");
}
else
{
printf("打开目录成功\n");
}
return 0;
}

img 将以上代码保存为openDir.c文件中,编译执行。

目录文件的关闭

关闭目录文件的库函数是closedir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <dirent.h>
  • 函数格式如下: int closedir(DIR *dirp);

参数说明:

1
dirp:需要被关闭的目录流指针;
  • 函数返回值说明: 调用成功时,返回值为 0;调用失败时,返回值为-1,并设置错误编号errno

案例演示1: 使用closedir函数关闭一个已经被打开的目录。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int main()
{
DIR* dirp = opendir("/home/fzm/Downloads");
if (dirp == NULL)
{
printf("打开目录失败\n");
}
else
{
printf("打开目录成功\n");
}
int ret = closedir(dirp);
if(ret == -1)
{
printf("关闭目录失败\n");
}
else
{
printf("关闭目录成功\n");
}
return 0;
}

img 将以上代码保存为closeDir.c文件中,编译执行。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全open_Dir函数,使其打开一个目录并返回目录流指针dirp

  • 补全close_Dir函数,使其关闭一个被打开的目录。

    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
    #include <dirent.h>
    #include <sys/types.h>
    #include <stdio.h>

    /************************
    * pathName: 需要被打开的目录路径
    *************************/
    DIR* open_Dir(char *pathName)
    {
    DIR* dirp = NULL; //存放目录流指针
    /********** BEGIN **********/
    dirp = opendir(pathName);

    /********** END **********/

    return dirp;
    }

    /************************
    * dirp: 需要被关闭的目录流指针
    *************************/
    void close_Dir(DIR* dirp)
    {
    /********** BEGIN **********/
    int ret = closedir(dirp);

    /********** END **********/
    }

目录文件的读取操作

相关知识

ls命令的背后实现方法就是通过打开被浏览的目录,然后从目录中读取目录项。Linux系统中使用readdir函数可以读取目录内容。本关将介绍目录的读函数的使用方法。

因为readdir函数是C语言提供的库函数,因此可以使用man 3 函数名来查询该函数的使用方法。

目录文件的读操作

读取目录的库调用函数是readdir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <dirent.h>
  • 函数格式如下: struct dirent *readdir(DIR *dirp);

参数说明:

1
dirp:表示被打开目录的流指针;

结构dirent指向目录项,其定义在Linux系统中的<dirent.h>头文件中,详细定义如下所示:

1
2
3
4
5
6
7
struct dirent {
ino_t d_ino; /* 索引节点号 */
off_t d_off; /* 在目录文件中的偏移 */
unsigned short d_reclen; /* 文件名长 */
unsigned char d_type; /* 文件类型 */
char d_name[256]; /* 文件名,最长255字符 */
};

其中d_name字段存放着所读取到的目录项名。d_type字段为该目录项的类型,常见类型如下所示:

1
DT_DIR:目录文件;DT_LNK:符号链接文件;DT_REG:常规文件;DT_SOCK:sock文件;DT_UNKNOWN:未知的文件类型;

注意:d_type字段并不是支持所有的文件系统,并且只是由BSD衍生出来的Linux系统中可用。在Linux系统中还提供了另一个系统调用函数用来判断文件类型,其名称为stat,有兴趣的学生可以执行去学习其使用方法。

  • 函数返回值说明: 调用成功时,返回值为所写入的字节数;调用失败时,返回值为-1并设置错误编号errno

案例演示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
31
32
33
34
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
int main()
{
//.表示当前目录
DIR* dirp = opendir(".");
if (dirp == NULL)
{
printf("打开目录失败\n");
return -1;
}
else
{
printf("打开目录成功\n");
}
struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
printf("%s ", dir->d_name);
dir = readdir(dirp);
}
printf("\n");
if(closedir(dirp) == -1)
{
printf("关闭目录失败\n");
}
else
{
printf("关闭目录成功\n");
}
return 0;
}

img 将以上代码保存为readdir.c文件中,编译执行。可以看到执行该命令后会将当前目录下所有的内容都打印出来。

案例演示2: 读取当前目录下的所有普通文件,并打印出其名称。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
int main()
{
//.表示当前目录
DIR* dirp = opendir(".");
if (dirp == NULL)
{
printf("打开目录失败\n");
return -1;
}
else
{
printf("打开目录成功\n");
}
struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
if(dir->d_type == DT_REG)
printf("%s ", dir->d_name);
dir = readdir(dirp);
}
printf("\n");
if(closedir(dirp) == -1)
{
printf("关闭目录失败\n");
}
else
{
printf("关闭目录成功\n");
}
return 0;
}

img 将以上代码保存为readRegDir.c文件中,编译执行。可以看到执行该命令后只会将当前目录下常规文件打印出来了。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全scanAll函数,完成读取一个目录下所有的内容,并将每个内容按空格分割打印出来。
  • 补全scanDir函数,完成读取一个目录下直接包含的目录名称(只读取当前目录层的内容,不往下读取),并将每个目录按空格分割打印出来。
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
42
43
44
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>

/************************
* dirp: 被打开的目录流指针
*************************/
void scanAll(DIR *dirp)
{
//提示:不需要关闭dirp指针,输出的内容不能有换行,每个目录项中间用空格(英文空格)分割
/********** BEGIN **********/

struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
printf("%s ", dir->d_name);
dir = readdir(dirp);
}

/********** END **********/
}

/************************
* dirp: 被打开的目录流指针
*************************/
void scanDir(DIR *dirp)
{

//提示:不需要关闭dirp指针,输出的内容不能有换行,每个目录项中间用空格(英文空格)分割
/********** BEGIN **********/
struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
if(dir->d_type == DT_DIR)
printf("%s ", dir->d_name);
dir = readdir(dirp);
}


/********** END **********/

}

进程控制

获取进程常见属性

在 Linux 环境下,进程是一个十分重要的概念。每个进程都由一个唯一的标识符来表示,即进程 ID ,通常称为 pid 。

Linux 系统中存在一个特殊的进程,即空闲进程( idle process ),当没有其他进程在运行时,内核所运行的进程就是空闲进程,它的 pid 为 0 。在启动后,内核运行的第一个进程称为 init 进程,它的 pid 是 1 。通常, Linux 系统中 init 进程就是我们在资源管理器中看到的名为 init 的程序。系统中其它的进程都是由 init 来创建出来的。

创建新进程的那个进程被称为父进程,而新创建的进程被称为子进程。每个进程都是由其他进程创建的(除了 init 进程),因此每个子进程都有一个父进程。

Linux 系统提供了两个系统调用函数来获取一个进程的 pid 和其父进程的 pid ,分别是 getpid 和 getppid 函数。在 Linux 系统中可以使用 man 命令来查询这些函数的使用方法。具体的查询命令为:

1
man 2 函数名 
获取进程自身 pid

获取进程本身的进程 ID 的系统调用函数是 getpid ,具体的说明如下:

  • 需要的头文件如下:
1
2
#include <sys/types.h>
#include <unistd.h>
  • 函数格式如下:

    1
    pid_t getpid(void); 
  • 函数返回值说明: 返回当前进程的 pid 值。

获取父进程 pid

获取父进程的进程 ID 的系统调用函数是 getppid ,具体的说明如下:

  • 需要的头文件如下:
1
2
#include <sys/types.h>
#include <unistd.h>
  • 函数格式如下:

    1
    pid_t getppid(void); 
  • 函数返回值说明: 返回当前进程的父进程的 pid 值。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 getProcInfo 函数,用于获取当前进程 ID 和其父进程 ID (提示:将结果存放在procIDInfo结构体中)。
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
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

/**********************
* pid: 当前进程ID
* ppid: 父进程ID
***********************/
struct procIDInfo
{
pid_t pid;
pid_t ppid;
};

/************************
* 返回值: 需要被打开的目录路径
*************************/
struct procIDInfo getProcInfo()
{
struct procIDInfo ret; //存放进程ID信息,并返回
/********** BEGIN **********/
pid_t pid = getpid();
pid_t ppid = getppid();

ret.pid=pid;
ret.ppid=ppid;
/********** END **********/

return ret;
}

进程创建操作-fork

当用户调用 fork 函数时,系统将会创建一个与当前进程相同的新进程。通常将原始进程称为父进程,而把新生成的进程称为子进程。子进程是父进程的一个拷贝,子进程获得同父进程相同的数据,但是同父进程使用不同的数据段和堆栈段。

在早期的系统中,创建进程比较简单。当调用 fork 时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容也复制到子进程的地址空间中。但是从内核角度来说,这种复制方式是非常耗时的。

因此,在现代的系统中采取了更多的优化。现代的 Linux 系统采用了写时复制技术( Copy on Write ),而不是一创建子进程就将所有的数据都复制一份。

Copy on Write ( COW )的主要思路是:如果子进程/父进程只是读取数据,而不是对数据进行修改,那么复制所有的数据是不必要的。因此,子进程/父进程只要保存一个指向该数据的指针就可以了。当子进程/父进程要去修改数据时,那么再复制该部分数据即可。这样也不会影响到子父进程的执行。因此,在执行 fork 时,子进程首先只复制一个页表项,当子进程/父进程有写操作时,才会对所有的数据块进行复制操作。

使用fork函数创建进程

fork 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下:

    1
    pid_t fork(void); 
  • 函数返回值说明: 调用成功, fork 函数两个值,分别是 0 和子进程 ID 号。当调用失败时,返回 -1 ,并设置错误编号 errno 。fork 函数调用将执行两次返回,它将从父进程和子进程中分别返回。从父进程返回时的返回值为子进程的 PID ,,而从子进程返回时的返回值为 0 ,并且返回都将执行 fork 之后的语句

案例演示1: 编写一个程序,使用 fork 函数创建一个新进程,并在子进程中打印出其进程 ID 和父进程 ID ,在父进程中返回进程 ID 。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
printf("当前进程为子进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
else
{
//父进程
printf("当前进程为父进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
//子进程和父进程分别会执行的内容
return 0;
}

img

将以上代码保存为 forkProcess.c 文件,编译执行。可以看到每次执行 forkProcess 时,子进程和父进程都不是固定的执行顺序,因此由 fork 函数创建的子进程执行顺序是由操作系统调度器来选择执行的。因此,子进程和父进行在执行的时候顺序不固定。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 createProcess 函数,使用 fork 函数创建进程,并在子进程中输出 “Children” 字符串,在父进程中输出 “Parent” 字符串。(注意:不要在 createProcess 函数中使用 exit 函数或者 return 来退出程序)。
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
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

/************************
* 提示: 不要在子进程或父进程中使用exit函数或者return来退出程序
*************************/
void createProcess()
{
/********** BEGIN **********/
pid_t pid;
pid = fork();
if(pid == 0)
{
//子进程
printf("Children");
}
else
{
//父进程
printf("Parent");
}
//子进程和父进程分别会执行的内容
/********** END **********/
}

进程创建操作-vfork

vfork 函数是一个历史遗留产物。 vfork 创建进程与 fork 创建的进程主要有一下几点区别:

  1. vfork创建的子进程与父进程共享所有的地址空间,而fork创建的子进程是采用COW技术为子进程创建地址空间;
  2. vfork会使得父进程被挂起,直到子进程正确退出后父进程才会被继续执行,而fork创建的子进程与父进程的执行顺序是由操作系统调度来决定

vfork 性能要比 fork 高,主要原因是 vfork 没有进行所有数据的复制,尽管 fork 采用了 COW 技术优化性能,但是也会为子进程的页表项进行复制,因此 vfork 要比 fork 快。

使用 vfork 时要注意,在子进程中对共享变量的修改也会影响到父进程,因此 vfork 在带来高性能的同时,也使得整个程序容易出错,因此,开发人员在使用 vfork 创建进程时,一定要注意对共享数据的修改。

由于 vfork 创建的子进程和父进程共享所有的数据(栈、堆等等),因此,采用 vfork 创建的子进程必须使用 exit 或者 exec 函数族(下一关将介绍这些函数的功能)来正常退出,不能使用 return 来退出

exit 函数是用来结束正在运行的整个程序, exit 是系统调用级别,它表示一个进程的结束;而 return 是语言级别的,它表示调用堆栈的返回

使用 vfork 函数创建进程

vfork 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    2
    #include <sys/types.h>
    #include <unistd.h>
  • 函数格式如下:

    1
    pid_t vfork(void); 
  • 函数返回值说明: 调用成功, vfork 函数两个值,分别是 0 和子进程 ID 号。当调用失败时,返回 -1 ,并设置错误编号 errno 。

注意: vfork 函数调用将执行两次返回,它将从父进程和子进程中分别返回从父进程返回时的返回值为子进程的 PID ,而从子进程返回时的返回值为 0 ,并且返回都将执行 vfork 之后的语句。 vfork 创建的子进程必须调用 exit 函数来退出子进程

案例演示 1 : 编写一个程序,使用 vfork 函数创建一个新进程,并在子进程中打印出其进程 ID 和父进程 ID ,在父进程中返回进程 ID 。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = vfork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
sleep(2); //睡眠2秒
printf("当前进程为子进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
else
{
//父进程
printf("当前进程为父进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
//子进程和父进程分别会执行的内容
exit(0);
}

img

将以上代码保存为 vforkProcess.c 文件,编译执行。可以看到 vforkProcess 创建的子进程尽管使用 sleep 函数睡眠了 2 秒,但是函数父进程的执行顺序在子进程后,这就是 vfork 的特性。

当我们将以上代码中的 exit(0) 换成 return 0 时,则会出现如下错误。

img

出现以上错误的原因是当子进程使用 return 退出时,操作系统也会把栈清空,那么当父进程继续使用 return 退出时,则会发现栈已经被清空了,这就相当于 free 两次同一块内存,因此会出现错误。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 createProcess 函数,使用 vfork 函数创建进程,并在子进程中输出”Children”字符串(提示:需要换行),在父进程中输出”Parent”字符串(提示:需要换行)。
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
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

/************************
* 提示: 不要在子进程中使用return来退出程序
*************************/
void createProcess()
{
/********** BEGIN **********/
pid_t pid;
pid = vfork();
if(pid == 0)
{
//子进程
printf("Children\n");
}
else
{
//父进程
printf("Parent\n");
}
//子进程和父进程分别会执行的内容

/********** END **********/

exit(0);
}

进程终止

常见与退出进程相关的函数有: exit 、 _exit 、 atexit 、 on_exit 、 abort 和 assert 。

  1. exit 函数是标准 C 库中提供的函数,它用来终止正在运行的程序,并且关闭所有 I/O 标准流
  2. _exit 函数也可用于结束一个进程,与 exit 函数不同的是, _exit 不会关闭所有 I/O 标准流
  3. atexit 函数用于注册一个不带参数也没有返回值的函数以供程序正常退出时被调用
  4. on_exit 函数的作用与 atxeit 函数十分类似,不同的是它注册的函数具有参数,退出状态和参数 arg 都是传递给该程序使用的
  5. abort 函数其实是用来发送一个 SIGABRT 信号这个信号将使当前进程终止
  6. assert 是一个宏。调用 assert 时,它将先计算参数表达式 expression 的值,如果为 0 ,则调用 abort 函数结束进程

img

[ exit 和 _exit 区别]

exit 和 _exit 使用方法

exit 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数族格式如下:

    1
    void exit(int status);

    参数说明: status:设置程序退出码;

    _exit 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数族格式如下:

    1
    void _exit(int status);

    参数说明: status :设置程序退出码;

  • 函数返回值说明: exit 和 _exit 均无返回值。

atexit 和 on_exit 使用方法

atexit 和 on_exit 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数族格式如下:

    1
    2
    int atexit(void (*function)(void));
    int on_exit(void (*function)(int , void *), void *arg);

    参数说明: atexit 函数的 function 参数是一个函数指针,指向无返回值和无参数的函数; on_exit 函数的 function 参数是一个函数指针,指向无返回值和有两个参数的函数,其中第一个参数是调用 exit() 或从 main 中返回时的值,参数 arg 指针会传给参数 function 函数;

  • 函数返回值说明: atexit 和 on_exit 调用成功返回 0 ;调用失败返回一个非零值。

注意: atexit 和 on_exit 只有在程序使用 exit 或者 main 中正常退出时才会有效。如果程序使用 _exit 、 abort 或 assert 退出程序时,则不会执行被注册的函数

abort 和 assert 使用方法

abort 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数族格式如下:

    1
    void abort(void);

    assert 宏的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <assert.h>
  • 函数族格式如下:

    1
    void assert(scalar expression);

    参数说明: expression :需要被判断的表达式;

注意: assert 宏通常用于调试程序。

  • 函数返回值说明: abort 和 assert 无返回值。

案例演示 1 : 使用 atexit 注册一个退出函数,使其在调用退出函数前被执行,详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>
void out()
{
printf("程序正在被退出\n");
}
int main()
{
if(atexit(out) != 0)
{
printf("调用atexit函数错误\n");
}
return 0;```//或者exit(0)
}

img

将以上代码保存为 atexit.c 文件,编译执行。可以看到执行 atexit 程序后, out 函数被调用。

案例演示 2 : 使用 on_exit 注册一个退出函数,使其在调用退出函数前被执行,详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>
void out(int status, void *arg)
{
printf("%s(%d)\n", (char *s)arg, status);
}
int main()
{
if(on_exit(out, "程序正在被退出") != 0)
{
printf("调用on_exit函数错误\n");
}
exit(1);```//或者return 1
}

img

将以上代码保存为 on_exit.c 文件,编译执行。可以看到执行 on_exit 程序后, out 函数被调用,并且 status 变量的值就是 exit 函数退出的值。

案例演示1: 使用 abort 终止一个程序,详细代码如下所示:

1
2
3
4
5
6
7
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf("Hello world\n");
abort();
}

img

将以上代码保存为 abort.c 文件,编译执行。可以看到执行 abort 程序后,程序被强行终止。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 exitProcess 函数,使用 atexit 函数注册一个函数,在注册函数中打印出当前进程的 ID 号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>

/************************
* 提示: 用户需要在exitProcess函数中使用atexit函数注册一个自定义函数,并在自定义函数中打印出当前进程ID号
*************************/
void out()
{
printf("%d",getpid());
}

void exitProcess()
{
/********** BEGIN **********/
if(atexit(out) != 0)
{
printf("调用atexit函数错误\n");
}
/********** END **********/
}

进程等待

**如果,当子进程在父进程前结束,则内核会把子进程设置为一个特殊的状态。这种状态的进程叫做僵死进程(zombie)**。尽管子进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存。但是仍然保留了一些信息(如进程号pid退出状态 运行时间等)。只有父进程获取了子进程的这些信息后,子进程才会彻底的被销毁,否则一直保持僵死状态。如果系统中产生大量的僵尸进程,将导致系统没有可用的进程号,从而导致系统不能创建新的进程。

Linux处理僵死进程的方法之一是使用进程等待的系统调用waitwaitpid来使得父进程获取子进程的终止信息。

wait函数使用方法

wait函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/wait.h>
  • ```
    wait

    1
    2
    3

    函数格式如下:

    pid_t wait(int *status);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    参数说明: 参数`status`是一个整数指针,当子进程结束时,将子进程的结束状态字存放在该指针指向的缓存区。利用这个状态字,需要时可以使用一些由 Linux 系统定义的宏来了解子程序结束的原因。这些宏的定义与作用如下:

    | 宏定义 | 含义 |
    | ------------------- | ------------------------------------------------------------ |
    | WIFEXITED(status) | 子进程正常结束时,返回值为真(非零值) |
    | WEXITSTATUS(status) | 当WIFEXITED为真时,此宏才可以使用。返回进程退出的代码 |
    | WIFSIGNALED(status) | 子进程接收到信号结束时,返回值为真。但如果进程接收到信号时调用exit函数结束,则返回值为假 |
    | WTERMSIG(status) | 当 WIFSIGNALED 为真时,将获得终止该进程的信号 |

    - 函数返回值说明: 调用成功时,返回值为被置于等待状态的进程的 `pid`;执行失败返回`-1`并设置错误代码`errno`。

    ##### waitpid函数使用方法

    `waitpid`函数的具体的说明如下:

    - 需要的头文件如下:

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

    1
    2
    3

    - ```
    waitpid

    函数格式如下:

    1
    pid_t waitpid(pid_t pid, int *status, int options);

    参数说明:

    1
    pid

    :用于指定所等待的进程。其取值和相应的含义如下所示:

pid 含义
pid > 0 等待进程IDpid所指定值的子进程
pid = 0 等待进程组ID与该进程相同的子进程
pid = -1 等待所有子进程,等价于wait调用
pid < -1 等待进程组IDpid绝对值的子进程

参数option则用于指定进程所做操作。其取值和相应的含义如下所示:

option 含义
0 将进程挂起等待其结束
WNOHANG 不使进程挂起而立刻返回
WUNTRACED 如果进程已结束则返回

参数status是一个整数指针,当子进程结束时,将子进程的结束状态字存放在该指针指向的缓存区。

宏定义 含义
WIFEXITED(status) 子进程正常结束时,返回值为真(非零值)
WEXITSTATUS(status) 当WIFEXITED为真时,此宏才可以使用。返回进程退出的代码
WIFSIGNALED(status) 子进程接收到信号结束时,返回值为真。但如果进程接收到信号时调用exit函数结束,则返回值为假
WTERMSIG(status) 当WIFSIGNALED为真时,将获得终止该进程的信号
WIFSTOPPED(status) 在调用函数waitpid时制定了WUNTRACED选项,且该子进程使waitpid返回时,这个宏的返回值为真
WSTOPSIG(status) 当WIFSTOPPED为真时,将获得停止该进程的信号
  • 函数返回值说明: 调用成功时,返回收集到的子进程的进程pid;当设置选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;执行失败返回-1并设置错误代码errno

案例演示1: 编写一个程序,使用fork函数与wait函数结合创建一个新进程,使得新创建的子进程在父进程前执行。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
sleep(2);
printf("This is child process\n");
exit(1);
}
else
{
//父进程
int status;
if(wait(&status) != -1)
{
if(WIFEXITED(status))
printf("子进程正常退出,退出代码:%d\n", WEXITSTATUS(status));
}
printf("This is parent process\n");
exit(0);
}
}

img

案例演示1: 编写一个程序,使用fork函数与waitpid函数结合创建一个新进程,使得新创建的子进程在父进程前执行。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
sleep(2);
printf("This is child process\n");
exit(1);
}
else
{
//父进程
int status;
if(waitpid(-1, &status, 0) != -1)
{
if(WIFEXITED(status))
printf("子进程正常退出,退出代码:%d\n", WEXITSTATUS(status));
}
printf("This is parent process\n");
exit(0);
}
}

img 将以上代码保存为waitpidProcess.c文件,编译执行。可以看到执行waitpidProcess程序后,尽管子进程使用sleep睡眠了2秒,还是子进程先执行,然后父进程才执行。waitpid函数可以实现与wait函数相同的功能。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全waitProcess函数,等待子进程结束,并且返回子进程的退出的代码。
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
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>


/************************
* 返回值: 调用成功且子进程正常退出返回退出代码,否则返回-1
*************************/
int waitProcess()
{
int status = -1;
/********** BEGIN **********/
pid_t pid;
pid = fork();
if(pid == 0)
{
//子进程
sleep(2);
printf("This is child process\n");
exit(1);
}
else
{
//父进程
int status;
if(waitpid(-1, &status, 0) != -1)
{
if(WIFEXITED(status))
return WEXITSTATUS(status);
}
exit(0);
}

/********** END **********/

return status;
}

进程创建操作-exec函数族

在上一个实训中提到,**vfork函数创建的子进程可以通过调用exec函数族来正确退出。其原理是,使用exec函数族可以执行一个新的程序,并且以新的子进程来完全替换原有的进程地址空间**。

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

通常exec函数族用来与vfork函数结合一起使用。使用vfork函数创建一个子进程,然后在子进程中使用exec函数族来执行一个新的程序。当在由vfork创建的子进程中使用exec函数族来执行新程序时,子进程的地址空间会被新执行的程序完全覆盖,并且此时vfork的父进程与子进程地址空间被分离开,也就是使用exec函数族创建的新程序不会对vfork的父进程造成任何影响。

exec函数族是库函数,因此使用man 3 exec来查看其使用方法。

使用exec函数族创建进程

exec函数族的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数族格式如下:

    1
    2
    3
    4
    5
    6
    int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
    int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
    int execle(const char *path, const char *arg, ... /*, (char *) NULL, 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[]);

    参数说明:

    1. 函数名中含有字母l的函数,其参数个数不定。其参数由所调用程序的命令行参数列表组成,最后一个NULL表示结束。函数名中含所有字母v的函数,则是使用一个字符串数组指针argv指向参数列表,这一字符串数组和含有l的函数中的参数列表完全相同,也同样以NULL结束。

    2. 函数名中含有字母p的函数可以自动在环境变量PATH指定的路径中搜索要执行的程序。因此它的第一个参数为file表示可执行函数的文件名。而其他函数则需要用户在参数列表中指定该程序路径,其第一个参数path 是路径名。路径的指定可以是绝对路径,也可一个是相对路径。但出于对系统安全的考虑,建议使用绝对路径而尽量避免使用相对路径。

    3. 函数名中含有字母e的函数,比其他函数多含有一个参数envp。该参数是字符串数组指针,用于制定环境变量。调用这两个函数时,可以由用户自行设定子进程的环境变量,存放在参数envp所指向的字符串数组中。这个字符串数组也必须由NULL结束。其他函数则是接收当前环境。

      函数返回值说明: 只有当函数执行失败时,exec函数族才会返回-1并设置错误代码errno。当执行成功时,exec函数族是不会返回任何值。

案例演示1: 编写一个程序,使用vfork函数与exec函数族结合创建一个新进程,并在子进程中执行touch testFile命令创建一个testFile文件,在父进程中返回进程ID。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = vfork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
if(execlp("touch", "touch", "testFile", NULL) < 0)
{
//执行execlp函数失败
exit(-1);
}
}
else
{
//父进程
printf("当前进程为父进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
//如果执行execlp成功,则以下代码只会被父进程执行
exit(0);
}

img 将以上代码保存为execlProcess.c文件,编译执行。可以看到执行execlProcess程序后,在当前目录下创建了一个名为testFile的文件。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全execlProcess函数,使用vfork函数创建进程,并在子进程中调用创建一个名为testDir的目录,在父进程中输出”Parent Process”字符串。
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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/************************
* 提示: 在子进程中如果执行exec函数失败要使用exit函数正确退出子进程
*************************/
void execlProcess()
{
pid_t pid = vfork();
if(pid == -1)
{
printf("创建子进程失败(%s)\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
/********** BEGIN **********/
if(execlp("mkdir", "mkdir", "testDir", NULL) < 0)
{
//执行execlp函数失败
exit(-1);
}

/********** END **********/
}
else
{
//父进程
/********** BEGIN **********/
printf("Parent Process");

/********** END **********/
}
}

进程创建操作-system

system函数是一个和操作系统相关紧密的函数。用户可以使用它来在用户自己的程序中调用系统提供的各种命令。

执行系统的命令行,其实也是调用程序创建一个进程来实现的。实际上,system函数的实现正是通过调用forkexecwaitpid函数来完成的。详细的实现思路是:首先使用fork创建一个新的进程,并且在子进程中通过调用exec函数族来执行一个新程序,在父进程中通过waitpid函数等待子进程的结束,同时也获取子进程退出代码。

system函数是库函数,因此使用man 3 system来查看其使用方法。

使用system函数执行程序一个新程序

system函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数格式如下:

    1
    int system(const char *command);

    参数说明: command:需要被执行的命令;

  • 函数返回值说明: 执行成功,返回值是执行命令得到的返回状态,如同wait的返回值一样。执行失败时返回的值分为以下几种情况:执行system函数时,它将调用forkexecwaitpid函数。其中任意一个调用失败都可以使得system函数的调用失败。如果调用fork函数出错,则返回值为-1errno被设置为相应错误;如果调用exec时失败,则表示shell无法执行所设命令,返回值为shell操作的返回值;如果调用waitpid函数失败,则返回值也为-1errno被置为相应值。

案例演示1: 编写一个程序,使用system函数来执行touch testFile命令创建一个testFile文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
int ret = system("touch testFile");
if(ret == -1)
{
printf("执行 touch testFile 命令失败(%s)\n", strerror(errno));
return -1;
}
return 0;
}

img 将以上代码保存为system.c文件,编译执行。可以看到执行system程序后,在当前目录下创建了一个名为testFile的文件。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全createProcess函数,使用system函数创建一个名为testDir的目录(** 调用成功返回命令的状态码,失败返回-1**)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

/************************
* 返回值: 调用成功返回命令的状态码,失败返回-1
*************************/
int createProcess()
{
int ret = -1;
/********** BEGIN **********/
ret = system("mkdir testDir");
if(ret == -1)
{
return -1;
}

/********** END **********/

return ret;
}

实现一个简单的命令解析器

Linux系统中Shell是非常重要的一个工具,Shell是一个用C语言编写的程序,它是用户使用 Linux 的桥梁。当打开一个Shell(终端),我们直接可以在命令行中输入要执行的命令,然后Shell会自动的读取我们输入的命令,最后执行这些命令。

RShell的主要思路是:(1)读取用户输入的命令;(2)然后创建一个子进程;(3)使用exec函数族来执行输入的命令,同时挂起父进程;当子进程执行完成后,重复执行步骤1-3即可实现一个简单的命令解析器工具。

使用system函数实现一个简单的命令解析器

system函数可以执行一个命令,那么本案例将介绍如何使用system函数来实现一个简单的命令解析器RShell。详细的步骤可分为以下几步:

1
2
3
读取用户输入;
调用system函数执行命令;
重复第一步;

详细的代码设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[])
{
char command[128];
while(1)
{
printf("RShell>>");
gets(command);
if(strcasecmp(command, "exit") == 0)
break; //当用户输入exit命令后,退出RShell工具
system(command);
}
return 0;
}

img 将以上代码保存为systemRShell.c文件中,编译执行。可以看到执行systemRShell命名后,我们就可以输入要执行的命令,然后按下回车,该命令就会被执行。当想退出systemRShell时,只需要输入exit回车即可。

使用forkexec函数族和wait实现一个简单的命令解析器

forkexec也可以完成执行一个新程序。详细的步骤可分为以下几步:

1
2
3
4
5
读取用户输入;
调用fork函数创建一个子进程;
在子进程中调用exec来执行用户输入的命令;
在父进程中使用wait来等待子进程执行结束;
重复第一步;

详细的代码设计为:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[])
{
char command[128];
while(1)
{
printf("RShell>>");
gets(command);
if(strcasecmp(command, "exit") == 0)
break; //当用户输入exit命令后,退出RShell工具
pid_t pid = fork();
if(pid < 0)
{
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
char *file;
char *point = NULL;
char *tmpArg[10];
point = strtok(command, " ");
file = point;
int index = 0;
while(point != NULL)
{
if(index == 9)
break;
tmpArg[index++] = point;
point = strtok(NULL, " ");
}
char **arg = malloc(sizeof(char *)*(index+1));
int i;
for(i = 0; i < index; i++)
arg[i] = tmpArg[i];
arg[index] = NULL;
if(execvp(file, arg) < 0)
{
//执行execvp函数失败
exit(-1);
}
}
else
{
int status;
wait(&status);
}
}
return 0;
}

img 将以上代码保存为execRShell.c文件中,编译执行。可以看到使用forkexecwait也可以实现一个简单的命令解析器工具。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全RShell函数,实现一个简单的命令解析器工具。(提示:可以使用system函数或者fork+exec+wait。)
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
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

/************************
* 参数cmd: 存放要被执行的命令
* 参数commandNum: 命令的个数
* 案例: cmd = {"ls", "touch testFile"} commandNum = 2
*************************/
void RShell(char *cmd[], int commandNum)
{
/********** BEGIN **********/
int temp=0;
while(temp<=commandNum){
pid_t pid = fork();
if(pid == 0)
{
system(cmd[temp]);
exit(0);
}
else
{
sleep(2);
temp++;
}
}
/********** END **********/
}


进程通讯

信号处理函数

相关知识

在 Linux 中,每一个信号都有一个名字,这些名字以 SIG 开头。例如, SIGABRT 是夭折信号,当进程调用 abort 函数时会产生这种信号。SIGALRM 是闹钟信号,由 alarm 函数设置的定时器超时后将产生此信号。

信号产生

信号产生是指触发信号的事件的发生。

例如,通过键盘输入组合键CTRL+C系统会收到 SIGINT。 通过killall -sigid processname以给指定进程发送信号。

比如killall -SIGKILL testsignal给 testsignal 发送 SIGKILL 信号,即杀死进程的信号。

SIGUSR1 和 SIGUSR2 是用户自定义信号,通过上述的方式也可以将信号 SIGUSR1 和 SIGUSR2 传递给进程。

信号的处理动作

信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量来判断是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行以下操作”。

在某个信号出现时,可以告诉内核按照以下三种方式之一进行处理,我们称之为信号的处理或与信号相关的动作:

  • 忽略此信号。大多数信号可以使用这种方式进行处理,但是 SIGKILL 和 SIGSTOP 除外。
  • 捕获信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。
  • 执行系统默认动作。对于大多数信号来说,系统默认动作是终止该进程。
信号处理过程
注册信号处理函数

信号的处理是由内核来代理的,首先程序通过 signal 为每个信号注册处理函数,而内核中有一张信号向量表,对应信号处理机制。这样,信号在进程中注销完毕之后,会调用相应的处理函数进行处理。

信号的检测与响应时机

在系统调用或中断返回用户态的前夕,内核会检查未决信号集,进行相应的信号处理。

处理过程
  • 程序运行在用户态时;
  • 进程由于系统调用或中断进入内核;
  • 转向用户态执行信号处理函数;
  • 信号处理函数完毕后进入内核;
  • 返回用户态继续执行程序。
signal处理接口

signal 函数是最简单的信号处理接口,也是使用比较广泛的一个接口。

1
2
3
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

预览大图

参数的含义:

  • signum:信号名,一般不允许是 SIGKILL 或 SIGSTOP ;
  • handler:常量 SIG_IGN、常量 SIG_DFL或者当收到此信号后要调用的函数的地址。如果是 SIG_IGN,则忽略此信号。如果是 SIG_DFL,则使用系统默认动作。

返回值:返回 sighandler_t句柄或者 SIG_ERR。

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int catch(int sig);
int main()
{
signal(SIGINT,catch);
printf("hello!
");
sleep(10);
printf("hello!
");
}
int catch(int sig)
{
printf("catch signal!
");
return 1;
}

运行步骤如下: 运行程序: 在 10s 内按键CTRL+C

运行结果如下: hello! ^Ccatch signal! hello!

编程要求

在主函数的最开始会初始化一个全部变量 g_i4event 为 0。

本关的编程任务是补全右侧代码片段中两段BeginEnd中间的代码,具体要求如下:

  • 在 do _signal中分别为信号 SIGUSR1 、 SIGUSR2 注册信号处理函数 funcA 和 funcB ,而后将 g_i4event 置为 1;
  • 完成两个信号处理函数,其中 funcA 中将 g_i4event 置为 2, funcB 中将 g_i4event 为 3。
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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_i4event;
typedef void (*sighandler_t)(int);
/********Begin********/
/*实现funcA和funcB*/
void funcA(int sig)
{
if(g_i4event==1){
printf("The courier has received the task of dispatching milk\n");
printf("The courier has gotten the milk from the store\n");
}
g_i4event=2;
}
void funcB(int sig)
{
if(g_i4event==1){
printf("The courier has received the task of dispatching milk\n");
printf("The courier has not taken the milk from store\n");
}else if(g_i4event==2){
printf("The courier has put the milk in your box\n");
}
g_i4event=3;
}
/*********End*********/

int do_signal(void)
{
/********Begin********/

signal(SIGUSR1,funcA);
signal(SIGUSR2,funcB);

g_i4event=1;

/*********End*********/
}

signal高级处理之sigaction

在 Linux 信号处理函数中,signal函数是最基本的,由于系统版本的不同,signal 由ISO C定义。因为 ISO C 不涉及到多进程、进程组以及终端 I /O等,所以它对信号的定义比较模糊

Unix system V派生的实现支持 signal 函数,但该函数提供旧的不可靠信号语义。4.4BSD 也提供了 signal 函数,并且提供了的信号语义。

因此,signal 的语义与实现有关,为了保险起见,最好使用别的函数来代替 signal 函数。这个函数是 sigaction,也是本实训讲解的重点。

sigaction函数

sigaction 函数取代了 UNIX 早期版本使用的 signal 函数。

1
2
#include <signal.h>
int sigaction(int signo, const struct sigaction *act,struct sigaction *oldact));

img

参数的含义:

  • signo :信号的值,可以为除 SIGKILL 及 SIGSTOP 外的任何一个特定有效的信号;
  • act :指向结构 sigaction 的一个实例的指针,在结构 sigaction 的实例中,指定了对特定信号的处理,但可以为空,进程会以缺省方式对信号处理;
  • oldact :对象指针,指向的对象用来保存返回的原来对相应信号的处理,可指定 oldact 为 NULL 。

注:如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

返回值: 0 表示成功,-1 表示有错误发生。

功能: sigaction 函数用于改变进程接收到特定信号后的行为。

sigaction结构体详解

sigaction 函数最重要的部分就是sigaction结构体,这个被应用于参数 act 和 oldact 中,其定义如下:

1
2
3
4
5
6
7
8
9
10
struct sigaction 
{
union
{
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int,struct siginfo *, void *);
}_u
sigset_t sa_mask;
unsigned long sa_flags;
}
  • 联合数据结构中的两个元素_sa_handler以及 _sa_sigaction 指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为 SIG_IGN (忽略信号);
  • 由 _sa_sigaction 指定的信号处理函数带有三个参数,是为实时信号而设的,它指定一个三参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向 siginfo_t 结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
siginfo_t 
{
int si_signo; /* 信号值,对所有信号有意义*/
int si_errno; /* errno值,对所有信号有意义*/
int si_code; /* 信号产生的原因,对所有信号有意义*/
union
{
/* 联合数据结构,不同成员适应不同信号 */
//确保分配足够大的存储空间
int _pad[SI_PAD_SIZE];
//对SIGKILL有意义的结构
struct
{
...
}...
... ...
//对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
struct
{
...
}...
... ...
}
}
  • sa_mask : 信号集,指定在信号处理程序执行过程中,哪些信号应当被阻塞缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定 SA_NODEFER 或者 SA_NOMASK 标志位。

注:请注意 sa_mask 指定的信号阻塞的前提条件:在sigaction()安装信号的处理函数执行过程中,由 sa_mask 指定的信号才会被阻塞。在使用 sigaction 之前,请务必清空或者设置自己所需要的屏蔽字段

  • sa_flags 中包含了许多标志位,包括 SA_NODEFER 及 SA_NOMASK 标志位。另一个比较重要的标志位是 SA_SIGINFO ,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为 sigaction 结构中的 sa_sigaction 指定处理函数,而不应该为 sa_handler 指定信号处理函数,否则设置该标志变得毫无意义。即使为 sa_sigaction 指定了信号处理函数,如果不设置 SA_SIGINFO ,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致错误。 一般的做法是,如果采用 _sa_handler 作为处理函数,则将 sa_flags 设定为0;如果采用 _sa_sigaction 作为处理函数,则将 sa_flags 设定为 SA_SIGINFO。

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <signal.h>
int catch(int sig);
int main()
{
struct sigaction act;
struct sigaction oldact;
/*注册信号处理函数*/
act.sa_handler = catch;
sigemptyset(&act.sa_mask);//清空sa_mask,这点尤为重要
act.sa_flags = 0;
sigaction(SIGINT, act ,oldact);
printf("hello!
");
sleep(10);
printf("hello!
");
}
int catch(int sig)
{
printf("catch signal!
");
return 1;
}

运行步骤如下:

  1. 运行程序;
  2. 在 10s 内按键CTRL +C

运行结果如下: hello! ^Ccatch signal! hello!

编程要求

在主函数的最开始会初始化一个全部变量 g_i4event 为 0。

本关的编程任务是补全右侧代码片段中两段BeginEnd中间的代码,具体要求如下:

  • 在 do _sigaction中分别为信号 SIGUSR1 、 SIGUSR2 注册信号处理函数 funcA 和 funcB ,而后将 g_i4event 置为 1;
  • 完成两个信号处理函数,其中 funcA 中将 g_i4event 置为 2, funcB 中将 g_i4event 置为 3。

注:采用_sa_sigactionSA_SIGINFO来实现。

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_i4event;
/********Begin********/
/*实现funcA和funcB*/
void funcA(int sig)
{
if(g_i4event==1){
printf("The JD has notified the storehouse and installer\n");
printf("The TV has delivered to your house from the storehouse\n");
}
g_i4event=2;


}
void funcB(int sig)
{
if(g_i4event==1){
printf("The JD has notified the storehouse and installer\n");
printf("The installer has arrived your house, but the TV is not delivered\n");
}else if(g_i4event==2){
printf("The installer has installed your TV\n");
}
g_i4event=3;

}

/*********End*********/

int do_sigaction(void)
{
/********Begin********/
signal(SIGUSR1,funcA);
signal(SIGUSR2,funcB);
g_i4event=1;
/*********End*********/
}

Linux定时器

alarm函数

使用 alarm 函数可以设置一个定时器,在将来的某个时刻,这个定时器就会超时。当超时时,会产生 SIGALRM 信号。如果忽略或者不捕捉此信号,则其默认动作时终止调用该 alarm 函数的进程。

1
2
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

参数: seconds ,是产生信号需要经过的时钟秒数,也就是定时器的时间。

alarm 安排内核调用进程——在指定的 seconds 秒后发出一个 SIGALRM 的信号。如果指定的参数 seconds 为 0 ,则不再发送 SIGALRM 信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回 0 。

注意,在使用时,alarm 只设定为发送一次信号,如果要多次发送,需要多次使用 alarm 调用。

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
int catch(int sig)
{
printf("catch alarm signal!
");
return 0;
}
int main()
{
alarm(3);
sighandler_t res = signal(SIGALRM, catch);
printf("wait for alarm signal!
");
sleep (5);
}

运行结果如下:

1
2
wait for alarm signal!
catch alarm signal!

在主函数的最开始会初始化一个全部变量 g_i4event 为 0 。

本关的编程任务是补全右侧代码片段中两段BeginEnd中间的代码,具体要求如下:

  • 在 do _alarm中首先启动 5s 定时器,将 g_i4event 置为 1;
  • 睡眠一秒,然后为信号 SIGALRM 注册信号处理函数 funcalarm ,将 g_i4event 置为 2;
  • 在信号处理函数,将 g_i4event 置为 3。
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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_i4event;
typedef void (*sighandler_t)(int);
/********Begin********/
/*实现funcA和funcB*/
void funcA(int sig)

{
if(g_i4event==1){printf("You set the alarm clock\n");
printf("You start to work and wait for the alarm ring\n");
}
g_i4event=3;
//return 1;
}

void funcB(int sig)
{
if(g_i4event==2){
printf("Now is 15:30, please leave home and go to apointment\n");
}
g_i4event=3;

}

/*********End*********/

int do_alarm(void)
{
/********Begin********/
alarm(5);
g_i4event=1;
sighandler_t res = signal(SIGALRM, funcA);
sleep (1);
g_i4event=2;

/*********End*********/
}

FIFO管道使用

本关将介绍命名管道的使用方法。对于命名管道和普通文件的操作一样,使用open函数打开,然后使用read和close进行读写操作,最后使用close函数对其进行关闭。与普通文件操作的不同点是:使用read读取普通文件,read不会阻塞。而read读取管道文件,read会阻塞运行,直到管道中有数据或者所有的写端关闭。

命名管道相对比无名管道的好处是可以在两个不同的程序进行数据的传输。并且命名管道当写端彻底关闭后,读端read才会返回0。 注意:

如果管道的读端提前关闭,写端继续写入数据就会发生异常。 读端读取管道中的数据,只要读过的数据就会被清空 。 命名管道使用方法 创建命名管道的库函数是mkfifo,具体的说明如下:

需要的头文件如下: #include <sys/types.h> #include <sys/stat.h> 函数格式如下: int mkfifo(const char *pathname, mode_t mode); 参数说明: pathname:存放命名管道的文件名; mode:创建命名管道的权限,与创建文件的权限参数一致; mode设置说明:

S_IRUSR: 文件所有者的读权限位 S_IWUSR: 文件所有者的写权限位 S_IXUSR : 文件所有者的执行权限位 S_IRGRP: 所有者同组用户的读权限位 S_IWGRP: 所有者同组用户的写权限位 S_IXGRP: 所有者同组用户的执行权限位 S_IROTH: 其他用户的读权限位 S_IWOTH: 其他用户的写权限位 S_IXOTH: 其他用户的执行权限位 函数返回值说明: 调用成功时,返回值0;调用失败时,返回值为-1并设置错误编号errno。 案例演示1: 使用mkfifo函数在当前目录下创建一个命名管道testFIFO,并设置权限为644,并用来传送数据。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int ret = mkfifo("testFIFO", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if(ret == -1)
{
printf("创建命名管道失败(%s)!\n", strerror(errno));
return -1;
}
else
{
printf("创建命名管道成功!\n");
}
return 0;
}

将以上代码保存为createFIFO.c文件,编译执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
char *buf = "This is test FIFO";
int fd = open("testFIFO", O_WRONLY);
if(fd == -1)
{
printf("打开命名管道失败(%s)\n", strerror(errno));
return -1;
}
else
{
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}

将以上代码保存为writeFIFO.c文件,编译执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
char buf[128] = "";
int fd = open("testFIFO", O_RDONLY);
if(fd == -1)
{
printf("打开命名管道失败(%s)\n", strerror(errno));
return -1;
}
else
{
int size = read(fd, buf, 128);
printf("管道中的数据为:%s(%d个字符)\n", buf, size);
}
close(fd);
return 0;
}

将以上代码保存为readFIFO.c文件,编译执行。 首先执行createFIFO程序来创建一个命名管道,然后执行writeFIFO程序用来向管道中写入数据,最后在另一个终端中执行readFIFO程序从管道中读取数据。

编程要求

本关的编程任务是补全右侧代码片段中Begin至End中间的代码,具体要求如下:

创建一个名为FIFO的命名管道文件,并设置权限为650。

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
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
/********** BEGIN **********/
int ret = mkfifo("FIFO", S_IRUSR | S_IWUSR | S_IRGRP | S_IXGRP);
if(ret == -1)
{
printf("创建命名管道失败(%s)!\n", strerror(errno));
return -1;
}
else
{
printf("创建命名管道成功!\n");
}

/********** END **********/

return 0;
}

网络编程

TCP套接字创建与端口绑定

相关知识

在Linux系统中,每种协议都有自己的网络地址数据结构,这些数据结构都以sockaddr_开头,不同的是后缀表示不同的协议。最为常见的是IPv4协议,它的网络地址数据结构为sockaddr_in

由于历史的缘故,在有些库函数中,特定协议的套接字地址结构都要强制转为通用的套接字地址结构,该数据结构被定义在<sys.socket.h>头文件中,详细定义如下所示:

1
2
3
4
5
struct sockaddr
{
unsigned short int sa_family; //套接字地址族
unsigned char sa_data[14]; //14个字节的协议地址
};

其中,sa_family表示套接字的协议族类型,对应于TCP/IP协议该值为AF_INET;成员sa_data存储具体的协议地址。一般在编程中并不对该结构体进行操作,而是使用另一个与它等价的数据结构,例如,IPv4协议的网络地址数据结构为sockaddr_in,格式如下所示:

1
2
3
4
5
6
struct sockaddr_in {
unsigned short sin_family; /*地址类型*/
unsigned short int sin_port; /* 端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /* 填充字节,一般赋值为0 */
};

其中,sa_family表示套接字的协议族类型,对应于TCP/IP协议该值为AF_INETsin_port表示端口号;sin_addr用来存储32位的IP地址,数组sin_zero为填充字段,一般赋值为0

struct in_addr的定义如下所示:

1
2
3
struct in_addr {    
unsigned long s_addr;
}

结构体sockaddr的长度为16字节,结构体sockaddr_in的长度也为16字节,通常在编写基于TCP/IP协议的网络程序时,使用结构体sockaddr_in来设置地址,然后通过强制类型转换成sockaddr类型。结构sockaddr_insockaddr的转换关系如下图所示:

img

TCP网络编程是目前比较通用的方式,例如:HTTP协议、FTP协议等很多广泛应用的协议都是基于TCP协议实现的。TCP编程有两种模式,分别是服务器模式和客户端模式。无论是服务器模式还是客户端模式首先需要创建一个TCP套接字,对于服务器模式,我们还需要绑定一个本地端口。

Linux系统中提供了socketbind两个系统调用函数用来创建套接字与绑定端口操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

创建TCP套接字

Linux系统提供一个socket系统调用来创建一个套接字。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    int socket(int domain, int type, int protocol);

    参数说明:

    1
    domain:创建套接字所使用的协议族;type:套接字类型;protocol:用于指定某个协议的特定类型,通常某个协议中只有一种特定类型,这样该参数的值仅能设置为0;

domain参数的常用的协议族如下表所示:

取值 含义
AF_UNIX 创建只在本机内进行通信的套接字
AF_INET 使用IPv4 TCP/IP协议
AF_INET6 使用IPv6 TCP/IP协议

type参数的常用的套接字类型如下表所示:

取值 含义
SOCK_STREAM 创建TCP流套接字
SOCK_DGRAM 创建UDP流套接字
SOCK_RAW 创建原始套接字
  • 函数返回值说明: 执行成功返回值为一个新创建的套接字,否则返回-1,并设置错误代码errno

案例演示1: 创建一个使用IPv4协议族的TCP类型的套接字。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
return 0;
}

img 将以上代码保存为createTCP.c文件,编译执行。

绑定端口

TCP服务器模式编程中,我们需要讲一个端口绑定到一个已经建立的套接字上,这样方便客户端程序根据绑定的端口来连接服务器程序。Linux提供了一个bind函数来将一个套接字和某个端口绑定在一起。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:是一个指向sockaddr参数的指针,其中包含了IP地址、端口;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

注意:

1
2
如果创建套接字时使用的是AF_INET协议族,则addr参数所使用的结构体为struct sockaddr_in指针(详细定义见相关知识)。当设置addr参数的sin_addr为INADDR_ANY而不是某个确定的IP地址时,就可以绑定到任何网络接口。对于只有一个IP地址的计算机,INADDR_ANY对应的就是它的IP地址;对于有多个网卡的主机,INADDR_ANY表示本服务器程序将处理来自任何网卡上相应端口的连接请求。由于计算机中的字符与网络中的字符存储顺序不同。计算机中的整型数与网络中的整型数进行交换时,需要借用相关的函数进行转换。这些函数如下所示:
uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);

使用man 2 函数名可以查看其详细的介绍。

案例演示1: 创建一个使用IPv4协议族的TCP类型的套接字,并与6666这个端口进行绑定。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("绑定端口%d成功\n", PORT);
}
return 0;
}

img 将以上代码保存为bindPort.c文件,编译执行。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全bindSocket函数中代码,绑定一个指定的本地端口。
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 <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

/************************
* sockfd: 已经创建的套接字
* port: 需要绑定的端口号
* 返回值: 调用成功返回0,否则返回-1
*************************/
int bindSocket(int sockfd, unsigned short port)
{
int ret = -1;
/********** BEGIN **********/
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
ret=bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

/********** END **********/

return ret;
}

TCP监听与接收连接

相关知识

所谓TCP套接字的监听,指的是TCP套接字的端口处于等待状态,如果有客户端发出连接请求时,这个端口将会接受这个连接请求。连接指的是客户端向服务器发出一个通信请求,服务器会响应这个请求。

当服务器处于监听状态时,如果获得了一个客户端请求,则服务器将会将这个请求存放等待队列中。当系统处于空闲状态时,服务器将会从等待队列中取出请求,接受这个请求并处理这个请求。

Linux系统中提供了listenaccept两个系统调用函数用来监听和接受连接请求操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

TCP监听

Linux系统提供一个listen系统调用来实现监听等待功能。 listen函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int listen(int sockfd, int backlog); 参数说明:

    1
    sockfd:已经创建的套接字;backlog:能同时监听的最大连接请求;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

注意:listen并未真正接受客户端的连接请求,只是设置socket的状态为listen模式。

案例演示1: 使用listen函数来监听指定端口的连接信息。详细代码如下所示:

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
42
43
44
45
46
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("绑定端口%d成功\n", PORT);
}
//设置最大监听客户端数为5
if(listen(sockfd, 5) == -1)
{
printf("监听端口%d失败: %s\n", PORT, strerror(errno));
return -1;
}
else
{
printf("监听%d端口中...\n", PORT);
}
return 0;
}

img 将以上代码保存为listenPort.c文件,编译执行。

接受连接

当服务器处理监听状态时,如果客户端发出一个连接请求,则服务器需要接受这个请求,并处理该请求。Linux提供了一个accept函数来接受客户端的连接请求。 accept函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:保存发起连接请求的主机IP地址和端口信息;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功返回值为被接受请求的套接字编号,否则返回-1,并设置错误代码errno

注意:accept函数接受了一个连接时,会返回一个新的socket编号。接下来的数据传送与读取都是通过这个新的socket编号进行处理的。

案例演示1: 使用accept函数来接受客户端的请求,并打印出客户端主机的基本信息。详细代码如下所示:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
//创建一个TCP套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与6666端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("绑定端口%d成功\n", PORT);
}
//监听6666端口,并设置最大监听个数为5
if(listen(sockfd, 5) == -1)
{
printf("监听端口%d失败: %s\n", PORT, strerror(errno));
return -1;
}
else
{
printf("监听%d端口中...\n", PORT);
}
int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
//接受连接请求
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) == -1)
{
printf("接受客户端请求失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("接受客户端请求成功\n");
//inet_ntoa函数将网络地址转换成.点隔的字符串格式
printf("客户端的IP地址:%s \t 端口:%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
}
return 0;
}

img 将以上代码保存为acceptClient.c文件,编译执行。在浏览器中,输入localhost:6666进行请求,由于HTTP请求也是采用TCP协议进行传送,所以在对6666端口进行访问时,服务器接受了该请求,并打印出了发起连接请求客户端IP地址和端口。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全main函数中代码,监听8888号端口(设置监听的个数大于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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与8888端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}

//监听8888端口,并设置最大监听个数大于1
/********** BEGIN **********/
listen(sockfd, 5);
/********** END **********/

//接受来自客户端的第一个连接请求
/********** BEGIN **********/
int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) != -1)
{
printf("接受客户端请求失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("监听8888端口成功,并且成功接受来自客户端的第一个请求\n");
}

/********** END **********/

close(sockfd);

return 0;
}


TCP连接的建立与终止相关知识

所谓请求连接,指的就是在客户机上向服务器发送信息时,首先需要发送一个连接请求。当服务器接受了客户端的连接请求后,客户端就可以与服务器进行数据的交换。

所谓终止连接,指的就是客户端与服务器之间交换完数据后,需要断开当前连接并释放相关的资源,这样方便下一个客户端的请求。由于服务器同时处理的连接请求数是有限的,所以当客户端交换完数据后不终止连接就会导致后续请求的连接失败。

建立TCP连接时需要经历3次握手操作,主要步骤如下:

1
连接开始时,客户端发送SYN包,并包含了自己的初始序号a;服务器收到SYN包以后会回复一个SYN包,其中包含了对上一个a包的回应信息ACK和自己的初始序号b;客户端收到回应的SYN包以后,回复一个ACK包做响应;

img [TCP建立连接三次握手过程]

终止TCP连接时需要经历4次握手操作,主要步骤如下:

1
首先进行关闭的一方(即发送第一个FIN)将执行主动关闭,而另一方(收到这个FIN)执行被动关闭;当被动关闭方收到这个FIN,它发回一个ACK;被动关闭方发送一个FIN请求;主动关闭方收到这个FIN请求后,回复一个ACK请求;

img [TCP终止连接四次握手过程]

Linux系统中提供了connectclose两个系统调用函数用来建立和终止连接请求操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

TCP建立连接请求

Linux系统提供一个connect系统调用来实现连接目标网络服务功能。 connect函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:指针要连接的服务器IP地址和端口信息;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

案例演示1: 在上一关卡的案例中,我们实现了如何监听和接受连接,本案例将使用connect实现与上一关卡案例中的服务器建立连接关系。详细代码如下所示:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
//连接服务器
if(connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)
{
printf("请求连接服务器失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("请求连接%s:%d成功\n", SERVER_IP, PORT);
}
return 0;
}

img 将以上代码保存为connectServer.c文件,编译执行。可以看到建立连接成功,服务器将客户端的IP地址和端口打印出来。注意:客户端的端口是随机分配的。

终止连接

当客户端与服务器进行数据交换完成后,我们需要断开已经建立的连接。Linux提供了两个函数来终止连接,分别是close函数和shutdown函数。

close函数的使用方法与文件的关闭一样,只需将要关闭的套接字传给函数即可,此处就不做详细的介绍。

shutdown函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/socket.h>
  • 函数格式如下:

    1
    int shutdown(int sockfd, int how);

    参数说明:

    1
    sockfd:已经建立连接的套接字编号;how:具体的关闭行为;

how参数的常用取值及其含义如下表所示:

取值 含义
SHUT_RD 表示切断读,之后不能使用该套接字进行读操作
SHUT_WR 表示切断写,之后不能使用该套接字进行写操作
SHUT_RDWR 表示切断读写,与close函数功能一样
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全connectSocket函数中代码,向指定的服务器发出连接请求。
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
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

/************************
* ipAddr: 远程服务器的IP地址
* port: 远程服务器的端口
*************************/
void connectSocket(char *ipAddr, unsigned short port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return ;
}

//连接到指定的服务器
/********** BEGIN **********/
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(port);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(ipAddr);
//连接服务器
connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr));

/********** END **********/

close(sockfd);
}


TCP数据传送

相关知识

当创建好TCP套接字并且完成了客户端与服务器间的连接,接下来,我们就可以实现在客户端与服务器间的数据传送功能。

在客户端与服务器间进行数据传送时,一端(客户端/服务器)用于向建立好的套接字写入数据,另一端(服务器/客户端)用于从建立好的套接字中读取数据,这样一来一回的就实现了客户端与服务器间的数据交换功能。

img [TCP服务器与客户端间的数据传送框架]

Linux系统中提供了recvsendreadwrite四个系统调用函数用来完成客户端与服务器间的数据发送和接收操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

数据发送与接收方式一

Linux系统提供recvsend系统调用来实现数据的发送与接收功能。 recvsend函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    参数说明:

    1
    sockfd:已经创建的套接字;buf:用于存放接收和发送的数据;len:buf的长度;flags:设置接收与发送的控制选项,一般设置为0;
  • 函数返回值说明: 调用成功,返回值为实际接收或发送的数据字节个数,否则返回-1,并设置错误代码errno

案例演示1: 使用recvsend函数实现客户端与服务器间的数据传送功能,客户端读取用户的数据并发送给服务器,服务器接收到数据后打印出来,当客户端读取到exit字符串时,关闭当前连接。详细代码如下所示:

客户端主要代码:

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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
//连接服务器
if(connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)
{
printf("请求连接服务器失败: %s\n", strerror(errno));
return -1;
}
else
{
char userInput[100];
while(gets(userInput) != NULL)
{
if(strcasecmp(userInput, "exit") == 0)
break;
send(sockfd, userInput, strlen(userInput), 0); //发送数据
}
close(sockfd); //关闭连接
}
return 0;
}

将以上代码保存为sendData.c文件。

服务器主要代码:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
//监听6666端口,并设置最大监听个数为5
if(listen(sockfd, 5) == -1)
{
printf("监听端口%d失败: %s\n", PORT, strerror(errno));
return -1;
}
int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
//接受连接请求
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) == -1)
{
printf("接受客户端请求失败: %s\n", strerror(errno));
return -1;
}
else
{
char userInput[100];
memset(userInput, 0, sizeof(userInput));
while(recv(clientSockfd, userInput, sizeof(userInput), 0) > 0)
{
printf("客户端:%s\n", userInput);
memset(userInput, 0, sizeof(userInput)); //清空上次接收的缓存
}
close(clientSockfd); //关闭客户端连接
close(sockfd); //关闭服务器套接字
}
return 0;
}

将以上代码保存为recvData.c文件。

img 编译执行以上两个程序,可以看到服务器接收到了客户端发过来的数据,并将其打印出来。注意:先执行recvData,再执行sendData程序。

数据发送与接收方式二

Linux系统可以使用writeread系统调用来实现数据的发送与接收功能。 writeread函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下:

    1
    ssize_t write(int fd, const void *buf, size_t count);ssize_t read(int fd, void *buf, size_t count);

    参数说明:

    1
    fd:已经创建的套接字;buf:用于存放接收和发送的数据;count:buf的长度;
  • 函数返回值说明: 调用成功,返回值为实际接收或发送的数据字节个数,否则返回-1,并设置错误代码errno

注意:writeread函数的使用方法与文件的读写相同,此处就不做详细的案例介绍。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全main函数中代码,实现服务器与客户端间的数据传送功能。
  • 将客户端发来的数据完全打印出来(提示:换行打印),并且将接收到的数据原样发送给客户端。
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

#define PORT 8888

int main()
{
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与PORT端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
return -1;
}

//监听PORT端口,并设置最大监听个数为5
if(listen(sockfd, 5) == -1)
{
return -1;
}

int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
//接受连接请求
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) == -1)
{
return -1;
}
else
{
char data[100];
//接收客户端传来的数据,并打印出来(提示:换行打印)
//同时将接收到的数据原样发送给客户端
/********** BEGIN **********/
memset(data, 0, sizeof(data));
while(recv(clientSockfd, data, sizeof(data), 0) > 0)
{
printf("%s\n", data);
send(clientSockfd, data, strlen(data), 0); //发送数据
memset(data, 0, sizeof(data)); //清空上次接收的缓存
}

/********** END **********/
}

close(clientSockfd);
close(sockfd);

return 0;
}


UDP套接字创建与端口绑定

相关知识

所谓无连接的套接字通信,指的就是使用UDP协议进行数据的传输。使用这种协议进行通信时,两台计算机间是不需要建立连接的。

在实训Linux之网络编程(TCP)中,我们介绍了使用TCP协议进行数据的传输。TCP协议是需要在服务器与客户端间进行建立连接的,并且能够保证传输的数据可靠的到达对方。那么TCP协议与UDP协议的区别主要包括以下几点:

1
TCP是面向连接的,也就是在数据传输前需要建立连接;而UDP时无连接的,即数据传输前不需要建立连接;TCP提供可靠的服务,也就是使用TCP协议传输的数据不会丢失、不会重复并且按序到达;而UDP协议尽最大努力将数据传输到对方,即不保证可靠交付;由于TCP需要对数据进行校验保证可靠交付,所以其实时性不如UDP好;TCP对系统资源需求较大;而UDP相对需求较少;

由于UDP以简单、传输快的优势,在很多应用场景下取代了TCP。例如:在视频会议、语音通话场景下,UDP协议就比TCP协议更适合。因为,视频和语音丢部分内容不会影响到结果,而这些对实时性要求很高,所以采用UDP协议更适合。

在编程方面,UDP使用到的数据结构与TCP一致。例如在IPv4协议下,都使用struct sockaddr_in结构体来表示网络地址信息。

Linux系统中提供了socketbind两个系统调用函数用来创建套接字与绑定端口操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

创建UDP套接字

Linux系统提供一个socket系统调用来创建一个套接字。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    int socket(int domain, int type, int protocol);

    参数说明:

    1
    domain:创建套接字所使用的协议族;type:套接字类型;protocol:用于指定某个协议的特定类型,通常某个协议中只有一种特定类型,这样该参数的值仅能设置为0;

domain参数的常用的协议族如下表所示:

取值 含义
AF_UNIX 创建只在本机内进行通信的套接字
AF_INET 使用IPv4 TCP/IP协议
AF_INET6 使用IPv6 TCP/IP协议

type参数的常用的套接字类型如下表所示:

取值 含义
SOCK_STREAM 创建TCP流套接字
SOCK_DGRAM 创建UDP流套接字
SOCK_RAW 创建原始套接字
  • 函数返回值说明: 执行成功返回值为一个新创建的套接字,否则返回-1,并设置错误代码errno

案例演示1: 创建一个使用IPv4协议族的UDP类型的套接字。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建UDP套接字成功\n");
}
return 0;
}

img 将以上代码保存为createUDP.c文件,编译执行。

绑定端口

UDP服务器模式编程中,我们需要将一个端口绑定到一个已经创建好的套接字上,这样方便客户端程序根据绑定的端口来向服务器传输数据。Linux提供了一个bind函数来将一个套接字和某个端口绑定在一起。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:是一个指向sockaddr参数的指针,其中包含了IP地址、端口;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

提示:使用bind函数为UDP协议绑定一个端口的使用方法与为TCP协议绑定端口一致,详细介绍请参看实训 Linux之网络编程(TCP) 中的第一关卡内容。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全UDPSocket函数中代码,创建一个UDP套接字,并绑定一个指定的本地端口。
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
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

/************************
* port: 需要绑定的端口号
* 返回值: 调用成功返回0,否则返回-1
*************************/
int UDPSocket(unsigned short port)
{
int ret = -1;
/********** BEGIN **********/
struct sockaddr_in adr_inet;
adr_inet.sin_family=AF_INET;
adr_inet.sin_port=htons(port);
adr_inet.sin_addr.s_addr =htonl(INADDR_ANY);
bzero(&(adr_inet.sin_zero),8);

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet))!=-1){
ret=0;
}
/********** END **********/

return ret;
}

UDP数据传送

相关知识

由于UDP协议不需要建立连接即可进行数据的传输,在服务器端创建好UDP套接字并绑定一个本地端口后,接下来,我们就可以实现在客户端与服务器间的数据传送功能。

在客户端与服务器间进行数据传送时,一端(客户端/服务器)用于向建立好的套接字写入数据,另一端(服务器/客户端)用于从建立好的套接字中读取数据,这样一来一回的就实现了客户端与服务器间的数据交换功能。

在实训Linux之网络编程(TCP)中介绍的TCP数据传输使用到的函数是recvsendreadwrite四个系统调用。而UDP协议数据传输使用的函数是sendtorecvfrom两个系统调用函数。

Linux系统中提供了sendtorecvfrom两个系统调用函数用来完成UDP协议的客户端与服务器间的数据发送和接收操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

UDP协议的数据发送

Linux系统提供sendto系统调用来实现数据的发送功能。 sendto函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

    参数说明:

    1
    sockfd:已经创建的套接字;buf:用于存放接收和发送的数据;len:buf的长度;flags:设置接收与发送的控制选项,一般设置为0;dest_addr:要发往主机的网络地址信息;addrlen:dest_addr的长度;
  • 函数返回值说明: 调用成功,返回值为实际发送的数据字节个数,否则返回-1,并设置错误代码errno

UDP协议的数据接收

Linux系统可以使用recvfrom系统调用来实现数据的接收功能。 recvfrom函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

    参数说明:

    1
    sockfd:已经创建的套接字;buf:用于存放接收和发送的数据;len:buf的长度;flags:设置接收与发送的控制选项,一般设置为0;src_addr:要接收主机的网络地址信息;addrlen:src_addr的长度;
  • 函数返回值说明: 调用成功,返回值为实际接收的数据字节个数,否则返回-1,并设置错误代码errno

案例演示1: 利用UDP协议实现如下功能:使用recvfromsendto函数实现客户端与服务器间的数据传送功能,客户端读取用户的数据并发送给服务器,服务器接收到数据后打印出来,当客户端读取到exit字符串时,关闭当前客户端套接字,当服务器收到exit字符串时,关闭当前服务器套接字。详细代码如下所示:

客户端主要代码:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
char userInput[100];
while(gets(userInput) != NULL)
{
sendto(sockfd, userInput, sizeof(userInput), 0, (struct sockaddr *)&servAddr, sizeof(servAddr)); //发送数据
if(strcasecmp(userInput, "exit") == 0)
break;
}
close(sockfd); //关闭连接
return 0;
}

将以上代码保存为sendData.c文件。

服务器主要代码:

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
char userInput[100];
while(recvfrom(sockfd, userInput, sizeof(userInput), 0, (struct sockaddr *)&clientAddr, &clientAddrLen) > 0)
{
if(strcasecmp(userInput, "exit") == 0)
break;
printf("客户端:%s\n", userInput);
}
close(sockfd); //关闭服务器套接字
return 0;
}

将以上代码保存为recvData.c文件。

img 编译执行以上两个程序,可以看到服务器接收到了客户端发过来的数据,并将其打印出来。 注意:

1
先执行recvData,再执行sendData程序;由于UDP协议可以实现多个客户端对于一个服务器,所以当客户端退出后,服务器是不会退出的,因此,我们通过接收客户端传来的数据来判断是否服务器也需要退出;

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全main函数中代码,使用UDP协议实现服务器与客户端间的数据传送功能。
  • 将客户端发来的数据完全打印出来(提示:换行打印),并且将接收到的数据原样发送给客户端。
  • 当服务器收到exit字符串时,退出当前服务器程序(提示:不打印退出字符串exit)。
  • 提示:在每次接收字符串前要将存放字符串的变量清空
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
42
43
44
45
46
47
48
49
50
51
52
53
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

#define PORT 8888

int main()
{
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与PORT端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
return -1;
}

char data[100];
//接收客户端传来的数据,并打印出来(提示:换行打印)
//同时将接收到的数据原样发送给客户端
//当接收到"exit"字符串时,退出当前程序,不打印出"exit"字符串
//提示:在每次接收字符串前要将存放字符串的变量清空
/********** BEGIN **********/
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
memset(data, 0, sizeof(data));
while(recvfrom(sockfd, data, sizeof(data), 0 ,(struct sockaddr *)&clientAddr, &clientAddrLen) > 0)
{
if(strcasecmp(data, "exit") == 0)
break;
printf("%s\n", data);
sendto(sockfd, data, strlen(data), 0 ,(struct sockaddr *)&clientAddr, sizeof(clientAddr)); //发送数据
memset(data, 0, sizeof(data)); //清空上次接收的缓存
}

/********** END **********/

close(sockfd);

return 0;
}

UDP项目实战

相关知识

通过前2关的学习,我们学会如何使用UDP协议来在不同的计算机间传输数据。使用UDP协议通信时,客户端与服务器的交互过程如下图所示:

img [UDP协议的套接字通信]

利用以上2关的知识就可以简单的文件上传工具。该工具包含两部分内容,分别是接收文件的服务器和上传文件的客户端。

实现文件上传工具-服务器端

实现文件上传服务器主要分为以下几个步骤:

1
首先是接收要上传的文件名称;当接收到客户端发来的文件名称后,在本地创建该文件;从客户端中接收文件的内容,并保存在创建好的文件中;当客户端上传文件内容接收后,服务器关闭创建的文件;

通过以上几步就可以实现文件上传服务器的功能。为了识别客户端发送来的数据类型,我们定义了以下协议:

1
我们定义客户端与服务器间的数据块为1024KB(也就是每次传输的数据大小);我们将每个数据块的前1个字节定义为数据块的类型,也就是用于标示该数据块中的内容是文件名称或文件内容或上传结束,我们使用f表示该数据块为文件名称,c标示该数据块为文件内容,e表示上传结束;

详细的代码设计为:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <arpa/inet.h>
//定义数据块大小
char data[1024];
//定义服务器端口
#define PORT 6667
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
//存放客户端主机信息
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
int fd;
int recvLen;
//接收来自客户端发来的数据
while((recvLen = recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&clientAddr, &clientAddrLen)) > 0)
{
//根据自定义的协议来判断客户端发送来的数据块类型
if(data[0] == 'e')
{
//上传文件完成,关闭当前打开的文件
close(fd);
break;
}
else if(data[0] == 'f')
{
//数据块是上传文件的名称,在本地创建该文件
fd = open(&(data[1]), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
printf("接收客户端上传的%s文件...\n", &(data[1]));
}
else
{
//数据块为上传文件的内容,将内容写入到新创建的文件中
//因为data的第一个字符为文件块类型,所以只需从第二个字符开始写文件
write(fd, &(data[1]), recvLen - 1);
}
//给客户端回复一个接收确认的标识OK
sendto(sockfd, "OK", strlen("OK"), 0, (struct sockaddr *)&clientAddr, clientAddrLen);
memset(data, 0, sizeof(data));
}
close(sockfd); //关闭服务器套接字
return 0;
}

将以上代码保存为uploadFileServer.c文件中,编译执行。

实现文件上传工具-客户端

实现文件上传客户端主要分为以下几个步骤:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
//定义数据块大小
char data[1024];
//定义服务器端口和服务器地址
#define PORT 6667
#define SERVER_IP "127.0.0.1"
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in servAddr;
int servAddrLen = sizeof(servAddr);
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
char filePath[100];
//读取要上传文件的路径
printf("请输入上传的文件路径+名称: ");
scanf("%s", filePath);
memset(data, 0, 1024);
data[0] = 'f'; //根据上传协议,设置数据块类型为文件名称
strcpy(&(data[1]), filePath);
//向服务器发送要上传文件的名称
sendto(sockfd, data, strlen(data), 0, (struct sockaddr *)&servAddr, servAddrLen);
memset(data, 0, 1024);
//等待服务器接收确认,然后再继续上传文件内容
recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen);
int fd = open(filePath, O_RDONLY); //打开要上传的文件
int readSize = 0;
data[0] = 'c'; //设置数据块类型为文件内容
//循环读取文件内容(注意:一次实际读取的文件内容最大字符个数为1023)
while((readSize = read(fd, &(data[1]), 1023*sizeof(char))) != 0)
{
//将读取到的文件内容连同数据块类型标识一起发送给服务器
sendto(sockfd, data, readSize+1, 0, (struct sockaddr *)&servAddr, servAddrLen);
//等待服务器接收确认,然后再上传文件下一块的内容
recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen);
memset(data, 0, sizeof(data));
}
//当文件内容上传完成后,发送上传结束标识
data[0] = 'e';
sendto(sockfd, data, 1, 0, (struct sockaddr *)&servAddr, servAddrLen);
close(fd);
close(sockfd); //关闭客户端套接字
return 0;
}

将以上代码保存为uploadFileClient.c文件中,编译执行。

img 编译执行,我们可以看到客户端将要上传的文件成功上传到了服务器。 注意:

1
因为同一目录下不能存在两个名称相同的文件,因此不要将服务器程序放置到与上传文件所在的目录下;首先要先执行服务器程序uploadFileServer,然后再执行客户端程序uploadFileClient;

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全downloadFileClient函数,该函数是用来从服务器上下载一个指定的文件,实现下载文件的客户端部分。

  • 下载文件要遵循以下步骤:

    1
    首先向服务器发送当前要下载的文件名称;从服务器接收数据块;向服务器发送一个接收确认请求(例如发送OK字符串);重复第2步骤,直到收到的数据块类型为下载文件结束时,关闭当前打开的文件,然后退出程序;
  • 客户端与服务器间所遵循以下下载协议:

    1
    我们定义客户端与服务器间的数据块为16KB(也就是每次传输的数据大小);我们将每个数据块的前1个字节定义为数据块的类型,也就是用于标示该数据块中的内容是文件内容还是下载结束标识,我们使用c标示该数据块为文件内容,e表示下载结束;
  • 提示:首先客户端向服务器先发送要下载文件的名称,告诉服务器要下载哪个文件;然后服务器读取文件,并发送给客户端。下载文件的客户端实现部分与以上案例中介绍的上传文件服务器端的实现大致相同,请仔细参看以上案例的实现。

文件下载工具的服务器端的核心伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//接收要下载的文件名称
recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
//数据块是下载文件的名称,在本地打开该文件
fd = open(filePath, O_RDONLY);
//设置数据块类型为文件内容
data[0] = 'c';
int readSize = 0;
//读取要下载文件的数据
while((readSize = read(fd, &(data[1]), 15*sizeof(char))) != 0)
{
//向客户端发送数据
sendto(sockfd, data, readSize+1, 0, (struct sockaddr *)&clientAddr, clientAddrLen);
//等待客户端的接收确认
recvfrom(sockfd, ack, sizeof(ack), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
//清空data文件数据部分
memset(&(data[1]), 0, sizeof(data)-1);
}
//当文件内容读取完成后,发送下载结束标识
data[0] = 'e';
sendto(sockfd, data, 1, 0, (struct sockaddr *)&clientAddr, clientAddrLen);
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

//定义数据块大小
char data[16];
//定义服务器端口和服务器地址
#define PORT 8889
#define SERVER_IP "127.0.0.1"

int main(int argc, char *argv[])
{
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in servAddr;
int servAddrLen = sizeof(servAddr);
bzero(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);

//由用户传入的要下载文件的名称
char *downLoadFileName = argv[1];
printf("%s\n", argv[1]);
//先在本地创建要下载的文件
int fd = fd = open(downLoadFileName, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
//向服务器发送要上传文件的名称
sendto(sockfd, downLoadFileName, strlen(downLoadFileName), 0, (struct sockaddr *)&servAddr, servAddrLen);

/********** BEGIN **********/
//等待服务器接收确认,然后再继续上传文件内容
int recvLen = 0;
recvLen =recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen);
write(fd, &(data[1]), recvLen - 1);
sendto(sockfd, "OK", strlen("OK"), 0, (struct sockaddr *)&servAddr, servAddrLen);

//data[0] = 'c'; //设置数据块类型为文件内容
//循环读取文件内容(注意:一次实际读取的文件内容最大字符个数为1023)
while((recvLen=recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen)) > 0)
{
//根据自定义的协议来判断客户端发送来的数据块类型
if(data[0] == 'e')
{
//下载文件完成,关闭当前打开的文件
close(fd);
break;
}
else
{
//数据块为下载文件的内容,将内容写入到新创建的文件中
//因为data的第一个字符为文件块类型,所以只需从第二个字符开始写文件
write(fd, &(data[1]), recvLen - 1);
sendto(sockfd, "OK", strlen("OK"), 0, (struct sockaddr *)&servAddr, servAddrLen);
}
memset(data, 0, sizeof(data));
}

/********** END **********/

close(sockfd);

return 0;
}

select机制

相关知识

select机制是一种很常见的多路复用方法,准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。 当调用select()时,由内核根据 IO 状态修改 fd_set 的内容,由此来通知执行了select()的进程的那个 Socket 或文件可读或可写。主要用于 Socket 通信当中。

select函数

使用select()可以完成非阻塞(所谓非阻塞方式 non-block ,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生,则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

1
2
3
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

img

######函数参数 通过传递函数参数告知内核以下三个信息: - 我们所关心的描述符; - 对于每个描述符我们关心的条件(读,写,异常); - 希望等待多长时间(永久等待,等一段时间,不等待)。

函数参数详解如下:

  • 第一个参数 maxfdp1 指定待测试的描述字个数,它的值是待测试的最大描述字加 1 (因此把该参数命名为 maxfdp1),描述字0,1,2...,maxfdp1-1均将被测试。文件描述符是从 0 开始的。
  • 中间的三个参数 readset 、writeset 和 exceptset 指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
1
2
3
4
void FD_ZERO(fd_set *fdset);//清空集合
void FD_SET(int fd, fd_set *fdset);//将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);//将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset);// 检查集合中指定的文件描述符是否可以读写
  • timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
1
2
3
4
5
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
};

这个参数有三种可能:

  • 永远等待下去:仅在有一个描述字准备好 I/O 时才返回。为此,把该参数设置为空指针 NULL 。

  • 等待一段固定时间:在有一个描述字准备好 I/O 时返回,但是不超过由该参数所指向的 timeval 结构中指定的秒数和微秒数。

  • 根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个 timeval 结构,而且其中的定时器值必须为 0。

    函数返回值

    select 函数返回,内核告诉我们:

  • 已准备好的描述符数量。

  • 哪个描述符已准备好读、写、或异常。 使用该返回值,就可以调用相应的 I/O 函数(read,write),并且明确知道该函数不会阻塞。

返回值如下:

  • 返回 -1,表示出错,例如指定的描述符集都没准备好时捕捉到一个信号。
  • 返回 0,表示没有描述符准备好,指定的时间已经超过。
  • 返回正值,表示已经准备好的描述符数,三个描述符集中仍旧打开的位是对应已准备好的描述符位。

整个 select 流程如下:

img

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
......
fd_set readfd;
struct timeval timeout;
int fd;
while(1)
{
ret=select(fd+1,&readfd,NULL,NULL,&timeout);
if(ret)//返回正值
{
......
}
continue;
......

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 将文件描述符加入到读集合中去。
  • 设置 3s 超时机制。
  • 检测 I/O 有变化,读取文件中的数据。
  • 注意:数据读取完后,立刻退出函数体,以免造成死循环.
  • 具体请参见后续测试样例。

本关涉及的代码文件SelectDamo.c的代码框架如下:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFFER_SIZE 1024
/*buffer为数据缓冲区,将读取的到数据填充进来*/
int do_select(int fd, char *buffer)
{
/*********Begin*******/
/**********End********/
return 0;
}
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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int do_select(int fd, char *buffer)
{
/*********Begin*******/
fd_set readfd;
FD_ZERO(&readfd);
FD_SET(fd,&readfd);
struct timeval timeout;
timeout.tv_sec=3;
int ret=-1;
while(1)
{
ret=select(fd+1,&readfd,NULL,NULL,&timeout);
if(ret)//返回正值
{
//int readSize = 0;
//char tempC[BUFFER_SIZE];
int temp=1,length=0;
//while(1){
temp = read(fd, buffer, BUFFER_SIZE);
/*if(buffer[length-1]==0|buffer[length-1]=='\n'){
break;
}else{
readSize = readSize+1;
buffer[length++]=tempC;
}
}*/
break;
}

}
/**********End********/
return 0;
}